diff --git a/proxmox-sys/src/linux/procfs.rs b/proxmox-sys/src/linux/procfs.rs index f4b52eab..1343fb82 100644 --- a/proxmox-sys/src/linux/procfs.rs +++ b/proxmox-sys/src/linux/procfs.rs @@ -15,6 +15,8 @@ use nix::unistd::Pid; use proxmox_tools::fs::file_read_firstline; use proxmox_tools::parse::hex_nibble; +pub mod mountinfo; + /// POSIX sysconf call pub fn sysconf(name: i32) -> i64 { extern "C" { diff --git a/proxmox-sys/src/linux/procfs/mountinfo.rs b/proxmox-sys/src/linux/procfs/mountinfo.rs new file mode 100644 index 00000000..0f5325e2 --- /dev/null +++ b/proxmox-sys/src/linux/procfs/mountinfo.rs @@ -0,0 +1,272 @@ +//! `/proc/PID/mountinfo` handling. + +use std::ffi::{OsStr, OsString}; +use std::os::unix::ffi::OsStrExt; +use std::path::PathBuf; +use std::str::FromStr; + +use failure::{bail, format_err, Error}; +use nix::sys::stat; +use nix::unistd::Pid; + +/// A mount ID as found within `/proc/PID/mountinfo`. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct MountId(usize); + +impl FromStr for MountId { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + s.parse().map(Self) + } +} + +/// A device node entry (major:minor). This is a more strongly typed version of dev_t. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Device { + major: u32, + minor: u32, +} + +impl Device { + pub fn from_dev_t(dev: stat::dev_t) -> Self { + Self { + major: stat::major(dev) as u32, + minor: stat::minor(dev) as u32, + } + } + + pub fn into_dev_t(self) -> stat::dev_t { + stat::makedev(u64::from(self.major), u64::from(self.minor)) + } +} + +impl FromStr for Device { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (major, minor) = s.split_at( + s.find(':') + .ok_or_else(|| format_err!("expected 'major:minor' format"))?, + ); + Ok(Self { + major: major.parse()?, + minor: minor[1..].parse()?, + }) + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct Tag { + pub tag: OsString, + pub value: Option, +} + +impl Tag { + fn parse(tag: &[u8]) -> Result { + Ok(match tag.iter().position(|b| *b == b':') { + Some(pos) => { + let (tag, value) = tag.split_at(pos); + Self { + tag: OsStr::from_bytes(tag).to_owned(), + value: Some(OsStr::from_bytes(&value[1..]).to_owned()), + } + } + None => Self { + tag: OsStr::from_bytes(tag).to_owned(), + value: None, + }, + }) + } +} + +pub struct Entry { + /// unique identifier of the mount (may be reused after being unmounted) + pub id: MountId, + + /// id of the parent (or of self for the top of the mount tree) + pub parent: MountId, + + /// value of st_dev for files on this file system + pub device: Device, + + /// root of the mount within the file system + pub root: PathBuf, + + /// mount point relative to the process' root + pub mount_point: PathBuf, + + /// per-mount options + pub mount_options: OsString, + + /// tags + pub tags: Vec, + + /// Name of the file system in the form "type[.subtype]". + pub fs_type: String, + + /// File system specific mount source information. + pub mount_source: Option, + + /// superblock options + pub super_options: OsString, +} + +impl Entry { + /// Parse a line from a `mountinfo` file. + pub fn parse(line: &[u8]) -> Result { + let mut parts = line.split(u8::is_ascii_whitespace); + + let mut next = || { + parts + .next() + .ok_or_else(|| format_err!("incomplete mountinfo line")) + }; + + let this = Self { + id: std::str::from_utf8(next()?)?.parse()?, + parent: std::str::from_utf8(next()?)?.parse()?, + device: std::str::from_utf8(next()?)?.parse()?, + root: OsStr::from_bytes(next()?).to_owned().into(), + mount_point: OsStr::from_bytes(next()?).to_owned().into(), + mount_options: OsStr::from_bytes(next()?).to_owned(), + tags: next()?.split(|b| *b == b',').try_fold( + Vec::new(), + |mut acc, tag| -> Result<_, Error> { + acc.push(Tag::parse(tag)?); + Ok(acc) + }, + )?, + fs_type: std::str::from_utf8({ + next()?; + next()? + })? + .to_string(), + mount_source: next().map(|src| match src { + b"none" => None, + other => Some(OsStr::from_bytes(other).to_owned()), + })?, + super_options: OsStr::from_bytes(next()?).to_owned(), + }; + + if parts.next().is_some() { + bail!("excess data in mountinfo line"); + } + + Ok(this) + } +} + +// TODO: Add some structure to this? Eg. sort by parent/child relation? Make a tree? +/// Mount info found in `/proc/PID/mountinfo`. +pub struct MountInfo { + entries: Vec, +} + +pub type Iter<'a> = std::slice::Iter<'a, Entry>; + +impl MountInfo { + /// Read the current mount point information. + pub fn read() -> Result { + Self::parse(&std::fs::read("/proc/self/mountinfo")?) + } + + /// Read the mount point information of a specific pid. + pub fn read_for_pid(pid: Pid) -> Result { + Self::parse(&std::fs::read(format!("/proc/{}/mountinfo", pid))?) + } + + /// Parse a `mountinfo` file. + pub fn parse(statstr: &[u8]) -> Result { + let entries = statstr.split(|b| *b == b'\n').try_fold( + Vec::new(), + |mut acc, line| -> Result<_, Error> { + acc.push(Entry::parse(line)?); + Ok(acc) + }, + )?; + + Ok(Self { entries }) + } + + /// Iterate over mount entries. + pub fn iter(&self) -> Iter { + self.entries.iter() + } +} + +#[test] +fn test_entry() { + use std::path::Path; + + let l1: &[u8] = + b"48 32 0:43 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:26 - cgroup \ + cgroup rw,blkio"; + let entry = Entry::parse(l1).expect("failed to parse first mountinfo test entry"); + + assert_eq!(entry.id, MountId(48)); + assert_eq!(entry.parent, MountId(32)); + assert_eq!( + entry.device, + Device { + major: 0, + minor: 43, + } + ); + assert_eq!(entry.root, Path::new("/")); + assert_eq!(entry.mount_point, Path::new("/sys/fs/cgroup/blkio")); + assert_eq!( + entry.mount_options, + OsStr::new("rw,nosuid,nodev,noexec,relatime") + ); + assert_eq!( + entry.tags, + &[Tag { + tag: OsString::from("shared"), + value: Some(OsString::from("26")), + }] + ); + assert_eq!(entry.fs_type, "cgroup"); + assert_eq!( + entry.mount_source.as_ref().map(|s| s.as_os_str()), + Some(OsStr::new("cgroup")) + ); + assert_eq!(entry.super_options, "rw,blkio"); + + let l2 = b"49 28 0:44 / /proxmox/debian rw,relatime shared:27 - autofs systemd-1 \ + rw,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=27726"; + let entry = Entry::parse(l2).expect("failed to parse second mountinfo test entry"); + assert_eq!(entry.id, MountId(49)); + assert_eq!(entry.parent, MountId(28)); + assert_eq!( + entry.device, + Device { + major: 0, + minor: 44, + } + ); + assert_eq!(entry.root, Path::new("/")); + assert_eq!(entry.mount_point, Path::new("/proxmox/debian")); + assert_eq!(entry.mount_options, OsStr::new("rw,relatime")); + assert_eq!( + entry.tags, + &[Tag { + tag: OsString::from("shared"), + value: Some(OsString::from("27")), + }] + ); + assert_eq!(entry.fs_type, "autofs"); + assert_eq!( + entry.mount_source.as_ref().map(|s| s.as_os_str()), + Some(OsStr::new("systemd-1")) + ); + assert_eq!( + entry.super_options, + "rw,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=27726" + ); + + let mount_info = [l1, l2].join(&b"\n"[..]); + MountInfo::parse(&mount_info).expect("failed to parse mount info file"); +}