use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Identity {
File {
volume: u64,
file: u64,
},
Path(PathBuf),
}
impl fmt::Display for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File { volume, file } => write!(f, "{volume:x}:{file:x}"),
Self::Path(p) => write!(f, "{}", p.display()),
}
}
}
pub fn of(path: &Path) -> io::Result<Identity> {
let real = std::fs::canonicalize(path)?;
let meta = std::fs::metadata(&real)?;
Ok(
numbers(&real, &meta).map_or(Identity::Path(real), |(volume, file)| Identity::File {
volume,
file,
}),
)
}
#[cfg(unix)]
fn numbers(_path: &Path, meta: &std::fs::Metadata) -> Option<(u64, u64)> {
use std::os::unix::fs::MetadataExt as _;
match (meta.dev(), meta.ino()) {
(_, 0) => None,
(dev, ino) => Some((dev, ino)),
}
}
#[cfg(windows)]
#[allow(unsafe_code)]
fn numbers(path: &Path, _meta: &std::fs::Metadata) -> Option<(u64, u64)> {
use std::os::windows::fs::OpenOptionsExt as _;
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_READ_ATTRIBUTES,
};
let file = std::fs::OpenOptions::new()
.access_mode(FILE_READ_ATTRIBUTES)
.open(path)
.ok()?;
let mut info = BY_HANDLE_FILE_INFORMATION::default();
let got = unsafe { GetFileInformationByHandle(file.as_raw_handle(), &raw mut info) };
if got == 0 {
return None;
}
let index = (u64::from(info.nFileIndexHigh) << 32) | u64::from(info.nFileIndexLow);
match (u64::from(info.dwVolumeSerialNumber), index) {
(_, 0) => None,
pair => Some(pair),
}
}
#[cfg(not(any(unix, windows)))]
fn numbers(_path: &Path, _meta: &std::fs::Metadata) -> Option<(u64, u64)> {
None
}
#[cfg(test)]
mod tests {
use super::of;
use std::fs;
#[test]
fn a_file_is_itself() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("report.slpc");
fs::write(&p, b"x").unwrap();
assert_eq!(of(&p).unwrap(), of(&p).unwrap());
}
#[test]
fn two_files_are_not_each_other() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.slpc");
let b = tmp.path().join("b.slpc");
fs::write(&a, b"x").unwrap();
fs::write(&b, b"x").unwrap();
assert_ne!(of(&a).unwrap(), of(&b).unwrap());
}
#[test]
fn a_relative_path_and_an_absolute_one_are_the_same_file() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("report.slpc");
fs::write(&p, b"x").unwrap();
let previous = std::env::current_dir().unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let relative = of(std::path::Path::new("report.slpc"));
std::env::set_current_dir(previous).unwrap();
assert_eq!(relative.unwrap(), of(&p).unwrap());
}
#[cfg(unix)]
#[test]
fn a_symbolic_link_is_the_file_it_points_at() {
let tmp = tempfile::tempdir().unwrap();
let real = tmp.path().join("report.slpc");
let link = tmp.path().join("link.slpc");
fs::write(&real, b"x").unwrap();
std::os::unix::fs::symlink(&real, &link).unwrap();
assert_eq!(of(&link).unwrap(), of(&real).unwrap());
}
#[test]
fn two_hard_links_are_one_container() {
let tmp = tempfile::tempdir().unwrap();
let a = tmp.path().join("a.slpc");
let b = tmp.path().join("b.slpc");
fs::write(&a, b"x").unwrap();
fs::hard_link(&a, &b).unwrap();
assert_ne!(fs::canonicalize(&a).unwrap(), fs::canonicalize(&b).unwrap());
assert_eq!(of(&a).unwrap(), of(&b).unwrap());
}
#[test]
fn a_file_that_is_not_there_has_no_identity() {
let tmp = tempfile::tempdir().unwrap();
assert!(of(&tmp.path().join("gone.slpc")).is_err());
}
#[test]
fn identity_survives_the_file_being_rewritten_in_place() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("report.slpc");
fs::write(&p, b"first").unwrap();
let before = of(&p).unwrap();
let scratch = tmp.path().join("scratch");
fs::write(&scratch, b"second").unwrap();
fs::rename(&scratch, &p).unwrap();
let after = of(&p).unwrap();
assert_ne!(before, after, "a replaced file is a different file");
assert_eq!(after, of(&p).unwrap());
}
}