1use std::fs::{self, File};
41use std::io::{self, BufRead, BufReader};
42use std::os::unix::fs::MetadataExt;
43use std::os::unix::io::AsRawFd;
44use std::path::{Path, PathBuf};
45
46pub trait ResolveDevice {
51 fn resolve_device(&self) -> io::Result<PathBuf>;
75}
76
77impl ResolveDevice for Path {
78 fn resolve_device(&self) -> io::Result<PathBuf> {
79 let metadata = fs::metadata(self)?;
80
81 let dev = metadata.dev();
82 let major = major(dev);
83 let minor = minor(dev);
84
85 resolve_device_from_dev(major, minor)
86 }
87}
88
89impl ResolveDevice for PathBuf {
90 fn resolve_device(&self) -> io::Result<PathBuf> {
91 self.as_path().resolve_device()
92 }
93}
94
95impl ResolveDevice for File {
96 fn resolve_device(&self) -> io::Result<PathBuf> {
97 let fd = self.as_raw_fd();
98 let (major, minor) = get_dev_from_fd(fd)?;
99 resolve_device_from_dev(major, minor)
100 }
101}
102
103impl ResolveDevice for &File {
104 fn resolve_device(&self) -> io::Result<PathBuf> {
105 (*self).resolve_device()
106 }
107}
108
109#[inline]
111fn major(dev: u64) -> u32 {
112 ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
113}
114
115#[inline]
117fn minor(dev: u64) -> u32 {
118 (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
119}
120
121fn get_dev_from_fd(fd: i32) -> io::Result<(u32, u32)> {
123 let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
124 let result = unsafe { libc::fstat(fd, &mut stat_buf) };
125
126 if result != 0 {
127 return Err(io::Error::last_os_error());
128 }
129
130 let dev = stat_buf.st_dev;
131 Ok((major(dev), minor(dev)))
132}
133
134fn resolve_device_from_dev(major: u32, minor: u32) -> io::Result<PathBuf> {
140 if let Some(path) = resolve_via_sysfs(major, minor) {
142 return Ok(path);
143 }
144
145 if let Some(path) = resolve_via_mountinfo(major, minor)? {
147 return Ok(path);
148 }
149
150 Err(io::Error::new(
151 io::ErrorKind::NotFound,
152 format!("Could not resolve device for dev {}:{}", major, minor),
153 ))
154}
155
156fn resolve_via_sysfs(major: u32, minor: u32) -> Option<PathBuf> {
161 let sysfs_path = format!("/sys/dev/block/{}:{}", major, minor);
162 let sysfs_path = Path::new(&sysfs_path);
163
164 if !sysfs_path.exists() {
165 return None;
166 }
167
168 let target = fs::read_link(sysfs_path).ok()?;
170
171 let device_name = target.file_name()?.to_str()?;
173
174 let dev_path = PathBuf::from(format!("/dev/{}", device_name));
175 dev_path.exists().then_some(dev_path)
176}
177
178fn resolve_via_mountinfo(major: u32, minor: u32) -> io::Result<Option<PathBuf>> {
193 let mountinfo_path = Path::new("/proc/self/mountinfo");
194 if !mountinfo_path.exists() {
195 return Ok(None);
196 }
197
198 let file = File::open(mountinfo_path)?;
199 let reader = BufReader::new(file);
200
201 for line in reader.lines() {
202 let line = line?;
203 if let Some(device) = parse_mountinfo_line(&line, major, minor) {
204 return Ok(Some(device));
205 }
206 }
207
208 Ok(None)
209}
210
211fn parse_mountinfo_line(line: &str, target_major: u32, target_minor: u32) -> Option<PathBuf> {
213 let fields: Vec<&str> = line.split_whitespace().collect();
214 if fields.len() < 10 {
215 return None;
216 }
217
218 let dev_field = fields.get(2)?;
220 let (major, minor) = parse_dev_field(dev_field)?;
221
222 if major != target_major || minor != target_minor {
223 return None;
224 }
225
226 let separator_idx = fields.iter().position(|&f| f == "-")?;
228
229 let mount_source = fields.get(separator_idx + 2)?;
231
232 if mount_source.starts_with('/') {
233 return Some(PathBuf::from(mount_source));
234 }
235
236 let dev_path = PathBuf::from(format!("/dev/{}", mount_source));
238 if dev_path.exists() {
239 return Some(dev_path);
240 }
241
242 None
243}
244
245fn parse_dev_field(field: &str) -> Option<(u32, u32)> {
247 let mut parts = field.split(':');
248 let major: u32 = parts.next()?.parse().ok()?;
249 let minor: u32 = parts.next()?.parse().ok()?;
250 Some((major, minor))
251}
252
253pub fn resolve_device<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
269 path.as_ref().resolve_device()
270}
271
272pub fn resolve_device_from_file(file: &File) -> io::Result<PathBuf> {
286 file.resolve_device()
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use std::fs::File;
293 use tempfile::TempDir;
294
295 #[test]
296 fn test_major_minor_extraction() {
297 let dev = 0x0801_u64; assert_eq!(major(dev), 8);
301 assert_eq!(minor(dev), 1);
302 }
303
304 #[test]
305 fn test_parse_dev_field() {
306 assert_eq!(parse_dev_field("8:1"), Some((8, 1)));
307 assert_eq!(parse_dev_field("254:0"), Some((254, 0)));
308 assert_eq!(parse_dev_field("invalid"), None);
309 assert_eq!(parse_dev_field("8:"), None);
310 assert_eq!(parse_dev_field(":1"), None);
311 }
312
313 #[test]
314 fn test_resolve_device_for_root() {
315 let path = Path::new("/");
317 let result = path.resolve_device();
318 if result.is_ok() {
320 let device = result.unwrap();
321 assert!(device.to_string_lossy().starts_with("/dev"));
322 }
323 }
324
325 #[test]
326 fn test_resolve_device_for_temp_file() {
327 let temp_dir = TempDir::new().unwrap();
328 let temp_path = temp_dir.path();
329
330 let result = temp_path.resolve_device();
331 if result.is_ok() {
333 let device = result.unwrap();
334 assert!(device.to_string_lossy().starts_with("/dev"));
335 }
336 }
337
338 #[test]
339 fn test_resolve_device_from_file() {
340 let file = File::open("/").unwrap();
341 let result = file.resolve_device();
342 if result.is_ok() {
344 let device = result.unwrap();
345 assert!(device.to_string_lossy().starts_with("/dev"));
346 }
347 }
348
349 #[test]
350 fn test_resolve_device_nonexistent() {
351 let path = Path::new("/nonexistent/path/that/does/not/exist");
352 let result = path.resolve_device();
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_parse_mountinfo_line() {
358 let line = "29 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw";
360 let result = parse_mountinfo_line(line, 8, 1);
361 assert_eq!(result, Some(PathBuf::from("/dev/sda1")));
362
363 let result = parse_mountinfo_line(line, 9, 2);
365 assert!(result.is_none());
366 }
367
368 #[test]
369 fn test_parse_mountinfo_line_with_special_fs() {
370 let line = "22 20 0:21 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw";
372 let result = parse_mountinfo_line(line, 0, 21);
373 assert!(result.is_none() || result == Some(PathBuf::from("/dev/tmpfs")));
376 }
377
378 #[test]
379 fn test_pathbuf_resolve_device() {
380 let pathbuf = PathBuf::from("/");
381 let result = pathbuf.resolve_device();
382 if result.is_ok() {
384 let device = result.unwrap();
385 assert!(device.to_string_lossy().starts_with("/dev"));
386 }
387 }
388}