graphforge_filesystem/
inode_generation.rs1use std::fs::File;
11use std::io;
12
13pub fn inode_generation(file: &File) -> io::Result<u32> {
23 platform(file)
24}
25
26#[must_use]
30pub fn admitted_filesystem_class(f_type: u64) -> Option<&'static str> {
31 match f_type {
32 0xEF53 => Some("ext"),
33 0x5846_5342 => Some("xfs"),
34 0x9123_683E => Some("btrfs"),
35 _ => None,
36 }
37}
38
39#[cfg(target_os = "linux")]
40fn platform(file: &File) -> io::Result<u32> {
41 let unsupported = |reason: String| {
42 io::Error::new(
43 io::ErrorKind::Unsupported,
44 format!("no inode generation for this file: {reason}"),
45 )
46 };
47 let stat = rustix::fs::fstatfs(file)
50 .map_err(|error| unsupported(format!("the filesystem cannot be classified: {error}")))?;
51 let class = u64::try_from(stat.f_type).unwrap_or_default();
52 if admitted_filesystem_class(class).is_none() {
53 return Err(unsupported(format!(
54 "filesystem type {class:#x} is not an admitted class (ext, xfs, btrfs)"
55 )));
56 }
57 get_version(file).map_err(|error| unsupported(format!("FS_IOC_GETVERSION is refused: {error}")))
58}
59
60#[cfg(target_os = "linux")]
61#[allow(unsafe_code)]
62fn get_version(file: &File) -> rustix::io::Result<u32> {
63 use rustix::ioctl::{Ioctl, IoctlOutput, Opcode, ioctl, opcode};
64
65 struct GetVersion {
73 buffer: core::ffi::c_long,
74 }
75
76 unsafe impl Ioctl for GetVersion {
81 type Output = u32;
82
83 const IS_MUTATING: bool = true;
84
85 fn opcode(&self) -> Opcode {
86 opcode::read::<core::ffi::c_long>(b'v', 1)
87 }
88
89 fn as_ptr(&mut self) -> *mut core::ffi::c_void {
90 std::ptr::addr_of_mut!(self.buffer).cast()
91 }
92
93 unsafe fn output_from_ptr(
94 _: IoctlOutput,
95 pointer: *mut core::ffi::c_void,
96 ) -> rustix::io::Result<u32> {
97 Ok(unsafe { pointer.cast::<u32>().read() })
100 }
101 }
102
103 unsafe { ioctl(file, GetVersion { buffer: 0 }) }
106}
107
108#[cfg(not(target_os = "linux"))]
109fn platform(_file: &File) -> io::Result<u32> {
110 Err(io::Error::new(
111 io::ErrorKind::Unsupported,
112 "inode generation numbers are only read on Linux",
113 ))
114}
115
116#[cfg(all(test, target_os = "linux"))]
117mod tests {
118 use super::*;
119
120 #[test]
127 fn no_inode_number_and_generation_name_two_file_lifetimes() {
128 const CYCLES: usize = 20_000;
129 const ENOUGH_REUSE: usize = 100;
130 let directory = tempfile::tempdir().unwrap();
131 let mut generations: std::collections::HashMap<(u64, [u8; 16]), Vec<u32>> =
132 std::collections::HashMap::new();
133 let mut reused = 0_usize;
134 for cycle in 0..CYCLES {
135 let path = directory.path().join(format!("file-{cycle}"));
136 let file = File::create(&path).unwrap();
137 let identity = crate::file_identity(&file).unwrap();
138 let generation = inode_generation(&file).unwrap();
139 assert_eq!(generation, inode_generation(&file).unwrap());
141 std::fs::remove_file(&path).unwrap();
142 drop(file);
143 let earlier = generations
144 .entry((identity.volume_serial, identity.file_id))
145 .or_default();
146 if !earlier.is_empty() {
147 reused += 1;
148 }
149 assert!(
150 !earlier.contains(&generation),
151 "inode {:?} was named by generation {generation} in two file lifetimes",
152 identity.file_id
153 );
154 earlier.push(generation);
155 if reused >= ENOUGH_REUSE {
156 break;
157 }
158 }
159 println!("inode numbers reused {reused} times; every reuse had a new generation");
160 }
161
162 #[test]
165 fn a_filesystem_that_is_not_admitted_is_an_error() {
166 let file = File::open("/proc/self/status").unwrap();
167 let error = inode_generation(&file).unwrap_err();
168 assert_eq!(error.kind(), io::ErrorKind::Unsupported, "{error}");
169 assert!(
170 error.to_string().contains("not an admitted class"),
171 "{error}"
172 );
173 }
174
175 #[test]
178 fn an_admitted_filesystem_reports_a_generation() {
179 let directory = tempfile::tempdir().unwrap();
180 let file = File::create(directory.path().join("file")).unwrap();
181 inode_generation(&file).unwrap();
182 let directory = File::open(directory.path()).unwrap();
183 inode_generation(&directory).unwrap();
184 }
185}