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

fix(path_provider_tvos): surface the tvOS sandbox write restrictions#6
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:

directoryresult
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 MissingPlatformDirectoryExceptionat 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.20.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
MemberAuthor

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:

callbefore (main)after (this PR)
getApplicationCacheDirectory()writablewritable
getApplicationDocumentsDirectory()returned, PathAccessException on writeunchanged (documented)
getApplicationSupportDirectory()returned a non-existent path, PathNotFoundException on writeMissingPlatformDirectoryException at the call

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

@DenisovAVDenisovAV 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.
@MAUstaogluMAUstaoglu changed the title fix(path_provider_tvos): stop returning directories tvOS cannot writefix(path_provider_tvos): surface the tvOS sandbox write restrictionsJul 21, 2026
@MAUstaoglu

Copy link
Copy Markdown
MemberAuthor

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.

@DenisovAV
DenisovAV merged commit 79dc1ac into mainJul 21, 2026
Sign up for freeto 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

@MAUstaoglu@DenisovAV