use std::ffi::OsString;
use std::fs::{File, OpenOptions};
use std::io;
use std::mem::MaybeUninit;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
use std::os::windows::io::{AsRawHandle, FromRawHandle};
use std::path::{Path, PathBuf};
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::{
FileIdInfo, FileIdType, GetFileInformationByHandleEx, GetFinalPathNameByHandleW, OpenFileById,
FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_ID_DESCRIPTOR, FILE_ID_DESCRIPTOR_0, FILE_ID_INFO, FILE_READ_ATTRIBUTES,
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
};
pub(crate) struct Anchor(File);
impl Anchor {
pub(crate) fn open(path: &Path) -> io::Result<Self> {
let file = OpenOptions::new()
.access_mode(FILE_READ_ATTRIBUTES)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let metadata = file.metadata()?;
if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io::Error::other("scratch anchor is not a plain directory"));
}
let anchor = reopen_by_id(&file)?;
require_same_directory(&file, &anchor)?;
drop(file);
Ok(Self(anchor))
}
pub(crate) fn remove(self) -> io::Result<()> {
let current = current_path(&self.0)?;
let directory = cap_std::fs::Dir::open_ambient_dir(¤t, cap_std::ambient_authority())?;
let file = directory.into_std_file();
require_same_directory(&self.0, &file)?;
let result = cap_std::fs::Dir::from_std_file(file).remove_open_dir_all();
drop(self);
result
}
}
fn reopen_by_id(original: &File) -> io::Result<File> {
let id = super::fs::file_identity(original)?
.ok_or_else(|| io::Error::other("scratch directory has no reopenable file identity"))?;
let descriptor = FILE_ID_DESCRIPTOR {
dwSize: std::mem::size_of::<FILE_ID_DESCRIPTOR>() as u32,
Type: FileIdType,
Anonymous: FILE_ID_DESCRIPTOR_0 {
FileId: id.file as i64,
},
};
let raw = unsafe {
OpenFileById(
original.as_raw_handle(),
&descriptor,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
)
};
if raw == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
Ok(unsafe { File::from_raw_handle(raw) })
}
fn require_same_directory(original: &File, candidate: &File) -> io::Result<()> {
if identity(original)? != identity(candidate)? {
return Err(io::Error::other(
"scratch directory changed while acquiring cleanup handle",
));
}
Ok(())
}
fn identity(file: &File) -> io::Result<(u64, [u8; 16])> {
let mut info = MaybeUninit::<FILE_ID_INFO>::uninit();
let success = unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle(),
FileIdInfo,
info.as_mut_ptr().cast(),
std::mem::size_of::<FILE_ID_INFO>() as u32,
)
};
if success == 0 {
return Err(io::Error::last_os_error());
}
let info = unsafe { info.assume_init() };
Ok((info.VolumeSerialNumber, info.FileId.Identifier))
}
fn current_path(file: &File) -> io::Result<PathBuf> {
let mut path = vec![0_u16; 32_768];
let length = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle(),
path.as_mut_ptr(),
path.len() as u32,
0,
)
};
if length == 0 {
return Err(io::Error::last_os_error());
}
if length as usize >= path.len() {
return Err(io::Error::other(
"scratch directory path exceeds Win32 limit",
));
}
Ok(PathBuf::from(OsString::from_wide(&path[..length as usize])))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_opened_directory_survives_ancestor_rename_and_cleans_original() {
let root = tempfile::tempdir().unwrap();
let parent = root.path().join("parent");
let scratch = parent.join("scratch");
std::fs::create_dir_all(&scratch).unwrap();
std::fs::write(scratch.join("partial"), b"owned").unwrap();
let by_id = Anchor::open(&scratch).unwrap();
let moved = root.path().join("moved");
std::fs::rename(&parent, &moved).unwrap();
std::fs::create_dir_all(&scratch).unwrap();
std::fs::write(scratch.join("keep"), b"unrelated").unwrap();
by_id.remove().unwrap();
assert!(!moved.join("scratch").exists());
assert_eq!(std::fs::read(scratch.join("keep")).unwrap(), b"unrelated");
}
#[test]
fn cleanup_identity_rejects_a_different_directory() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let original = Anchor::open(first.path()).unwrap();
let replacement = Anchor::open(second.path()).unwrap();
assert!(require_same_directory(&original.0, &replacement.0).is_err());
assert!(require_same_directory(&original.0, &original.0).is_ok());
}
}