Originals: a Windows file index of 0 does not identify a file

same_file::is_same_file treats two files with the same volume serial
number and file index as one file, also when both report index 0. The
Windows comparison now reads both identities through winapi-util and
accepts a match only when both file indexes are nonzero; otherwise the
resolved paths are compared, as for a volume without file indexes.
This commit is contained in:
dotCooCoo
2026-10-26 11:34:06 -07:00
parent 86f2ba81eb
commit 4826fff734
4 changed files with 32 additions and 7 deletions
Generated
+1 -1
View File
@@ -3221,11 +3221,11 @@ dependencies = [
"lightcraft-testkit",
"log",
"proptest",
"same-file",
"serde",
"serde_json",
"thiserror 2.0.21",
"web-time",
"winapi-util",
]
[[package]]
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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 }
+29 -4
View File
@@ -301,13 +301,13 @@ fn compare_files(a: &Path, b: &Path, strict: bool) -> io::Result<bool> {
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<bool> {
}
}
/// 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)));
}
}