From 9106093f775a09f899561d7823518e76f9bd6b45 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 23 Sep 2026 12:58:28 -0300 Subject: [PATCH] fix(fs): map open options to valid Android modes (#3624) In plugins/fs/src/lib.rs OpenOptions::android_mode, the mode was built by concatenating r/w/t/a, so open(url, {append: true}) (read defaults to true) produced "ra" and write+truncate+append "wta", which ParcelFileDescriptor.parseMode / ContentResolver reject. The options now map to r, w, wt, wa, rw or rwt. append wins over the default read (no read+append mode exists). create/createNew still have no Android equivalent (documented on the function). Unit tests for the mapping, compiled on the host with cfg(test). --- .changes/fix-fs-android-modes.md | 6 +++ plugins/fs/src/lib.rs | 68 ++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 .changes/fix-fs-android-modes.md diff --git a/.changes/fix-fs-android-modes.md b/.changes/fix-fs-android-modes.md new file mode 100644 index 000000000..dbe1af000 --- /dev/null +++ b/.changes/fix-fs-android-modes.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +Fixed opening Android `content://` URIs with `append` or with `write`, `truncate` and `append` together: the options were turned into invalid modes such as `ra` or `wta`. They now map to the modes Android accepts (`r`, `w`, `wt`, `wa`, `rw`, `rwt`), `append` taking precedence over `read` since there is no read and append mode. diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index cac582bb4..7f57f11cb 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -361,24 +361,23 @@ impl std::os::unix::fs::OpenOptionsExt for OpenOptions { } impl OpenOptions { - #[cfg(target_os = "android")] - fn android_mode(&self) -> String { - let mut mode = String::new(); - - if self.read { - mode.push('r'); + /// The mode passed to `ContentResolver.openAssetFileDescriptor` / `ParcelFileDescriptor.parseMode`, + /// which only accept `r`, `w`, `wt`, `wa`, `rw` and `rwt`. + /// + /// `create` and `create_new` have no equivalent: whether a missing file is created + /// depends on the content provider. + #[cfg(any(target_os = "android", test))] + fn android_mode(&self) -> &'static str { + match (self.read, self.write || self.append) { + (_, false) => "r", + // there is no read + append mode, and `read` defaults to `true` from JavaScript: + // honor the explicit append + (_, true) if self.append => "wa", + (true, true) if self.truncate => "rwt", + (true, true) => "rw", + (false, true) if self.truncate => "wt", + (false, true) => "w", } - if self.write { - mode.push('w'); - } - if self.truncate { - mode.push('t'); - } - if self.append { - mode.push('a'); - } - - mode } } @@ -630,3 +629,38 @@ pub fn init() -> TauriPlugin> { }) .build() } + +#[cfg(test)] +mod tests { + use super::OpenOptions; + + #[test] + fn android_modes_are_valid() { + let mode = |json: &str| { + serde_json::from_str::(json) + .unwrap() + .android_mode() + }; + + // `read` defaults to true when deserialized + assert_eq!(mode(r#"{}"#), "r"); + assert_eq!(mode(r#"{ "read": false }"#), "r"); + assert_eq!(mode(r#"{ "write": true }"#), "rw"); + assert_eq!(mode(r#"{ "write": true, "truncate": true }"#), "rwt"); + assert_eq!(mode(r#"{ "append": true }"#), "wa"); + assert_eq!(mode(r#"{ "read": false, "write": true }"#), "w"); + assert_eq!( + mode(r#"{ "read": false, "write": true, "truncate": true }"#), + "wt" + ); + assert_eq!(mode(r#"{ "read": false, "append": true }"#), "wa"); + assert_eq!( + mode(r#"{ "read": false, "write": true, "truncate": true, "append": true }"#), + "wa" + ); + assert_eq!( + mode(r#"{ "read": false, "write": true, "create": true }"#), + "w" + ); + } +}