diff --git a/Cargo.lock b/Cargo.lock index 3e9acf98c..0fcf8a35c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3221,11 +3221,11 @@ dependencies = [ "lightcraft-testkit", "log", "proptest", - "same-file", "serde", "serde_json", "thiserror 2.0.21", "web-time", + "winapi-util", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fc2d90d2e..4e122eb24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +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 +winapi-util = "0.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" diff --git a/crates/catalog/Cargo.toml b/crates/catalog/Cargo.toml index 7a9c80439..b12e18803 100644 --- a/crates/catalog/Cargo.toml +++ b/crates/catalog/Cargo.toml @@ -21,7 +21,7 @@ chrono = { workspace = true } web-time = { workspace = true } [target.'cfg(windows)'.dependencies] -same-file = { workspace = true } +winapi-util = { workspace = true } [dev-dependencies] lightcraft-geom = { workspace = true } diff --git a/crates/catalog/src/safe_file.rs b/crates/catalog/src/safe_file.rs index 9b8bf5bcd..bc05d4139 100644 --- a/crates/catalog/src/safe_file.rs +++ b/crates/catalog/src/safe_file.rs @@ -301,13 +301,13 @@ fn compare_files(a: &Path, b: &Path, strict: bool) -> io::Result { return Ok(false); } // 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. + // its original. A volume that reports file index 0 is compared by resolved path below. // On ReFS two files can share the 64-bit file index, so the lengths are compared first. #[cfg(windows)] { - match ::same_file::is_same_file(a, b) { - Ok(true) => return Ok(true), - Ok(false) => {} + match file_ids(a, b) { + Ok((ia, ib)) if same_file_id(ia, ib) => return Ok(true), + Ok(_) => {} Err(e) if strict => return Err(e), Err(_) => {} } @@ -319,6 +319,22 @@ fn compare_files(a: &Path, b: &Path, strict: bool) -> io::Result { } } +/// The volume serial number and file index of `a` and `b`, read while both files are open. +#[cfg(windows)] +fn file_ids(a: &Path, b: &Path) -> io::Result<((u64, u64), (u64, u64))> { + use winapi_util::{Handle, file::information}; + let (ha, hb) = (Handle::from_path_any(a)?, Handle::from_path_any(b)?); + let (ia, ib) = (information(&ha)?, information(&hb)?); + Ok(((ia.volume_serial_number(), ia.file_index()), (ib.volume_serial_number(), ib.file_index()))) +} + +/// Whether two (volume serial number, file index) pairs name one file. A file index of 0 is not +/// an identity and matches nothing. +#[cfg(any(windows, test))] +fn same_file_id(a: (u64, u64), b: (u64, u64)) -> bool { + a.1 != 0 && b.1 != 0 && a == b +} + #[cfg(test)] mod tests { use super::*; @@ -480,4 +496,13 @@ mod tests { assert!(!same_file_strict(&p.join("under-a-file.jpg"), &p).unwrap()); let _ = fs::remove_dir_all(&d); } + + #[test] + fn a_zero_file_index_is_not_a_file_identity() { + assert!(same_file_id((7, 42), (7, 42))); + assert!(!same_file_id((7, 42), (7, 43))); + assert!(!same_file_id((7, 42), (8, 42))); + assert!(!same_file_id((7, 0), (7, 0)), "two files on a volume that reports index 0 are not the same file"); + assert!(!same_file_id((0, 0), (0, 0))); + } }