Skip to content

fix(path_provider_tvos): surface the tvOS sandbox write restrictions - #6

Merged
DenisovAV merged 3 commits into
mainfrom
fix/tvos-writable-dirs
Jul 21, 2026
Merged

DenisovAV merged 3 commits into
mainfrom
fix/tvos-writable-dirs

Conversation

@MAUstaoglu

Copy link
Copy Markdown
Member

Follow-up to #4. That PR fixed sqflite's default database path; this fixes the package that told everyone to put it there.

The wrong assumption

PathProviderPlugin.swift was written believing tvOS has an iOS-style sandbox, and said so in its header:

tvOS has a normal app sandbox (Documents, Library, Library/Caches, Library/Application Support) … so the standard NSSearchPath* lookups path_provider_foundation uses on iOS work unchanged here.

That is wrong for two of the four directories it names. Measured on a physical Apple TV 4K (tvOS 26.5) by writing a file into each:

directory result
tmp ✅ writable
Library/Caches ✅ writable
Documents ❌ exists, writes denied (errno 1)
Library/Application Support ❌ does not exist and cannot be created (errno 1)

The tvOS simulator permits all of these writes — the same asymmetry that hid the sqflite default-path bug in #4, and the reason this survived the "verified on a physical Apple TV" note in the README.

The concrete defect

getApplicationSupportDirectory() ran createDirectory through try?. On tvOS that creation genuinely fails, the error was swallowed, and the caller got back a path that does not exist and cannot be made — discovering it only at their first write, as an errno naming nothing useful.

It now returns nil, which path_provider surfaces as MissingPlatformDirectoryException at the call site, and logs the reason.

getApplicationDocumentsDirectory() deliberately keeps returning its path. The directory is real and reads work, so reporting it isn't a lie — and returning nil would break cross-platform code that calls it unconditionally. Instead the README now states plainly that writes fail there and points callers at getApplicationCacheDirectory().

Verified on hardware, after the change

Through the public path_provider API on the physical Apple TV:

temporary     -> /…/tmp                       WRITABLE
appCache      -> /…/Library/Caches            WRITABLE
appDocuments  -> /…/Documents                 NOT-WRITABLE (PathAccessException)
appSupport    -> THREW MissingPlatformDirectoryException
library       -> /…/Library                   NOT-WRITABLE (PathAccessException)
downloads     -> null

Matches the documented table exactly, including getLibraryDirectory() being a container you can't write to directly.

Behaviour change

Apps calling getApplicationSupportDirectory() on tvOS now get an exception instead of an unusable path. That's the point — it fails where the problem is. Documented in the CHANGELOG with the migration (getApplicationCacheDirectory()), plus the reminder that tvOS storage is purgeable by platform contract, so durable data belongs on a server or in iCloud key-value storage.

Version 0.0.2 → 0.0.3.

The plugin was written on the assumption that tvOS has a normal iOS-style
sandbox, and its header comment said so: "tvOS has a normal app sandbox
(Documents, Library, Library/Caches, Library/Application Support) ... so
the standard NSSearchPath* lookups work unchanged here."

That is wrong for two of the four directories it names. Measured on a
physical Apple TV 4K (tvOS 26.5) by writing a file into each:

  tmp                          writable
  Library/Caches               writable
  Documents                    exists, writes DENIED (errno 1)
  Library/Application Support  does not exist, CANNOT be created (errno 1)

