use std::fs::{self, File};
use std::io::{self, BufRead, BufReader};
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
pub trait ResolveDevice {
fn resolve_device(&self) -> io::Result<PathBuf>;
}
impl ResolveDevice for Path {
fn resolve_device(&self) -> io::Result<PathBuf> {
let metadata = fs::metadata(self)?;
let dev = metadata.dev();
let major = major(dev);
let minor = minor(dev);
resolve_device_from_dev(major, minor)
}
}
impl ResolveDevice for PathBuf {
fn resolve_device(&self) -> io::Result<PathBuf> {
self.as_path().resolve_device()
}
}
impl ResolveDevice for File {
fn resolve_device(&self) -> io::Result<PathBuf> {
let fd = self.as_raw_fd();
let (major, minor) = get_dev_from_fd(fd)?;
resolve_device_from_dev(major, minor)
}
}
impl ResolveDevice for &File {
fn resolve_device(&self) -> io::Result<PathBuf> {
(*self).resolve_device()
}
}
#[inline]
fn major(dev: u64) -> u32 {
((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
}
#[inline]
fn minor(dev: u64) -> u32 {
(dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
}
fn get_dev_from_fd(fd: i32) -> io::Result<(u32, u32)> {
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
let result = unsafe { libc::fstat(fd, &mut stat_buf) };
if result != 0 {
return Err(io::Error::last_os_error());
}
let dev = stat_buf.st_dev;
Ok((major(dev), minor(dev)))
}
fn resolve_device_from_dev(major: u32, minor: u32) -> io::Result<PathBuf> {
if let Some(path) = resolve_via_sysfs(major, minor) {
return Ok(path);
}
if let Some(path) = resolve_via_mountinfo(major, minor)? {
return Ok(path);
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Could not resolve device for dev {}:{}", major, minor),
))
}
fn resolve_via_sysfs(major: u32, minor: u32) -> Option<PathBuf> {
let sysfs_path = format!("/sys/dev/block/{}:{}", major, minor);
let sysfs_path = Path::new(&sysfs_path);
if !sysfs_path.exists() {
return None;
}
let target = fs::read_link(sysfs_path).ok()?;
let device_name = target.file_name()?.to_str()?;
let dev_path = PathBuf::from(format!("/dev/{}", device_name));
dev_path.exists().then_some(dev_path)
}
fn resolve_via_mountinfo(major: u32, minor: u32) -> io::Result<Option<PathBuf>> {
let mountinfo_path = Path::new("/proc/self/mountinfo");
if !mountinfo_path.exists() {
return Ok(None);
}
let file = File::open(mountinfo_path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if let Some(device) = parse_mountinfo_line(&line, major, minor) {
return Ok(Some(device));
}
}
Ok(None)
}
fn parse_mountinfo_line(line: &str, target_major: u32, target_minor: u32) -> Option<PathBuf> {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 10 {
return None;
}
let dev_field = fields.get(2)?;
let (major, minor) = parse_dev_field(dev_field)?;
if major != target_major || minor != target_minor {
return None;
}
let separator_idx = fields.iter().position(|&f| f == "-")?;
let mount_source = fields.get(separator_idx + 2)?;
if mount_source.starts_with('/') {
return Some(PathBuf::from(mount_source));
}
let dev_path = PathBuf::from(format!("/dev/{}", mount_source));
if dev_path.exists() {
return Some(dev_path);
}
None
}
fn parse_dev_field(field: &str) -> Option<(u32, u32)> {
let mut parts = field.split(':');
let major: u32 = parts.next()?.parse().ok()?;
let minor: u32 = parts.next()?.parse().ok()?;
Some((major, minor))
}
pub fn resolve_device<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
path.as_ref().resolve_device()
}
pub fn resolve_device_from_file(file: &File) -> io::Result<PathBuf> {
file.resolve_device()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
#[test]
fn test_major_minor_extraction() {
let dev = 0x0801_u64; assert_eq!(major(dev), 8);
assert_eq!(minor(dev), 1);
}
#[test]
fn test_parse_dev_field() {
assert_eq!(parse_dev_field("8:1"), Some((8, 1)));
assert_eq!(parse_dev_field("254:0"), Some((254, 0)));
assert_eq!(parse_dev_field("invalid"), None);
assert_eq!(parse_dev_field("8:"), None);
assert_eq!(parse_dev_field(":1"), None);
}
#[test]
fn test_resolve_device_for_root() {
let path = Path::new("/");
let result = path.resolve_device();
if result.is_ok() {
let device = result.unwrap();
assert!(device.to_string_lossy().starts_with("/dev"));
}
}
#[test]
fn test_resolve_device_for_temp_file() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
let result = temp_path.resolve_device();
if result.is_ok() {
let device = result.unwrap();
assert!(device.to_string_lossy().starts_with("/dev"));
}
}
#[test]
fn test_resolve_device_from_file() {
let file = File::open("/").unwrap();
let result = file.resolve_device();
if result.is_ok() {
let device = result.unwrap();
assert!(device.to_string_lossy().starts_with("/dev"));
}
}
#[test]
fn test_resolve_device_nonexistent() {
let path = Path::new("/nonexistent/path/that/does/not/exist");
let result = path.resolve_device();
assert!(result.is_err());
}
#[test]
fn test_parse_mountinfo_line() {
let line = "29 1 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw";
let result = parse_mountinfo_line(line, 8, 1);
assert_eq!(result, Some(PathBuf::from("/dev/sda1")));
let result = parse_mountinfo_line(line, 9, 2);
assert!(result.is_none());
}
#[test]
fn test_parse_mountinfo_line_with_special_fs() {
let line = "22 20 0:21 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw";
let result = parse_mountinfo_line(line, 0, 21);
assert!(result.is_none() || result == Some(PathBuf::from("/dev/tmpfs")));
}
#[test]
fn test_pathbuf_resolve_device() {
let pathbuf = PathBuf::from("/");
let result = pathbuf.resolve_device();
if result.is_ok() {
let device = result.unwrap();
assert!(device.to_string_lossy().starts_with("/dev"));
}
}
}