From 3b19ca096ca2ad85a1c5d4256d743230c05d1519 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sat, 10 Oct 2026 16:06:02 -0300 Subject: [PATCH] fix(fs): do not parse relative paths with a colon as URLs (#3632) In plugins/fs/src/file_path.rs SafeFilePath::from_str, any string that parsed as a URL with a scheme of 2+ characters was a URL, so a relative path such as `notes:2024.txt` became a cannot-be-a-base URL and failed with InvalidPathUrl. Cannot-be-a-base URLs are now parsed as paths. The scheme-agnostic url.to_file_path() stays: its result is scope checked (not a bypass). Unit test for the parser; e2e spec writes and reads such a file (not on Windows, where `:` is not allowed in file names). --- .changes/fix-fs-url-like-paths.md | 6 ++++ packages/api-e2e/test/specs/fs.spec.ts | 20 +++++++++++++ plugins/fs/src/file_path.rs | 40 +++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changes/fix-fs-url-like-paths.md diff --git a/.changes/fix-fs-url-like-paths.md b/.changes/fix-fs-url-like-paths.md new file mode 100644 index 000000000..f1f4bf5bb --- /dev/null +++ b/.changes/fix-fs-url-like-paths.md @@ -0,0 +1,6 @@ +--- +fs: patch +fs-js: patch +--- + +Fixed relative paths whose first component contains a colon, such as `notes:2024.txt`, being parsed as URLs and rejected with "URL is not a valid path". Only strings with a hierarchical URL form (`scheme://...` or `scheme:/...`) are treated as URLs now. diff --git a/packages/api-e2e/test/specs/fs.spec.ts b/packages/api-e2e/test/specs/fs.spec.ts index 68e9f22a3..203a73e01 100644 --- a/packages/api-e2e/test/specs/fs.spec.ts +++ b/packages/api-e2e/test/specs/fs.spec.ts @@ -8,6 +8,7 @@ import { tauriError, describePlugin, itDesktop, + itOn, scratchDir } from '../helpers/index.js' @@ -394,6 +395,25 @@ describePlugin('fs', () => { expect(message).toMatch(/resource id \d+ is invalid/) }) + // `:` is not allowed in Windows file names + itOn( + ['linux', 'darwin', 'android', 'ios'], + 'relative paths that look like a URL scheme are paths', + async () => { + const result = await tauri(async (api) => { + const baseDir = api.fs.BaseDirectory.AppData + const path = 'e2e-notes:2024.txt' + await api.fs.writeTextFile(path, 'notes', { baseDir }) + try { + return await api.fs.readTextFile(path, { baseDir }) + } finally { + await api.fs.remove(path, { baseDir }) + } + }) + expect(result).toBe('notes') + } + ) + it('rejects paths outside the configured scope', async () => { // `$HOME` itself is not in the example's fs scope (only the app dirs, // `$DOWNLOAD` and `$RESOURCE` are). diff --git a/plugins/fs/src/file_path.rs b/plugins/fs/src/file_path.rs index 86c2aa377..dd0c08998 100644 --- a/plugins/fs/src/file_path.rs +++ b/plugins/fs/src/file_path.rs @@ -203,7 +203,9 @@ impl FromStr for SafeFilePath { type Err = Error; fn from_str(s: &str) -> Result { if let Ok(url) = url::Url::from_str(s) - && url.scheme().len() != 1 + // single letter schemes are Windows drive letters, and URLs that cannot be a base + // (no `//` nor `/` after the scheme) are relative paths such as `notes:2024.txt` + && url.scheme().len() != 1 && !url.cannot_be_a_base() { return Ok(Self::Url(url)); } @@ -306,3 +308,39 @@ impl TryFrom for SafeFilePath { } } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::SafeFilePath; + + #[test] + fn safe_file_path_from_str() { + for url in [ + "file:///C:/Users", + "file:///home/user/file.txt", + "content://com.android.providers/document/1", + "asset://localhost/file.txt", + ] { + assert!( + matches!(SafeFilePath::from_str(url), Ok(SafeFilePath::Url(_))), + "{url}" + ); + } + + for path in [ + "C:/Users", + "C:\\Users", + "notes:2024.txt", + "ab:c", + "dir/file.txt", + "/home/user/file.txt", + ] { + assert!( + matches!(SafeFilePath::from_str(path), Ok(SafeFilePath::Path(_))), + "{path}" + ); + } + } +}