use std::path::Path;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FileIdentity {
volume: u64,
index: u64,
}
impl FileIdentity {
#[cfg(unix)]
pub fn of_open_file(file: &std::fs::File) -> std::io::Result<Self> {
use std::os::unix::fs::MetadataExt as _;
let metadata = file.metadata()?;
Ok(Self {
volume: metadata.dev(),
index: metadata.ino(),
})
}
#[cfg(unix)]
pub fn of_path_nofollow(path: &Path) -> std::io::Result<Self> {
use std::os::unix::fs::MetadataExt as _;
let metadata = std::fs::symlink_metadata(path)?;
Ok(Self {
volume: metadata.dev(),
index: metadata.ino(),
})
}
#[cfg(windows)]
pub fn of_open_file(file: &std::fs::File) -> std::io::Result<Self> {
let information = winapi_util::file::information(file)?;
Ok(Self {
volume: information.volume_serial_number(),
index: information.file_index(),
})
}
#[cfg(windows)]
pub fn of_path_nofollow(path: &Path) -> std::io::Result<Self> {
use std::os::windows::fs::OpenOptionsExt as _;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const FILE_SHARE_DELETE: u32 = 0x0000_0004;
let handle = std::fs::OpenOptions::new()
.access_mode(0)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
Self::of_open_file(&handle)
}
#[cfg(not(any(unix, windows)))]
pub fn of_open_file(_file: &std::fs::File) -> std::io::Result<Self> {
Err(unsupported())
}
#[cfg(not(any(unix, windows)))]
pub fn of_path_nofollow(_path: &Path) -> std::io::Result<Self> {
Err(unsupported())
}
}
#[cfg(not(any(unix, windows)))]
fn unsupported() -> std::io::Error {
std::io::Error::new(
std::io::ErrorKind::Unsupported,
"this platform exposes no stable file identity",
)
}
#[cfg(test)]
mod tests {
use super::FileIdentity;
#[test]
fn a_file_matches_itself_through_both_constructors() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("artifact.log");
std::fs::write(&path, b"payload").expect("write");
let by_path = FileIdentity::of_path_nofollow(&path).expect("identity by path");
let file = std::fs::File::open(&path).expect("open");
let by_handle = FileIdentity::of_open_file(&file).expect("identity by handle");
assert_eq!(
by_path, by_handle,
"the same file must have one identity whether reached by path or by handle"
);
}
#[test]
fn a_same_name_replacement_is_a_different_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("artifact.log");
std::fs::write(&path, b"first").expect("write first");
let opened = std::fs::File::open(&path).expect("open first");
let first = FileIdentity::of_open_file(&opened).expect("identity of first");
std::fs::remove_file(&path).expect("remove first");
std::fs::write(&path, b"second").expect("write second");
let second = FileIdentity::of_path_nofollow(&path).expect("identity of second");
assert_ne!(
first, second,
"a replacement at the same path must not inherit the original's identity"
);
assert_eq!(
first,
FileIdentity::of_open_file(&opened).expect("re-identify the open handle"),
"the open handle still names the file it was opened on"
);
}
#[test]
fn two_distinct_files_do_not_share_an_identity() {
let dir = tempfile::tempdir().expect("tempdir");
let left = dir.path().join("left.log");
let right = dir.path().join("right.log");
std::fs::write(&left, b"left").expect("write left");
std::fs::write(&right, b"right").expect("write right");
assert_ne!(
FileIdentity::of_path_nofollow(&left).expect("left identity"),
FileIdentity::of_path_nofollow(&right).expect("right identity"),
);
}
#[test]
fn a_directory_has_an_identity_too() {
let dir = tempfile::tempdir().expect("tempdir");
let nested = dir.path().join("nested");
std::fs::create_dir(&nested).expect("create nested");
assert_ne!(
FileIdentity::of_path_nofollow(dir.path()).expect("parent identity"),
FileIdentity::of_path_nofollow(&nested).expect("nested identity"),
"a directory identity must distinguish a parent from its child"
);
}
#[test]
fn a_missing_path_reports_not_found() {
let dir = tempfile::tempdir().expect("tempdir");
let err = FileIdentity::of_path_nofollow(&dir.path().join("absent.log"))
.expect_err("a missing path has no identity");
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
}