From 711fc9bccd01678a9fbe1a284495f2fa2a698418 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 16:59:03 -0800 Subject: [PATCH] Data Recovery: on Windows the lock file check compares file identity On Windows a recovery area's lock file counted as the one that was opened when the two had the same creation time. NTFS gives a file made within 15 seconds under a removed file's name that file's creation time, so a second running copy could take an area another copy had just made again. The check now compares the volume serial number and file index, as Unix compares the device and inode, and counts a file it cannot read as not the same. The test that a lock file made again is not the one opened now also runs on Windows. --- Cargo.lock | 1 + crates/engine/Cargo.toml | 5 +++++ crates/engine/src/cmd/recovery.rs | 9 +++++---- crates/engine/src/tests/recovery.rs | 5 +++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d018aa027..fff01f81d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6498,6 +6498,7 @@ dependencies = [ "roxmltree 0.21.2", "rustix 1.1.6", "ruzstd", + "same-file", "serde", "serde_json", "skrifa 0.47.0", diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 836d4b744..0fccbcdec 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -67,6 +67,11 @@ log = { workspace = true } [target.'cfg(unix)'.dependencies] rustix = { workspace = true, features = ["std", "process"] } +# Whether a recovery area's lock file is still the one that was opened (`FolderStore::still_named`). +# Already in Cargo.lock (walkdir), so this adds no new crate. +[target.'cfg(windows)'.dependencies] +same-file = "1.0.6" + # The tiles of a HEIC photo decode in parallel on native; wasm stays single-threaded. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] heic-rs = { version = "=0.1.4", default-features = false, features = ["std", "parallel"], optional = true } diff --git a/crates/engine/src/cmd/recovery.rs b/crates/engine/src/cmd/recovery.rs index ffd3badf0..b75066ec6 100644 --- a/crates/engine/src/cmd/recovery.rs +++ b/crates/engine/src/cmd/recovery.rs @@ -342,7 +342,8 @@ impl FolderStore { } /// Does `path` still name `file`? On Unix the two must be the same file (device and inode); on - /// Windows they must have the same creation time; elsewhere `path` must exist. + /// Windows they must be the same file (volume serial number and file index); elsewhere `path` + /// must exist. When either file can't be read, it does not. fn still_named(file: &std::fs::File, path: &std::path::Path) -> bool { #[cfg(unix)] { @@ -354,9 +355,9 @@ impl FolderStore { } #[cfg(windows)] { - use std::os::windows::fs::MetadataExt as _; - match (file.metadata(), std::fs::symlink_metadata(path)) { - (Ok(a), Ok(b)) => a.creation_time() == b.creation_time(), + let opened = file.try_clone().and_then(same_file::Handle::from_file); + match (opened, same_file::Handle::from_path(path)) { + (Ok(a), Ok(b)) => a == b, _ => false, } } diff --git a/crates/engine/src/tests/recovery.rs b/crates/engine/src/tests/recovery.rs index ce76b612c..4994a9072 100644 --- a/crates/engine/src/tests/recovery.rs +++ b/crates/engine/src/tests/recovery.rs @@ -572,8 +572,9 @@ fn an_area_tidied_away_before_it_is_locked_is_busy() { // The folder went before it was checked or its lock file opened. std::fs::remove_dir(&area).unwrap(); assert!(matches!(FolderStore::lock_area(&area), Ok(Lock::Busy))); - // A lock file made again under the same name is not the one opened. - #[cfg(unix)] + // A lock file made again under the same name is not the one opened, also where it keeps the + // removed one's creation time (NTFS does within 15 seconds). + #[cfg(any(unix, windows))] { std::fs::create_dir_all(&area).unwrap(); let opened = open(true);