fix: Templates folder needs write access; portal saves keep exact file names

This commit is contained in:
Dylan M. Taylor
2026-11-03 22:17:45 +07:00
committed by bflatastic
parent c879ff369b
commit 9b10a40709
2 changed files with 24 additions and 7 deletions
+14 -5
View File
@@ -96,6 +96,12 @@ fn answer(code: u32, results: &HashMap<String, OwnedValue>) -> Result<Option<Sav
.map_err(|e: zbus::zvariant::Error| e.to_string())?;
let uri = uris.first().ok_or("no file in the portal's answer")?;
let path = file_uri_path(uri).ok_or_else(|| format!("not a local file: {uri}"))?;
// A name that isn't UTF-8 can't be written as picked: replacing its bytes would save over (and
// ask to replace) another file. The dialog ends without a file instead.
let Ok(path) = String::from_utf8(path) else {
log::warn!("the portal's save dialog picked a file name that isn't UTF-8: nothing is saved");
return Ok(None);
};
let filter = results.get("current_filter").and_then(|v| {
let (label, _globs): (String, Vec<(u32, String)>) = v.try_clone().ok()?.try_into().ok()?;
Some(label)
@@ -103,9 +109,9 @@ fn answer(code: u32, results: &HashMap<String, OwnedValue>) -> Result<Option<Sav
Ok(Some(Saved { path, filter }))
}
/// `file:///home/me/A%20b.svg` → `/home/me/A b.svg`; `None` for a file elsewhere (`file://host/`)
/// or another scheme.
fn file_uri_path(uri: &str) -> Option<String> {
/// `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<Vec<u8>> {
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<String> {
}
}
}
// `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]
+10 -2
View File
@@ -134,8 +134,9 @@ pub fn templates_folder(prefs: &Prefs) -> Option<String> {
pub fn templates_dialog_folder(prefs: &Prefs) -> Option<String> {
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);
}
}