proxmox/proxmox-sys/src/linux/procfs/mountinfo.rs
Wolfgang Bumiller df5eb296cc sys: mountinfo parsing fixups
* include the failing line on errors
* don't fail on the final empty line...

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-01-17 13:30:28 +01:00

356 lines
10 KiB
Rust

//! `/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 = <usize as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self, Error> {
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<OsString>,
}
impl Tag {
fn parse(tag: &[u8]) -> Result<Self, Error> {
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<Tag>,
/// Name of the file system in the form "type[.subtype]".
pub fs_type: String,
/// File system specific mount source information.
pub mount_source: Option<OsString>,
/// superblock options
pub super_options: OsString,
}
impl Entry {
/// Parse a line from a `mountinfo` file.
pub fn parse(line: &[u8]) -> Result<Self, Error> {
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<MountId, Entry>,
}
/// 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, Error> {
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, Error> {
Self::parse(&std::fs::read(format!("/proc/{}/mountinfo", pid))?)
}
/// Parse a `mountinfo` file.
pub fn parse(statstr: &[u8]) -> Result<Self, Error> {
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<P>(&self, path: &P) -> bool
where
PathBuf: PartialEq<P>,
{
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<T>(&self, source: &T) -> bool
where
OsString: PartialEq<T>,
{
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<MountId, Entry>;
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<MountId, Entry>;
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");
}