Originals: on Windows a hard link to an original counts as that original

safe_file::same_file compared length and canonical paths on every
platform other than Unix. On Windows canonicalize returns a hard link's
own path, so a hard link to a library original under another name was
not the same file, and pin_write_target accepted it as a screenshot
destination. For this reason the engine test
write_targets_pin_absent_paths_and_recheck_the_current_library failed
on Windows.

On Windows same_file now first compares the volume serial number and
file index through the same-file crate (already in Cargo.lock through
walkdir), then falls back to the resolved-path comparison. The catalog
crate takes same-file as a Windows-only dependency.
This commit is contained in:
dotCooCoo
2026-10-25 15:55:21 -07:00
parent 522beb0c3e
commit 7ebbb17a82
4 changed files with 30 additions and 1 deletions
Generated
+1
View File
@@ -3220,6 +3220,7 @@ dependencies = [
"lightcraft-raw",
"log",
"proptest",
"same-file",
"serde",
"serde_json",
"thiserror 2.0.21",
+1
View File
@@ -53,6 +53,7 @@ log = "0.4"
# All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md).
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
rayon = "1"
same-file = "1" # lightcraft-catalog reads a file's volume serial number and file index with it on Windows
proptest = "1"
bytemuck = { version = "1", features = ["derive"] }
crc32fast = "1"
+3
View File
@@ -20,6 +20,9 @@ log = { workspace = true }
chrono = { workspace = true }
web-time = { workspace = true }
[target.'cfg(windows)'.dependencies]
same-file = { workspace = true }
[dev-dependencies]
lightcraft-geom = { workspace = true }
proptest = { workspace = true }
+25 -1
View File
@@ -23,6 +23,7 @@
//! replacing anything** (a hard link, which fails on a taken name; where the volume has no hard
//! links, an exclusive create that claims the name, then a rename over that placeholder).
//! - [`same_file`] tells whether two paths name the same file on disk (device + inode on Unix,
//! volume serial number + file index on Windows,
//! resolved paths elsewhere), for "is this target an original?" checks.
use std::cell::Cell;
@@ -267,7 +268,8 @@ fn same_content(path: &Path, data: &[u8]) -> io::Result<bool> {
}
}
/// Do `a` and `b` name the same existing file (through links, `..`, case-insensitive names)?
/// Do `a` and `b` name the same existing file (through hard links, symlinks, `..`,
/// case-insensitive names)?
pub fn same_file(a: &Path, b: &Path) -> bool {
let (Ok(ma), Ok(mb)) = (fs::metadata(a), fs::metadata(b)) else { return false };
#[cfg(unix)]
@@ -277,6 +279,14 @@ pub fn same_file(a: &Path, b: &Path) -> bool {
}
#[cfg(not(unix))]
{
// A hard link has its own path but shares the volume serial number and file index with
// its original. A volume that does not report them is compared by resolved path below.
#[cfg(windows)]
{
if ::same_file::is_same_file(a, b).unwrap_or(false) {
return true;
}
}
if ma.len() != mb.len() {
return false;
}
@@ -397,6 +407,20 @@ mod tests {
let _ = fs::remove_dir_all(&d);
}
#[test]
fn same_file_sees_a_hard_link_under_another_name() {
let d = temp("same-link");
let p = d.join("IMG_1.jpg");
fs::write(&p, b"original").unwrap();
let linked = d.join("linked.png");
fs::hard_link(&p, &linked).unwrap();
fs::write(d.join("copy.png"), b"original").unwrap();
assert!(same_file(&p, &linked), "a hard link is the same file");
assert!(same_file(&linked, &p));
assert!(!same_file(&p, &d.join("copy.png")), "a copy with the same content and length is another file");
let _ = fs::remove_dir_all(&d);
}
#[test]
fn same_file_sees_through_other_spellings() {
let d = temp("same");