use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
pub const MOUNTINFO_PATH: &str = "/proc/self/mountinfo";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MountFacts {
pub mount_id: u64,
pub parent_id: u64,
pub mount_point: PathBuf,
pub filesystem_type: String,
pub source: String,
pub mount_options: Vec<String>,
pub super_options: Vec<String>,
}
impl MountFacts {
pub fn has_option(&self, name: &str) -> bool {
return self.mount_options.iter().any(|option| { return option == name; })
|| self.super_options.iter().any(|option| { return option == name; });
}
}
fn split_options(field: &[u8]) -> Vec<String> {
if field.is_empty() {
return Vec::new();
}
return field
.split(|byte: &u8| { return *byte == b','; })
.map(|option: &[u8]| { return String::from_utf8_lossy(option).into_owned(); })
.collect();
}
fn parse_line(line: &[u8]) -> Option<MountFacts> {
let fields: Vec<&[u8]> = line.split(|byte: &u8| { return *byte == b' '; }).collect();
let separator: usize = fields.iter().position(|field: &&[u8]| { return *field == b"-"; })?;
if separator < 6 || fields.len() < separator + 4 {
return None;
}
return Some(MountFacts {
mount_id: String::from_utf8_lossy(fields[0]).parse::<u64>().ok()?,
parent_id: String::from_utf8_lossy(fields[1]).parse::<u64>().ok()?,
mount_point: PathBuf::from(OsStr::from_bytes(unmangle_octal(fields[4]).as_slice())),
filesystem_type: String::from_utf8_lossy(fields[separator + 1]).into_owned(),
source: String::from_utf8_lossy(unmangle_octal(fields[separator + 2]).as_slice()).into_owned(),
mount_options: split_options(fields[5]),
super_options: split_options(fields[separator + 3]),
});
}
pub fn parse_mount_table(text: &[u8]) -> Vec<MountFacts> {
return text
.split(|byte: &u8| { return *byte == b'\n'; })
.filter_map(parse_line)
.collect();
}
fn longest_mount_holding<'table>(table: &'table [MountFacts], path: &Path) -> Option<&'table MountFacts> {
let mut best: Option<&MountFacts> = None;
for candidate in table {
if !path.starts_with(candidate.mount_point.as_path()) {
continue;
}
let longer_or_equal: bool = match best {
Some(current) => candidate.mount_point.as_os_str().len() >= current.mount_point.as_os_str().len(),
None => true,
};
if longer_or_equal {
best = Some(candidate);
}
}
return best;
}
pub fn mount_holding<'table>(table: &'table [MountFacts], path: &Path) -> Option<&'table MountFacts> {
let known: std::collections::HashSet<u64> = table.iter().map(|mount: &MountFacts| { return mount.mount_id; }).collect();
let mut prefixes: Vec<&Path> = path.ancestors().collect();
prefixes.reverse();
let mut current: Option<&MountFacts> = None;
for prefix in prefixes {
loop {
let here = |mount: &&MountFacts| -> bool { return mount.mount_point == prefix; };
let on_top = |mount: &&MountFacts| -> bool {
return match current {
Some(held) => mount.parent_id == held.mount_id,
None => !known.contains(&mount.parent_id),
};
};
let linked: Option<&MountFacts> = table.iter().rev().filter(here).find(on_top);
let unlinked: Option<&MountFacts> = if linked.is_none() && current.is_some_and(|held: &MountFacts| { return held.mount_point != prefix; }) {
table.iter().rev().filter(here).find(|mount: &&MountFacts| { return !known.contains(&mount.parent_id); })
} else {
None
};
match linked.or(unlinked) {
Some(found) if current.is_none_or(|held: &MountFacts| { return !std::ptr::eq(held, found); }) => current = Some(found),
_ => break,
}
}
}
return current.or_else(|| { return longest_mount_holding(table, path); });
}
pub fn read_mount_facts(path: &Path) -> Result<Option<MountFacts>, String> {
let real: PathBuf = std::fs::canonicalize(path)
.map_err(|error| { return format!("cannot resolve {}: {error}", path.display()); })?;
let text: Vec<u8> = std::fs::read(MOUNTINFO_PATH)
.map_err(|error| { return format!("cannot read {MOUNTINFO_PATH}: {error}"); })?;
let table: Vec<MountFacts> = parse_mount_table(text.as_slice());
return Ok(mount_holding(table.as_slice(), real.as_path()).cloned());
}
pub fn unmangle_octal(field: &[u8]) -> Vec<u8> {
let mut decoded: Vec<u8> = Vec::with_capacity(field.len());
let mut index: usize = 0;
while index < field.len() {
let is_escape: bool = field[index] == b'\\'
&& index + 3 < field.len()
&& field[index + 1..index + 4]
.iter()
.all(|digit: &u8| { return (b'0'..=b'7').contains(digit); });
if is_escape {
let value: u32 = 64 * u32::from(field[index + 1] & 7)
+ 8 * u32::from(field[index + 2] & 7)
+ u32::from(field[index + 3] & 7);
decoded.push((value & 0xff) as u8);
index += 4;
} else {
decoded.push(field[index]);
index += 1;
}
}
return decoded;
}
#[cfg(test)]
#[path = "fs_mount_options_tests.rs"]
mod tests;