//! `/proc/PID/mountinfo` handling. use std::collections::BTreeMap; use std::ffi::{OsStr, OsString}; use std::iter::FromIterator; 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, Ord, PartialOrd)] #[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, }, }) } } #[derive(Clone, Debug)] 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`. #[derive(Clone, Debug)] pub struct MountInfo { entries: BTreeMap, } /// An iterator over entries in a `MountInfo`. pub type Iter<'a> = std::collections::btree_map::Iter<'a, MountId, Entry>; /// An iterator over mutable entries in a `MountInfo`. pub type IterMut<'a> = std::collections::btree_map::IterMut<'a, MountId, 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') .filter(|line| !line.is_empty()) .try_fold(Vec::new(), |mut acc, line| -> Result<_, Error> { let entry = match Entry::parse(line) { Ok(entry) => entry, Err(err) => { bail!( "failed to parse mount info line: {:?}\n error: {}", line, err, ); } }; acc.push(Entry::parse(line)?); Ok(acc) })?; let entries = BTreeMap::from_iter(entries.into_iter().map(|entry| (entry.id, entry))); Ok(Self { entries }) } /// Iterate over mount entries. pub fn iter(&self) -> Iter { self.entries.iter() } /// Check if there exists a mount point for a specific path. /// /// FIXME: Do we need to verify that mount points don't get "hidden" by other higher level /// mount points? For this we'd need to implement mountpoint-tree iteration first, the info for /// which we have available in the `Entry` struct! pub fn path_is_mounted

(&self, path: &P) -> bool where PathBuf: PartialEq

, { self.iter().any(|(_id, entry)| entry.mount_point == *path) } /// Check whether there exists a mount point for a specified source. pub fn source_is_mounted(&self, source: &T) -> bool where OsString: PartialEq, { self.iter() .filter_map(|(_id, entry)| entry.mount_source.as_ref()) .any(|s| *s == *source) } } impl IntoIterator for MountInfo { type Item = (MountId, Entry); type IntoIter = std::collections::btree_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.entries.into_iter() } } impl<'a> IntoIterator for &'a MountInfo { type Item = (&'a MountId, &'a Entry); type IntoIter = Iter<'a>; fn into_iter(self) -> Self::IntoIter { (&self.entries).into_iter() } } impl<'a> IntoIterator for &'a mut MountInfo { type Item = (&'a MountId, &'a mut Entry); type IntoIter = IterMut<'a>; fn into_iter(self) -> Self::IntoIter { (&mut self.entries).into_iter() } } impl std::ops::Deref for MountInfo { type Target = BTreeMap; fn deref(&self) -> &Self::Target { &self.entries } } impl std::ops::DerefMut for MountInfo { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.entries } } #[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"); }