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:
sanyamsingh001
2026-10-14 02:14:42 +10:00
co-authored by Codex
parent 549e9d8251
commit e52ce29579
3 changed files with 55 additions and 4 deletions
+14 -1
View File
@@ -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}")));
}
+39 -2
View File
@@ -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);
}
+2 -1
View File
@@ -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:?}");