use regex::Regex;
use std::fs::File;
use std::io::{self, BufRead};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;
mod error;
mod fstype;
pub use error::{MountInfoError, ParseLineError};
pub use fstype::FsType;
#[derive(Debug)]
pub struct MountPoint {
pub id: Option<u32>,
pub parent_id: Option<u32>,
pub root: Option<PathBuf>,
pub what: String,
pub path: PathBuf,
pub fstype: FsType,
pub options: MountOptions,
}
static PROC_MOUNTINFO_LINE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(\d*)\s(\d*)\s(\d*:\d*)\s([\S]*)\s([\S]*)\s([A-Za-z0-9,]*)\s([A-Za-z0-9:\s]*)\s\- ([\S]*)\s([\S]*)(.*)").unwrap()
});
impl MountPoint {
fn parse_proc_mountinfo_line(line: &str) -> Result<Self, ParseLineError> {
if !PROC_MOUNTINFO_LINE_RE.is_match(line) {
return Err(ParseLineError::InvalidFormat);
}
let caps = PROC_MOUNTINFO_LINE_RE
.captures(line)
.ok_or(ParseLineError::MissingCaptureGroups)?;
Ok(MountPoint {
id: Some(
caps[1]
.parse::<u32>()
.map_err(ParseLineError::InvalidMountId)?,
),
parent_id: Some(
caps[2]
.parse::<u32>()
.map_err(ParseLineError::InvalidParentId)?,
),
root: Some(PathBuf::from(caps[4].to_string())),
path: PathBuf::from(caps[5].to_string()),
options: MountOptions::new(&caps[6]),
fstype: FsType::from_str(&caps[8]).unwrap(),
what: caps[9].to_string(),
})
}
}
#[derive(Debug, PartialEq)]
pub enum ReadWrite {
ReadOnly,
ReadWrite,
}
#[derive(Debug)]
pub struct MountOptions {
pub read_write: ReadWrite,
pub others: Vec<String>,
}
impl MountOptions {
pub fn new(options: &str) -> Self {
let mut read_write = ReadWrite::ReadOnly;
let mut others = Vec::new();
for option in options.split(',') {
match option {
"ro" => read_write = ReadWrite::ReadOnly,
"rw" => read_write = ReadWrite::ReadWrite,
&_ => others.push(option.to_owned()),
}
}
MountOptions { read_write, others }
}
}
#[derive(Debug)]
pub struct MountInfo {
pub mounting_points: Vec<MountPoint>,
}
impl MountInfo {
const MOUNT_INFO_FILE: &'static str = "/proc/self/mountinfo";
const MTAB_FILE: &'static str = "/etc/mtab";
pub fn new() -> Result<Self, MountInfoError> {
match Self::new_from_proc() {
Err(MountInfoError::NoMountInfoFile) => {}
x => {
return x;
}
};
if Path::new(MountInfo::MTAB_FILE).exists() {
let mut mtab = File::open(MountInfo::MTAB_FILE)?;
Ok(MountInfo {
mounting_points: MountInfo::parse_mtab(&mut mtab).map_err(MountInfoError::Io)?,
})
} else {
Err(MountInfoError::NoMountInfoFile)
}
}
pub fn new_from_proc() -> Result<Self, MountInfoError> {
if Path::new(MountInfo::MOUNT_INFO_FILE).exists() {
let mut mtab = File::open(MountInfo::MOUNT_INFO_FILE)?;
Ok(MountInfo {
mounting_points: MountInfo::parse_proc_mountinfo(&mut mtab)?,
})
} else {
Err(MountInfoError::NoMountInfoFile)
}
}
pub fn contains<P: AsRef<Path>>(&self, mounting_point: P, fstype: FsType) -> bool {
let path = mounting_point.as_ref();
self.mounting_points
.iter()
.any(|mts| mts.path == path && mts.fstype == fstype)
}
pub fn is_mounted<P: AsRef<Path>>(&self, path: P) -> bool {
self.mounting_points
.iter()
.any(|mts| mts.path == path.as_ref())
}
fn parse_proc_mountinfo(
file: &mut dyn std::io::Read,
) -> Result<Vec<MountPoint>, MountInfoError> {
let mut result = Vec::new();
let reader = io::BufReader::new(file);
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
let mpoint = MountPoint::parse_proc_mountinfo_line(&line).map_err(|source| {
MountInfoError::ParseError {
line: line_num + 1,
source,
}
})?;
result.push(mpoint);
}
Ok(result)
}
fn parse_mtab(file: &mut dyn std::io::Read) -> Result<Vec<MountPoint>, std::io::Error> {
let mut results: Vec<MountPoint> = vec![];
let reader = io::BufReader::new(file);
for line in reader.lines() {
let l = line?;
let parts: Vec<&str> = l.split_whitespace().collect();
if !parts.is_empty() {
results.push(MountPoint {
what: parts[0].to_string(),
path: PathBuf::from(parts[1]),
fstype: FsType::from_str(parts[2]).unwrap(),
options: MountOptions::new(parts[3]),
id: None,
parent_id: None,
root: None,
})
}
}
Ok(results)
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io::Cursor;
#[test]
fn test_load_mount_points() {
let mut file = Cursor::new(b"tmpfs /tmp tmpfs rw,seclabel,nosuid,nodev,size=8026512k,nr_inodes=1048576,inode64 0 0");
let munt_points = MountInfo::parse_mtab(&mut file).unwrap();
assert_eq!(munt_points.len(), 1);
assert_eq!(munt_points[0].what, "tmpfs".to_owned());
assert_eq!(munt_points[0].path, PathBuf::from("/tmp"));
assert_eq!(munt_points[0].fstype, FsType::Tmpfs);
}
#[test]
fn test_contains() {
let mut file = Cursor::new(b"tmpfs /tmp tmpfs rw,seclabel,nosuid,nodev,size=8026512k,nr_inodes=1048576,inode64 0 0");
let mtab = MountInfo {
mounting_points: MountInfo::parse_mtab(&mut file).unwrap(),
};
assert_eq!(mtab.contains("/tmp", FsType::Tmpfs), true);
}
#[test]
fn test_is_mounted() {
let mut file = Cursor::new(b"tmpfs /tmp tmpfs rw,seclabel,nosuid,nodev,size=8026512k,nr_inodes=1048576,inode64 0 0");
let mtab = MountInfo {
mounting_points: MountInfo::parse_mtab(&mut file).unwrap(),
};
assert_eq!(mtab.is_mounted("/tmp"), true);
}
#[test]
fn test_mount_options() {
let options =
MountOptions::new("rw,seclabel,nosuid,nodev,size=8026512k,nr_inodes=1048576,inode64");
assert_eq!(options.read_write, ReadWrite::ReadWrite);
assert_ne!(options.others.len(), 0);
let more_options =
MountOptions::new("ro,seclabel,nosuid,nodev,size=8026512k,nr_inodes=1048576,inode64");
assert_eq!(more_options.read_write, ReadWrite::ReadOnly);
assert_ne!(more_options.others.len(), 0);
}
#[test]
fn parse_proc_mountinfo_line() {
let line: String = "48 46 253:15 / /boot/efi rw,relatime shared:46 - vfat /dev/vda15 rw,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro".into();
let mount_point = MountPoint::parse_proc_mountinfo_line(&line).unwrap();
assert_eq!(Some(48), mount_point.id);
assert_eq!(Some(46), mount_point.parent_id);
assert_eq!(Some(PathBuf::from("/")), mount_point.root);
assert_eq!(&PathBuf::from("/boot/efi"), &mount_point.path);
assert_eq!("/dev/vda15", &mount_point.what);
assert_eq!(FsType::Other("vfat".into()), mount_point.fstype);
assert_eq!(ReadWrite::ReadWrite, mount_point.options.read_write);
}
}