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).
This commit is contained in:
Lucas Fernandes Nogueira
2026-09-23 12:58:28 -03:00
committed by GitHub
parent 97e865bc13
commit 9106093f77
2 changed files with 57 additions and 17 deletions
+6
View File
@@ -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.
+51 -17
View File
@@ -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<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
})
.build()
}
#[cfg(test)]
mod tests {
use super::OpenOptions;
#[test]
fn android_modes_are_valid() {
let mode = |json: &str| {
serde_json::from_str::<OpenOptions>(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"
);
}
}