The tvOS simulator permits all of these writes, which is why it went
unnoticed — the same asymmetry that hid the sqflite_tvos default-path bug
(#4).

The concrete defect: getApplicationSupportDirectory() ran createDirectory
through `try?`, so a genuinely failed creation was swallowed and the
caller received a path that does not exist and cannot be made. The
failure only surfaced at the caller's first write, as an errno naming
nothing useful. It now returns nil, which path_provider surfaces as
MissingPlatformDirectoryException at the call site.

getApplicationDocumentsDirectory() keeps returning its path: the
directory is real and reads work, so reporting it is not a lie — but the
README now says plainly that writes fail there and points callers at
getApplicationCacheDirectory().

Verified on the device through the public path_provider API after the
change:

  temporary     -> writable
  appCache      -> writable
  appDocuments  -> returned, PathAccessException on write
  appSupport    -> MissingPlatformDirectoryException
  library       -> returned, PathAccessException on write
  downloads     -> null
@MAUstaoglu

Copy link
Copy Markdown
Member Author

Reproduced the broken behaviour on hardware, so the before-state is now observed rather than inferred from reading try?.

Same physical Apple TV 4K (tvOS 26.5), same probe, plugin reverted to main:

appCache     -> /…/Library/Caches               exists=true  WRITABLE
appDocuments -> /…/Documents                    exists=true  NOT-WRITABLE (PathAccessException)
appSupport   -> /…/Library/Application Support  exists=false NOT-WRITABLE (PathNotFoundException)

exists=false is the defect in one line: getApplicationSupportDirectory() hands back a path to a directory that isn't there, and the caller only finds out at its first write — as PathNotFoundException, which names nothing about the sandbox.

Before / after through the public path_provider API:

call before (main) after (this PR)
getApplicationCacheDirectory() writable writable
getApplicationDocumentsDirectory() returned, PathAccessException on write unchanged (documented)
getApplicationSupportDirectory() returned a non-existent path, PathNotFoundException on write MissingPlatformDirectoryException at the call

The failure now happens where the problem is, with a message that says what's wrong.

@MAUstaoglu
MAUstaoglu requested a review from DenisovAV July 20, 2026 19:55

@DenisovAV DenisovAV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Swift change + the docs. This is a correct, honest fix — the getApplicationSupportDirectory path is right (fail loud with MissingPlatformDirectoryException instead of handing back a path that can't be created), the do/try/catch replacing try? is the fix, the breaking change is flagged, and the on-device measurement is documented properly. No blockers.

One design note on getApplicationDocumentsDirectory (inline), and a minor: no test covers the new nil-return path. On-device dir behavior can't be unit-tested (the simulator lies, as the PR notes), but a Dart mock-method-channel test could at least assert App Support now surfaces the exception. Optional.

LGTM once you've decided on the Documents point.

case "getTemporaryDirectory":
result(NSTemporaryDirectory())
case "getApplicationDocumentsDirectory":
// Returned for parity with iOS, but NOT writable on tvOS — see above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth deciding explicitly — the PR title says "stop returning directories tvOS cannot write", but Documents (writes denied per your own table) is still returned here. So the most common path_provider usage — getApplicationDocumentsDirectory() + write a file — still fails silently at the first write with the same unhelpful errno this PR fixes for App Support. It's only mitigated by docs ("use getApplicationCacheDirectory instead").

Keeping the path is defensible: Documents exists and reads work (some apps read pre-populated data), nil'ing it would break iOS parity and readers, and writability can depend on entitlements — so returning it lets apps that can write still work, whereas App Support genuinely can't be created. That asymmetry is reasonable.

But the silent-write-failure the PR sets out to kill still lives for the more common directory. Cheap way to make it loud without breaking parity or readers: a one-time NSLog when getApplicationDocumentsDirectory is called on tvOS ("Documents is not writable on a real Apple TV; use the cache directory"). Either add that, or soften the title to reflect that Documents is a docs-only mitigation. Your call — not a blocker.

Review point: the branch stops returning Application Support, which
cannot be created, but still returns Documents, which cannot be written.
So the silent write failure this change set out to kill survived for the
*more common* call — getApplicationDocumentsDirectory() + write a file —
mitigated only by documentation.

Keep returning the path: Documents exists, reads work, some apps ship
pre-populated data there, and returning nil would break iOS parity and
readers. That asymmetry with Application Support is deliberate. But make
the failure traceable — log once, on first request, naming the sandbox
restriction and pointing at getApplicationCacheDirectory().

A `static let` is lazily initialised exactly once per process, so this is
a thread-safe one-shot with no flag to manage.

Verified on a physical Apple TV 4K (tvOS 26.5). Calling
getApplicationDocumentsDirectory() twice, then writing:

  PP_DOC_WRITE FAILED PathAccessException
  PP_SUPPORT   THREW  MissingPlatformDirectoryException

with exactly one warning in the device log for the two calls, plus the
Application Support failure now naming the real cause instead of being
swallowed by `try?`:

  [path_provider_tvos] cannot create .../Library/Application Support:
  You don't have permission to save the file "Application Support" in
  the folder "Library".. The tvOS sandbox only permits writes to
  Library/Caches and tmp.

Note the CLI's simulator log stream filters on the Flutter image, so a
plugin NSLog does not surface in `flutter-tvos run` there; on device it
comes through the console. A simulator run also creates Application
Support without complaint — the divergence from hardware that hid this.
The package had one test asserting the class exists. Replace it with
tests that would actually fail if this branch regressed.

The device behaviour itself cannot be unit-tested — the simulator
happily creates Application Support, which is what hid the bug — so
these lock in the Dart half of the contract the Swift change depends on:
given nil from the native side, the app-facing call must raise
MissingPlatformDirectoryException rather than hand back a path. Also
covered: every getter maps to the right channel method, downloads
short-circuits without a channel round-trip, the Android-only APIs still
throw, and Documents keeps returning its path (nil'ing it would break
iOS parity and readers).

Adds path_provider as a dev_dependency: it is the layer that converts a
null platform result into the exception, so asserting the user-visible
behaviour needs it.

Checked the tests have teeth by making getApplicationSupportPath fall
back to a path instead of returning null — the Application Support test
fails, the rest stay green.
@MAUstaoglu MAUstaoglu changed the title fix(path_provider_tvos): stop returning directories tvOS cannot write fix(path_provider_tvos): surface the tvOS sandbox write restrictions Jul 21, 2026
@MAUstaoglu

Copy link
Copy Markdown
Member Author

Both points addressed, and you were right that the title overclaimed.

Documents: warn rather than soften the title

Took the first option. Keeping the path is the call for the reasons you laid out — Documents exists, reads work, apps ship pre-populated data there, and nil'ing it breaks iOS parity — but you're right that the silent write failure surviving for the more common call undercut the whole point of the change. There's now a one-time NSLog on first request, naming the restriction and pointing at getApplicationCacheDirectory().

static let is lazily initialised exactly once per process, so it's a thread-safe one-shot with no flag to manage.

Retitled anyway: even with the warning, "stop returning directories tvOS cannot write" isn't accurate while Documents is still returned.

Verified on the physical Apple TV 4K (tvOS 26.5) — two calls, one warning:

PP_DOC1       /var/mobile/.../Documents
PP_DOC2       /var/mobile/.../Documents
PP_DOC_WRITE  FAILED PathAccessException
PP_SUPPORT    THREW  MissingPlatformDirectoryException

A side effect of dropping try? that I hadn't expected: the Application Support failure now carries the OS's own message, which used to be swallowed entirely.

[path_provider_tvos] cannot create .../Library/Application Support:
You don't have permission to save the file "Application Support" in the folder "Library"..
The tvOS sandbox only permits writes to Library/Caches and tmp.

One trap worth recording: the CLI's simulator log stream filters on the Flutter image, so a plugin NSLog never appears in flutter-tvos run output there — it has to be read from the simulator's system log. On device it comes through the console normally. I briefly read a "0 occurrences" as the code not working.

Tests

Added. The package had a single test asserting the class exists; it now has six.

To be straight about what they do and don't prove: they can't test the Swift, and the simulator actively lies here — it creates Application Support without complaint, which is exactly what hid this. What they lock in is the Dart half of the contract the Swift change depends on: given nil from native, the app-facing call must raise MissingPlatformDirectoryException rather than hand back a path. Plus channel-method mapping, downloads short-circuiting without a round-trip, the Android-only APIs throwing, and Documents keeping its path.

Checked they have teeth rather than trusting a green run — made getApplicationSupportPath fall back to a path instead of returning null, and the Application Support test fails while the rest stay green.

path_provider is now a dev_dependency, since it's the layer that converts a null platform result into the exception.

@MAUstaoglu
MAUstaoglu requested a review from DenisovAV July 21, 2026 07:08
@DenisovAV
DenisovAV merged commit 79dc1ac into main Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants