From 1064911fc4b047d2ceeafefd24e8bc4c3811c3f5 Mon Sep 17 00:00:00 2001 From: aiongg Date: Fri, 16 Oct 2026 16:09:50 +0000 Subject: [PATCH] Edit: Edit Original opens only regular files with a footage media extension A folder or bundle, a link to another kind of file, or a file whose extension the footage importer doesn't take is refused with a message instead of being handed to the system's default app. --- crates/engine/src/commands/edit.rs | 73 ++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/commands/edit.rs b/crates/engine/src/commands/edit.rs index bbdc43fe..42a2baea 100644 --- a/crates/engine/src/commands/edit.rs +++ b/crates/engine/src/commands/edit.rs @@ -808,8 +808,28 @@ fn original_path(s: &Session) -> Option { } } +/// 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 a media extension the footage +/// importer takes. Anything else (a folder or app bundle, a script, a shortcut) is refused. +pub(crate) fn openable_original(path: &str) -> std::result::Result<(), String> { + let media = |p: &std::path::Path| { + let ext = p.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default(); + crate::media_browser::kind_of(&p.to_string_lossy()).is_some_and(|k| k != "project") || matches!(ext.as_str(), "pdf" | "ai" | "eps") + }; + let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.to_string()); + let real = std::fs::canonicalize(path).map_err(|e| format!("cannot open “{name}”: {e}"))?; + if !real.is_file() { + return Err(format!("“{name}” is not a file")); + } + if !media(std::path::Path::new(path)) || !media(&real) { + return Err(format!("Edit Original opens media files only; “{name}” is not one")); + } + Ok(()) +} + fn edit_original(s: &mut Session, _: &Value) -> Result { let path = original_path(s).ok_or_else(|| super::bad("edit.editOriginal", "select a footage item or layer"))?; + openable_original(&path).map_err(|e| super::bad("edit.editOriginal", e))?; let url = super::layer_menu::file_url(&path); s.events.push(crate::Event::OpenUrl(url.clone())); Ok(json!({"url": url})) @@ -952,20 +972,67 @@ fn always_ok(_: &Session) -> std::result::Result<(), String> { mod tests { use serde_json::json; + fn tmp(name: &str) -> std::path::PathBuf { + let d = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out").join(name); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + /// Edit Original opens the footage as a well-formed `file://` URL, whatever its name contains. #[test] fn edit_original_opens_an_encoded_file_url() { + let clip = tmp("edit-original-url").join("take #1.mov"); + std::fs::write(&clip, b"").unwrap(); + let clip = clip.to_string_lossy().into_owned(); + let url = super::super::layer_menu::file_url(&clip); + assert!(url.ends_with("/take%20%231.mov"), "{url}"); let mut s = crate::Session::default(); let id = s .edit("add", None, |p, _| { - let f = effectcraft_project::Footage { path: "/clips/take #1.mov".into(), ..Default::default() }; + let f = effectcraft_project::Footage { path: clip.clone(), ..Default::default() }; Ok(p.add_item("take", effectcraft_color::Label::Lavender, None, effectcraft_project::ItemKind::Footage(f))) }) .unwrap(); s.state.project_selection = vec![id]; s.drain_events(); let r = s.execute("edit.editOriginal", json!({})).unwrap(); - assert_eq!(r["url"], "file:///clips/take%20%231.mov"); - assert!(s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenUrl(u) if u == "file:///clips/take%20%231.mov"))); + assert_eq!(r["url"], url.as_str()); + assert!(s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenUrl(u) if *u == url))); + } + + /// Only regular media files reach the system opener. + #[test] + fn edit_original_opens_media_files_only() { + let d = tmp("edit-original-kinds"); + for f in ["clip.mov", "Still.PNG", "run.bat", "app.desktop", "clip.mov.lnk"] { + std::fs::write(d.join(f), b"").unwrap(); + } + for f in ["Tool.app", "folder.mov"] { + std::fs::create_dir_all(d.join(f)).unwrap(); + } + let ok = |f: &str| super::openable_original(&d.join(f).to_string_lossy()).is_ok(); + assert!(ok("clip.mov") && ok("Still.PNG")); + for f in ["run.bat", "app.desktop", "clip.mov.lnk", "Tool.app", "folder.mov", "missing.mov", ""] { + assert!(!ok(f), "{f}"); + } + #[cfg(unix)] + { + std::os::unix::fs::symlink(d.join("app.desktop"), d.join("link.png")).unwrap(); + assert!(!ok("link.png"), "a media name linking to another kind of file"); + } + // The command refuses without opening anything. + let mut s = crate::Session::default(); + let path = d.join("run.bat").to_string_lossy().into_owned(); + let id = s + .edit("add", None, |p, _| { + let f = effectcraft_project::Footage { path, ..Default::default() }; + Ok(p.add_item("bat", effectcraft_color::Label::Lavender, None, effectcraft_project::ItemKind::Footage(f))) + }) + .unwrap(); + s.state.project_selection = vec![id]; + s.drain_events(); + assert!(s.execute("edit.editOriginal", json!({})).is_err()); + assert!(!s.drain_events().iter().any(|e| matches!(e, crate::Event::OpenUrl(_)))); } }