#[cfg(windows)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FileIdentity {
Id128 {
volume_serial_number: u64,
file_id: [u8; 16],
},
Id64 {
volume_serial_number: u32,
file_index: u64,
},
}
#[cfg(all(test, windows))]
std::thread_local! {
static FORCE_FALLBACK_ONCE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(all(test, windows))]
pub fn force_identity_fallback_once() {
FORCE_FALLBACK_ONCE.with(|flag| flag.set(true));
}
#[cfg(windows)]
fn should_force_identity_fallback() -> bool {
#[cfg(test)]
{
FORCE_FALLBACK_ONCE.with(|flag| flag.replace(false))
}
#[cfg(not(test))]
{
false
}
}
#[cfg(windows)]
pub fn identity_of(file: &std::fs::File) -> std::io::Result<FileIdentity> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::{ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED, HANDLE};
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, FILE_ID_INFO, FileIdInfo, GetFileInformationByHandle,
GetFileInformationByHandleEx,
};
let handle: HANDLE = file.as_raw_handle();
if !should_force_identity_fallback() {
let mut id128 = FILE_ID_INFO::default();
let succeeded = unsafe {
GetFileInformationByHandleEx(
handle,
FileIdInfo,
(&raw mut id128).cast(),
u32::try_from(size_of::<FILE_ID_INFO>()).unwrap_or(u32::MAX),
)
};
if succeeded != 0 {
return Ok(FileIdentity::Id128 {
volume_serial_number: id128.VolumeSerialNumber,
file_id: id128.FileId.Identifier,
});
}
let error = std::io::Error::last_os_error();
match error.raw_os_error() {
Some(code)
if code == ERROR_NOT_SUPPORTED as i32 || code == ERROR_INVALID_PARAMETER as i32 => {
}
_ => return Err(error),
}
}
let mut info = BY_HANDLE_FILE_INFORMATION::default();
let succeeded = unsafe { GetFileInformationByHandle(handle, &raw mut info) };
if succeeded == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(FileIdentity::Id64 {
volume_serial_number: info.dwVolumeSerialNumber,
file_index: (u64::from(info.nFileIndexHigh) << 32) | u64::from(info.nFileIndexLow),
})
}
#[cfg(windows)]
pub fn current_path_of(file: &std::fs::File) -> std::io::Result<std::path::PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::io::AsRawHandle;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::GetFinalPathNameByHandleW;
let handle: HANDLE = file.as_raw_handle();
let mut buffer: Vec<u16> = vec![0; 512];
loop {
let capacity = u32::try_from(buffer.len()).unwrap_or(u32::MAX);
let written =
unsafe { GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), capacity, 0) };
if written == 0 {
return Err(std::io::Error::last_os_error());
}
if written < capacity {
buffer.truncate(written as usize);
break;
}
buffer.resize(written as usize, 0);
}
Ok(PathBuf::from(OsString::from_wide(&buffer)))
}
#[cfg(windows)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ProcessLiveness {
Exists,
DoesNotExist,
Indeterminate,
}
#[cfg(windows)]
pub fn process_liveness(pid: u32) -> ProcessLiveness {
use windows_sys::Win32::Foundation::{
ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, WaitForSingleObject,
};
if pid == 0 {
return ProcessLiveness::Indeterminate;
}
let handle: HANDLE = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
0,
pid,
)
};
if handle.is_null() {
return match std::io::Error::last_os_error().raw_os_error() {
Some(code) if code == ERROR_INVALID_PARAMETER as i32 => ProcessLiveness::DoesNotExist,
Some(code) if code == ERROR_ACCESS_DENIED as i32 => ProcessLiveness::Exists,
_ => ProcessLiveness::Indeterminate,
};
}
let guard = OwnedProcessHandle(handle);
let waited = unsafe { WaitForSingleObject(guard.0, 0) };
match waited {
WAIT_TIMEOUT => ProcessLiveness::Exists,
WAIT_OBJECT_0 => ProcessLiveness::DoesNotExist,
_ => ProcessLiveness::Indeterminate,
}
}
#[cfg(windows)]
struct OwnedProcessHandle(windows_sys::Win32::Foundation::HANDLE);
#[cfg(windows)]
impl Drop for OwnedProcessHandle {
fn drop(&mut self) {
unsafe { windows_sys::Win32::Foundation::CloseHandle(self.0) };
}
}
#[cfg(all(test, windows))]
mod tests {
use super::{
FileIdentity, ProcessLiveness, current_path_of, force_identity_fallback_once, identity_of,
process_liveness,
};
#[test]
fn process_liveness_of_the_current_process_is_exists() {
assert_eq!(
process_liveness(std::process::id()),
ProcessLiveness::Exists
);
}
#[test]
fn process_liveness_of_pid_zero_is_indeterminate() {
assert_eq!(process_liveness(0), ProcessLiveness::Indeterminate);
}
#[test]
fn process_liveness_of_an_implausible_pid_is_does_not_exist() {
assert_eq!(process_liveness(0x7FFF_FFFF), ProcessLiveness::DoesNotExist);
}
#[test]
fn process_liveness_of_the_system_process_is_exists() {
assert_eq!(process_liveness(4), ProcessLiveness::Exists);
}
#[test]
fn identity_distinguishes_different_files_and_matches_the_same_one() -> std::io::Result<()> {
let directory = std::env::temp_dir();
let suffix = std::process::id();
let path_a = directory.join(format!("prikk-ffi-identity-test-a-{suffix}"));
let path_b = directory.join(format!("prikk-ffi-identity-test-b-{suffix}"));
std::fs::write(&path_a, b"a")?;
std::fs::write(&path_b, b"b")?;
let identity_a = identity_of(&std::fs::File::open(&path_a)?)?;
let identity_b = identity_of(&std::fs::File::open(&path_b)?)?;
let identity_a_reopened = identity_of(&std::fs::File::open(&path_a)?)?;
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
assert!(
matches!(identity_a, FileIdentity::Id128 { .. }),
"the CI runner's NTFS drives are expected to take the primary 128-bit path by default: \
{identity_a:?}"
);
assert_ne!(
identity_a, identity_b,
"two different files must not compare equal"
);
assert_eq!(
identity_a, identity_a_reopened,
"the same file, reopened, must compare equal"
);
Ok(())
}
#[test]
fn identity_fallback_distinguishes_different_files_and_matches_the_same_one()
-> std::io::Result<()> {
let directory = std::env::temp_dir();
let suffix = std::process::id();
let path_a = directory.join(format!("prikk-ffi-identity-fallback-test-a-{suffix}"));
let path_b = directory.join(format!("prikk-ffi-identity-fallback-test-b-{suffix}"));
std::fs::write(&path_a, b"a")?;
std::fs::write(&path_b, b"b")?;
force_identity_fallback_once();
let identity_a = identity_of(&std::fs::File::open(&path_a)?)?;
force_identity_fallback_once();
let identity_b = identity_of(&std::fs::File::open(&path_b)?)?;
force_identity_fallback_once();
let identity_a_reopened = identity_of(&std::fs::File::open(&path_a)?)?;
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
assert!(
matches!(identity_a, FileIdentity::Id64 { .. }),
"the override must have forced the fallback branch, not the primary one: {identity_a:?}"
);
assert!(matches!(identity_b, FileIdentity::Id64 { .. }));
assert!(matches!(identity_a_reopened, FileIdentity::Id64 { .. }));
assert_ne!(
identity_a, identity_b,
"two different files must not compare equal, even via the fallback form"
);
assert_eq!(
identity_a, identity_a_reopened,
"the same file, reopened through the fallback form both times, must compare equal"
);
Ok(())
}
#[test]
fn identity_fallback_override_is_consumed_after_one_call() -> std::io::Result<()> {
let path = std::env::temp_dir().join(format!(
"prikk-ffi-identity-fallback-once-test-{}",
std::process::id()
));
std::fs::write(&path, b"x")?;
force_identity_fallback_once();
let forced = identity_of(&std::fs::File::open(&path)?)?;
let unforced = identity_of(&std::fs::File::open(&path)?)?;
let _ = std::fs::remove_file(&path);
assert!(matches!(forced, FileIdentity::Id64 { .. }));
assert!(
matches!(unforced, FileIdentity::Id128 { .. }),
"the override must not persist past its one call: {unforced:?}"
);
Ok(())
}
#[test]
fn file_identity_is_copy_and_comparable() {
fn assert_bounds<T: Copy + PartialEq + Eq + std::fmt::Debug>() {}
assert_bounds::<FileIdentity>();
}
fn open_directory(path: &std::path::Path) -> std::io::Result<std::fs::File> {
use std::os::windows::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(0x0200_0000)
.open(path)
}
#[test]
fn current_path_of_follows_the_handle_across_a_rename() -> std::io::Result<()> {
let temporary_root = std::env::temp_dir();
let suffix = std::process::id();
let original = temporary_root.join(format!("prikk-ffi-rename-test-original-{suffix}"));
let renamed = temporary_root.join(format!("prikk-ffi-rename-test-renamed-{suffix}"));
let _ = std::fs::remove_dir_all(&original);
let _ = std::fs::remove_dir_all(&renamed);
std::fs::create_dir(&original)?;
let handle = open_directory(&original)?;
let path_before = current_path_of(&handle)?;
let expected_before = std::fs::canonicalize(&original)?;
std::fs::rename(&original, &renamed)?;
let path_after = current_path_of(&handle)?;
let expected_after = std::fs::canonicalize(&renamed)?;
let _ = std::fs::remove_dir_all(&renamed);
assert_eq!(
path_before, expected_before,
"before any rename, the handle's path must match the directory it was opened from"
);
assert_eq!(
path_after, expected_after,
"after renaming the directory out from under the still-open handle, current_path_of \
must report the NEW path -- this is the whole mechanism DC-96 depends on"
);
assert_ne!(
path_before, path_after,
"the rename must actually have been observed, not silently ignored"
);
Ok(())
}
}