Links: Edit Original opens only regular files of a format VectorCraft reads

A folder or app bundle, a link to another kind of file, or a file with
any other extension is refused with a message instead of being handed to
the system's default app. On Windows, a path containing a quotation mark
is refused before it reaches Explorer.
This commit is contained in:
aiongg
2026-10-16 16:39:46 +00:00
parent 8bc6532928
commit b46ffd0059
4 changed files with 70 additions and 2 deletions
+8
View File
@@ -386,6 +386,7 @@ fn pick_now(request: PickRequest, _parent: &Parent) -> Vec<String> {
/// File → Show in Folder: select `path` in Finder / Explorer, or open its folder elsewhere.
fn reveal(path: &str) -> Result<(), String> {
explorer_safe(path)?;
reveal_command(path).spawn().map(|_| ()).map_err(|e| format!("can't show {path}: {e}"))
}
@@ -548,8 +549,15 @@ fn services(parent: Parent, ui: egui::Context) -> Services {
}
}
/// Windows passes `path` to Explorer inside quotes it adds itself, so a path holding a quote (which
/// no Windows file name has) is refused rather than split into other arguments.
fn explorer_safe(path: &str) -> Result<(), String> {
if cfg!(windows) && path.contains('"') { Err(format!("can't open {path}: the path contains a quotation mark")) } else { Ok(()) }
}
/// Edit Original, Show Package: open `path` (a file or a folder) in the system's default app for it.
fn open_file(path: &str) -> Result<(), String> {
explorer_safe(path)?;
#[cfg(windows)]
let mut c = {
use std::os::windows::process::CommandExt as _;
+1 -1
View File
@@ -733,7 +733,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"links.editOriginal",
"Edit Original",
"",
"{id?} open the file of linked image `id` (default: the selected linked image) in the system's default app for its type (Links panel, Edit › Edit Original); a placed document's file opens here in a new tab, and saving it updates the documents placing it → {path}; edits saved elsewhere show after links.update",
"{id?} open the file of linked image `id` (default: the selected linked image) in the system's default app for its type (Links panel, Edit › Edit Original; only a regular file with the extension of a format document.open reads); a placed document's file opens here in a new tab, and saving it updates the documents placing it → {path}; edits saved elsewhere show after links.update",
),
(
"links.reveal",
+25 -1
View File
@@ -205,10 +205,34 @@ pub(crate) fn open_file(app: &mut VectorcraftApp, p: &Value, reveal: bool) -> Re
crate::dialogs::settle(app);
return Ok(json!({ "path": path }));
}
if reveal { crate::io::reveal_path(app, &path) } else { crate::io::open_in_app(app, &path) }?;
if reveal {
crate::io::reveal_path(app, &path)?;
} else {
openable_original(&path)?;
crate::io::open_in_app(app, &path)?;
}
Ok(json!({ "path": path }))
}
/// Whether Edit Original may hand `path` to the system's default app: a regular file (after
/// following links) whose name, and the name it links to, have the extension of a format
/// VectorCraft reads. Anything else (a folder or app bundle, a script, a shortcut) is refused.
pub(crate) fn openable_original(path: &str) -> Result<(), String> {
let readable = |p: &std::path::Path| {
let ext = p.extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default();
!ext.is_empty() && vectorcraft_engine::cmd::fileio::FORMATS.iter().any(|f| f.read && f.extensions.contains(&ext.as_str()))
};
let name = std::path::Path::new(path).file_name().map_or_else(|| path.to_string(), |n| n.to_string_lossy().into_owned());
let real = std::fs::canonicalize(path).map_err(|e| format!("can't open “{name}”: {e}"))?;
if !real.is_file() {
return Err(format!("“{name}” is not a file"));
}
if !readable(std::path::Path::new(path)) || !readable(&real) {
return Err(format!("Edit Original opens image and document files only; “{name}” is not one"));
}
Ok(())
}
/// A status badge: missing (red, ?), modified (amber, !), embedded (the image icon).
fn badge(ui: &Ui, r: egui::Rect, status: &str) {
let t = Tokens::get(ui.ctx());
+36
View File
@@ -111,6 +111,42 @@ fn edit_original_and_show_in_folder_hand_the_file_to_the_system() {
assert!(app.run("links.editOriginal", json!({"id": linked})).is_err());
}
/// Edit Original hands only regular image and document files to the system's default app.
#[test]
fn edit_original_opens_image_and_document_files_only() {
let dir = Folder::new("kinds");
for f in ["photo.png", "Art.SVG", "run.exe", "run.bat", "photo.png.lnk", "page.hta", "app.desktop"] {
dir.file(f, b"");
}
for f in ["Tool.app", "folder.png"] {
std::fs::create_dir_all(dir.0.join(f)).unwrap();
}
let ok = |f: &str| super::links::openable_original(&dir.0.join(f).to_string_lossy()).is_ok();
assert!(ok("photo.png") && ok("Art.SVG"));
for f in ["run.exe", "run.bat", "photo.png.lnk", "page.hta", "app.desktop", "Tool.app", "folder.png", "missing.png", ""] {
assert!(!ok(f), "{f}");
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(dir.0.join("app.desktop"), dir.0.join("link.png")).unwrap();
assert!(!ok("link.png"), "an image name linking to another kind of file");
// The command refuses a linked image that became such a link, without opening anything.
let (mut app, linked, _) = app(&dir);
let opened: Rc<RefCell<Vec<String>>> = Rc::default();
let o = opened.clone();
app.services.open_file = Some(Box::new(move |p: &str| {
o.borrow_mut().push(p.to_string());
Ok(())
}));
let png = dir.0.join("linked.png");
std::fs::remove_file(&png).unwrap();
std::os::unix::fs::symlink(dir.0.join("app.desktop"), &png).unwrap();
let e = app.run("links.editOriginal", json!({"id": linked})).unwrap_err();
assert!(e.contains("image and document files only"), "{e}");
assert!(opened.borrow().is_empty());
}
}
#[test]
fn placement_options_dialog_sets_the_options() {
let dir = Folder::new("dialog");