mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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 _;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user