diff --git a/apps/vectorcraft/src/portal_save.rs b/apps/vectorcraft/src/portal_save.rs index b6dd3cc8e..0357829ae 100644 --- a/apps/vectorcraft/src/portal_save.rs +++ b/apps/vectorcraft/src/portal_save.rs @@ -96,6 +96,12 @@ fn answer(code: u32, results: &HashMap) -> Result) = v.try_clone().ok()?.try_into().ok()?; Some(label) @@ -103,9 +109,9 @@ fn answer(code: u32, results: &HashMap) -> Result Option { +/// `file:///home/me/A%20b.svg` → `/home/me/A b.svg` (its bytes); `None` for a file elsewhere +/// (`file://host/`) or another scheme. +fn file_uri_path(uri: &str) -> Option> { let rest = uri.strip_prefix("file://")?; let rest = rest.strip_prefix("localhost").unwrap_or(rest); if !rest.starts_with('/') { @@ -126,8 +132,7 @@ fn file_uri_path(uri: &str) -> Option { } } } - // `show_dialog` makes rfd's path a string with `to_string_lossy` too. - Some(String::from_utf8_lossy(&out).into_owned()) + Some(out) } /// `saved` as the answer of the save dialog for `request`: the path, then the first extension of @@ -406,6 +411,7 @@ mod tests { #[test] fn file_uris_become_local_paths() { + let file_uri_path = |uri: &str| super::file_uri_path(uri).map(|b| String::from_utf8(b).unwrap()); assert_eq!(file_uri_path("file:///home/me/Untitled-1.vectorcraft").as_deref(), Some("/home/me/Untitled-1.vectorcraft")); assert_eq!(file_uri_path("file:///home/me/A%20b%C3%A9.svg").as_deref(), Some("/home/me/A bé.svg")); assert_eq!(file_uri_path("file://localhost/tmp/x.pdf").as_deref(), Some("/tmp/x.pdf")); @@ -413,6 +419,9 @@ mod tests { assert_eq!(file_uri_path("file:///tmp/100%+1%2").as_deref(), Some("/tmp/100%+1%2")); assert_eq!(file_uri_path("file://host/x.svg"), None); assert_eq!(file_uri_path("sftp://host/x.svg"), None); + // Not UTF-8: the bytes as they are, which `answer` refuses rather than changing them. + assert_eq!(super::file_uri_path("file:///tmp/a%FF.svg"), Some(b"/tmp/a\xff.svg".to_vec())); + assert!(matches!(answer(0, &results("file:///tmp/a%FF.svg", None)), Ok(None))); } #[test] diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index 3ce489f03..de67968da 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -134,8 +134,9 @@ pub fn templates_folder(prefs: &Prefs) -> Option { pub fn templates_dialog_folder(prefs: &Prefs) -> Option { let folder = std::path::PathBuf::from(templates_folder(prefs)?); if !folder.is_dir() { - // Best effort: a folder that can't be made leaves the nearest one that exists. - let _ = std::fs::create_dir_all(&folder); + // Best effort: a folder that can't be made (or automation may not write) leaves the + // nearest one that exists. + let _ = super::create_dir(&folder.to_string_lossy()); } folder.ancestors().find(|a| a.is_dir()).map(|a| a.to_string_lossy().into_owned()).or_else(home_folder) } @@ -1074,6 +1075,13 @@ mod tests_folders { std::fs::write(base.join("blocked"), b"x").unwrap(); prefs.templates_folder = base.join("blocked").join("Templates").to_string_lossy().into_owned(); assert_eq!(super::templates_dialog_folder(&prefs).map(PathBuf::from), Some(base.clone())); + // Automation that may not write there makes no folder. + let outside = base.join("Outside").join("Templates"); + prefs.templates_folder = outside.to_string_lossy().into_owned(); + std::fs::create_dir_all(base.join("Allowed")).unwrap(); + let roots = crate::file_access::AutomationRoots::new(&[&base], &[&base.join("Allowed")]).unwrap().unwrap(); + crate::file_access::confine(Some(&roots), || super::templates_dialog_folder(&prefs)); + assert!(!outside.exists(), "not made"); let _ = std::fs::remove_dir_all(&base); } }