mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Preserve CLI template inputs when output is a hard link
Compare device and inode on Linux alongside existing path checks. Cover template and values aliases with input-preservation and diagnostic checks. Sort the existing footage test's expected paths for temporary-directory portability. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -36,7 +36,20 @@ pub(super) fn validate(cmd: &str, args: &Args) -> Result<(), Failure> {
|
||||
{
|
||||
for input in ["--template", "--values"] {
|
||||
if let Some(path) = args.opt(input)
|
||||
&& std::fs::canonicalize(path).is_ok_and(|path| path == output)
|
||||
&& std::fs::canonicalize(path).is_ok_and(|path| {
|
||||
if path == output {
|
||||
return true;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
// Distinct canonical paths can still name the same file through hard links.
|
||||
if let (Ok(input), Ok(output)) = (std::fs::metadata(&path), std::fs::metadata(&output)) {
|
||||
return input.dev() == output.dev() && input.ino() == output.ino();
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
{
|
||||
return Err(Failure::Error(format!("--out {out} would overwrite {input} {path}")));
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ fn template_output_alias_fixture(case: &str) -> PathBuf {
|
||||
dir
|
||||
}
|
||||
|
||||
fn assert_template_output_preserves_inputs(dir: &PathBuf, out: &str) {
|
||||
fn assert_template_output_preserves_inputs(dir: &PathBuf, out: &str) -> Value {
|
||||
let template_before = std::fs::read(dir.join("lt.ectemplate")).unwrap();
|
||||
let values_before = std::fs::read(dir.join("values.json")).unwrap();
|
||||
// Read the inputs before interpreting the subprocess status or error text.
|
||||
@@ -272,7 +272,8 @@ fn assert_template_output_preserves_inputs(dir: &PathBuf, out: &str) {
|
||||
);
|
||||
assert!(template_preserved, "template rendering must preserve its template input when --out aliases that file");
|
||||
assert!(values_preserved, "template rendering must preserve its values input when --out aliases that file");
|
||||
assert!(!result.status.success(), "an output alias must fail before rendering overwrites an input");
|
||||
assert_eq!(result.status.code(), Some(1), "an output alias must fail before rendering overwrites an input");
|
||||
serde_json::from_slice(&result.stdout).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -652,3 +653,39 @@ fn template_missing_output_does_not_replace_extracted_media() {
|
||||
assert!(media_preserved, "missing --out must fail before overwriting previously extracted media");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn template_frame_output_cannot_alias_its_template_through_a_hard_link() {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let dir = template_output_alias_fixture("hard-linked-template");
|
||||
let input = dir.join("lt.ectemplate");
|
||||
let output = dir.join("hard-linked-template.png");
|
||||
std::fs::hard_link(&input, &output).unwrap();
|
||||
let input_metadata = std::fs::metadata(&input).unwrap();
|
||||
let output_metadata = std::fs::metadata(&output).unwrap();
|
||||
assert_eq!((input_metadata.dev(), input_metadata.ino()), (output_metadata.dev(), output_metadata.ino()), "actual hard-link file identity");
|
||||
assert_ne!(input.canonicalize().unwrap(), output.canonicalize().unwrap(), "the hard link has a distinct canonical path");
|
||||
let reply = assert_template_output_preserves_inputs(&dir, "hard-linked-template.png");
|
||||
assert_eq!(reply["error"], "--out hard-linked-template.png would overwrite --template lt.ectemplate", "{reply}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn template_frame_output_cannot_alias_its_values_through_a_hard_link() {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let dir = template_output_alias_fixture("hard-linked-values");
|
||||
let input = dir.join("values.json");
|
||||
let output = dir.join("hard-linked-values.png");
|
||||
std::fs::hard_link(&input, &output).unwrap();
|
||||
let input_metadata = std::fs::metadata(&input).unwrap();
|
||||
let output_metadata = std::fs::metadata(&output).unwrap();
|
||||
assert_eq!((input_metadata.dev(), input_metadata.ino()), (output_metadata.dev(), output_metadata.ino()), "actual hard-link file identity");
|
||||
assert_ne!(input.canonicalize().unwrap(), output.canonicalize().unwrap(), "the hard link has a distinct canonical path");
|
||||
let reply = assert_template_output_preserves_inputs(&dir, "hard-linked-values.png");
|
||||
assert_eq!(reply["error"], "--out hard-linked-values.png would overwrite --values values.json", "{reply}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
@@ -235,7 +235,8 @@ mod tests {
|
||||
paths.sort();
|
||||
// Windows joins the stored "media/..." onto the project's folder and leaves a path with mixed
|
||||
// separators; `Path` equality compares components, so both spellings are the same file.
|
||||
let expected = ["/nonexistent/ec/abs.png", clip.to_str().unwrap(), "media/ec-relative-gone.png"];
|
||||
let mut expected = ["/nonexistent/ec/abs.png", clip.to_str().unwrap(), "media/ec-relative-gone.png"];
|
||||
expected.sort();
|
||||
assert_eq!(paths.len(), expected.len(), "{paths:?}");
|
||||
for (got, want) in paths.iter().zip(expected) {
|
||||
assert_eq!(std::path::Path::new(got), std::path::Path::new(want), "{paths:?}");
|
||||
|
||||
Reference in New Issue
Block a user