use std::fs::File;
use std::io;
pub fn inode_generation(file: &File) -> io::Result<u32> {
platform(file)
}
#[must_use]
pub fn admitted_filesystem_class(f_type: u64) -> Option<&'static str> {
match f_type {
0xEF53 => Some("ext"),
0x5846_5342 => Some("xfs"),
0x9123_683E => Some("btrfs"),
_ => None,
}
}
#[cfg(target_os = "linux")]
fn platform(file: &File) -> io::Result<u32> {
let unsupported = |reason: String| {
io::Error::new(
io::ErrorKind::Unsupported,
format!("no inode generation for this file: {reason}"),
)
};
let stat = rustix::fs::fstatfs(file)
.map_err(|error| unsupported(format!("the filesystem cannot be classified: {error}")))?;
let class = u64::try_from(stat.f_type).unwrap_or_default();
if admitted_filesystem_class(class).is_none() {
return Err(unsupported(format!(
"filesystem type {class:#x} is not an admitted class (ext, xfs, btrfs)"
)));
}
get_version(file).map_err(|error| unsupported(format!("FS_IOC_GETVERSION is refused: {error}")))
}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
fn get_version(file: &File) -> rustix::io::Result<u32> {
use rustix::ioctl::{Ioctl, IoctlOutput, Opcode, ioctl, opcode};
struct GetVersion {
buffer: core::ffi::c_long,
}
unsafe impl Ioctl for GetVersion {
type Output = u32;
const IS_MUTATING: bool = true;
fn opcode(&self) -> Opcode {
opcode::read::<core::ffi::c_long>(b'v', 1)
}
fn as_ptr(&mut self) -> *mut core::ffi::c_void {
std::ptr::addr_of_mut!(self.buffer).cast()
}
unsafe fn output_from_ptr(
_: IoctlOutput,
pointer: *mut core::ffi::c_void,
) -> rustix::io::Result<u32> {
Ok(unsafe { pointer.cast::<u32>().read() })
}
}
unsafe { ioctl(file, GetVersion { buffer: 0 }) }
}
#[cfg(not(target_os = "linux"))]
fn platform(_file: &File) -> io::Result<u32> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"inode generation numbers are only read on Linux",
))
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[test]
fn no_inode_number_and_generation_name_two_file_lifetimes() {
const CYCLES: usize = 20_000;
const ENOUGH_REUSE: usize = 100;
let directory = tempfile::tempdir().unwrap();
let mut generations: std::collections::HashMap<(u64, [u8; 16]), Vec<u32>> =
std::collections::HashMap::new();
let mut reused = 0_usize;
for cycle in 0..CYCLES {
let path = directory.path().join(format!("file-{cycle}"));
let file = File::create(&path).unwrap();
let identity = crate::file_identity(&file).unwrap();
let generation = inode_generation(&file).unwrap();
assert_eq!(generation, inode_generation(&file).unwrap());
std::fs::remove_file(&path).unwrap();
drop(file);
let earlier = generations
.entry((identity.volume_serial, identity.file_id))
.or_default();
if !earlier.is_empty() {
reused += 1;
}
assert!(
!earlier.contains(&generation),
"inode {:?} was named by generation {generation} in two file lifetimes",
identity.file_id
);
earlier.push(generation);
if reused >= ENOUGH_REUSE {
break;
}
}
println!("inode numbers reused {reused} times; every reuse had a new generation");
}
#[test]
fn a_filesystem_that_is_not_admitted_is_an_error() {
let file = File::open("/proc/self/status").unwrap();
let error = inode_generation(&file).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Unsupported, "{error}");
assert!(
error.to_string().contains("not an admitted class"),
"{error}"
);
}
#[test]
fn an_admitted_filesystem_reports_a_generation() {
let directory = tempfile::tempdir().unwrap();
let file = File::create(directory.path().join("file")).unwrap();
inode_generation(&file).unwrap();
let directory = File::open(directory.path()).unwrap();
inode_generation(&directory).unwrap();
}
}