mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-24 21:40:48 +02:00
fix(fs): reject malformed write_file options instead of ignoring them (#3622)
In plugins/fs/src/commands.rs write_file_inner, a malformed or non-ASCII `options` header was dropped with .ok(), so baseDir/append/createNew were silently ignored and the call became a truncating write to the raw path; the JSON array body fallback truncated values above 255 and dropped non-numbers. The header is parsed with parse_write_file_options: empty, `undefined` (what fetch sends for JSON.stringify(undefined)) and `null` mean no options, anything else must parse. Array bodies must only contain bytes. Unit test for the header parser; e2e spec for writes without options, which is the case the `undefined` handling keeps working.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
fs: patch
|
||||||
|
fs-js: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
`writeFile` and `writeTextFile` now fail when their options cannot be parsed instead of silently ignoring them (including `baseDir`, `append` and `createNew`) and writing to the raw path. Data sent as a JSON array is rejected if it contains values that are not bytes, instead of truncating them.
|
||||||
@@ -70,6 +70,21 @@ describePlugin('fs', () => {
|
|||||||
expect(read).toBe('first second')
|
expect(read).toBe('first second')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('writeFile and writeTextFile work without options', async () => {
|
||||||
|
const result = await tauri(async (api, dir) => {
|
||||||
|
const path = await api.path.join(
|
||||||
|
await api.path.appDataDir(),
|
||||||
|
dir,
|
||||||
|
'no-options.txt'
|
||||||
|
)
|
||||||
|
await api.fs.writeTextFile(path, 'text')
|
||||||
|
const text = await api.fs.readTextFile(path)
|
||||||
|
await api.fs.writeFile(path, new Uint8Array([98, 121, 116, 101, 115]))
|
||||||
|
return { text, bytes: await api.fs.readTextFile(path) }
|
||||||
|
}, dir)
|
||||||
|
expect(result).toEqual({ text: 'text', bytes: 'bytes' })
|
||||||
|
})
|
||||||
|
|
||||||
it('writeFile and readFile round-trip binary data', async () => {
|
it('writeFile and readFile round-trip binary data', async () => {
|
||||||
const bytes = [0, 1, 2, 3, 250, 251, 252, 253, 254, 255]
|
const bytes = [0, 1, 2, 3, 250, 251, 252, 253, 254, 255]
|
||||||
const read = await tauri(
|
const read = await tauri(
|
||||||
|
|||||||
@@ -1093,11 +1093,12 @@ async fn write_file_inner<R: Runtime>(
|
|||||||
})
|
})
|
||||||
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
|
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
|
||||||
|
|
||||||
let options: Option<WriteFileOptions> = request
|
let options = request
|
||||||
.headers()
|
.headers()
|
||||||
.get("options")
|
.get("options")
|
||||||
.and_then(|p| p.to_str().ok())
|
.map(|options| parse_write_file_options(options.as_bytes()))
|
||||||
.and_then(|opts| serde_json::from_str(opts).ok());
|
.transpose()?
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let mut file_handle = resolve_file(
|
let mut file_handle = resolve_file(
|
||||||
permission,
|
permission,
|
||||||
@@ -1140,8 +1141,12 @@ async fn write_file_inner<R: Runtime>(
|
|||||||
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
|
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
|
||||||
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
|
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
|
||||||
data.iter()
|
data.iter()
|
||||||
.flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8)))
|
.map(|v| {
|
||||||
.collect(),
|
v.as_u64()
|
||||||
|
.and_then(|v| u8::try_from(v).ok())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("invalid byte in the data to write: {v}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<u8>, _>>()?,
|
||||||
),
|
),
|
||||||
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
|
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
|
||||||
};
|
};
|
||||||
@@ -1157,6 +1162,20 @@ async fn write_file_inner<R: Runtime>(
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the `options` header of the `write_file` command.
|
||||||
|
///
|
||||||
|
/// Fails instead of silently falling back to the defaults, which would e.g. ignore `baseDir`.
|
||||||
|
fn parse_write_file_options(header: &[u8]) -> CommandResult<Option<WriteFileOptions>> {
|
||||||
|
let header = String::from_utf8_lossy(header);
|
||||||
|
match header.trim() {
|
||||||
|
// `JSON.stringify(undefined)` is sent as `undefined` by `fetch`
|
||||||
|
"" | "undefined" | "null" => Ok(None),
|
||||||
|
options => serde_json::from_str(options)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| anyhow::anyhow!("invalid write file options {options}: {e}").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn write_file<R: Runtime>(
|
pub async fn write_file<R: Runtime>(
|
||||||
webview: Webview<R>,
|
webview: Webview<R>,
|
||||||
@@ -1810,6 +1829,25 @@ mod test {
|
|||||||
|
|
||||||
use super::LinesBytes;
|
use super::LinesBytes;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_file_options_header() {
|
||||||
|
use super::parse_write_file_options;
|
||||||
|
|
||||||
|
assert!(parse_write_file_options(b"undefined").unwrap().is_none());
|
||||||
|
assert!(parse_write_file_options(b"null").unwrap().is_none());
|
||||||
|
assert!(parse_write_file_options(b"").unwrap().is_none());
|
||||||
|
|
||||||
|
let options = parse_write_file_options(br#"{"baseDir":14,"append":true}"#)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(options.append);
|
||||||
|
assert!(options.create);
|
||||||
|
assert!(options.base.base_dir.is_some());
|
||||||
|
|
||||||
|
assert!(parse_write_file_options(b"{not json").is_err());
|
||||||
|
assert!(parse_write_file_options(br#"{"append":"yes"}"#).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn safe_file_path_parse() {
|
fn safe_file_path_parse() {
|
||||||
use super::SafeFilePath;
|
use super::SafeFilePath;
|
||||||
|
|||||||
Reference in New Issue
Block a user