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 list_has(self.mount_options.as_slice(), name) || list_has(self.super_options.as_slice(), name);
}
}
fn list_has(options: &[String], name: &str) -> bool {
for option in options {
if option == name {
return true;
}
}
return false;
}
fn is_comma(byte: &u8) -> bool {
return *byte == b',';
}
fn is_space(byte: &u8) -> bool {
return *byte == b' ';
}
fn is_newline(byte: &u8) -> bool {
return *byte == b'\n';
}
fn is_octal_digit(byte: &u8) -> bool {
return (b'0'..=b'7').contains(byte);
}
fn split_options(field: &[u8]) -> Vec<String> {
if field.is_empty() {
return Vec::new();
}
let mut options: Vec<String> = Vec::new();
for option in field.split(is_comma) {
options.push(String::from_utf8_lossy(option).into_owned());
}
return options;
}
fn parse_line(line: &[u8]) -> Option<MountFacts> {
let fields: Vec<&[u8]> = line.split(is_space).collect();
let mut separator_at: Option<usize> = None;
for (position, field) in fields.iter().enumerate() {
if *field == b"-" {
separator_at = Some(position);
break;
}
}
let separator: usize = separator_at?;
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(is_newline)
.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 mut known: std::collections::HashSet<u64> = std::collections::HashSet::new();
for mount in table {
known.insert(mount.mount_id);
}
let mut prefixes: Vec<&Path> = path.ancestors().collect();
prefixes.reverse();
let mut current: Option<&MountFacts> = None;
for prefix in prefixes {
loop {
let linked: Option<&MountFacts> = topmost_linked(table, prefix, current, &known);
let placed_elsewhere: bool = match current {
Some(held) => held.mount_point != prefix,
None => false,
};
let unlinked: Option<&MountFacts> = if linked.is_none() && placed_elsewhere { topmost_unplaced(table, prefix, &known) } else { None };
match linked.or(unlinked) {
Some(found) if is_new_mount(current, found) => current = Some(found),
_ => break,
}
}
}
if current.is_some() {
return current;
}
return longest_mount_holding(table, path);
}
fn topmost_linked<'table>(table: &'table [MountFacts], prefix: &Path, current: Option<&MountFacts>, known: &std::collections::HashSet<u64>) -> Option<&'table MountFacts> {
for mount in table.iter().rev() {
if mount.mount_point != prefix {
continue;
}
let on_top: bool = match current {
Some(held) => mount.parent_id == held.mount_id,
None => !known.contains(&mount.parent_id),
};
if on_top {
return Some(mount);
}
}
return None;
}
fn topmost_unplaced<'table>(table: &'table [MountFacts], prefix: &Path, known: &std::collections::HashSet<u64>) -> Option<&'table MountFacts> {
for mount in table.iter().rev() {
if mount.mount_point == prefix && !known.contains(&mount.parent_id) {
return Some(mount);
}
}
return None;
}
fn is_new_mount(current: Option<&MountFacts>, found: &MountFacts) -> bool {
return match current {
Some(held) => !std::ptr::eq(held, found),
None => true,
};
}
pub fn read_mount_facts(path: &Path) -> Result<Option<MountFacts>, String> {
let real: PathBuf = match std::fs::canonicalize(path) {
Ok(resolved) => resolved,
Err(error) => return Err(format!("cannot resolve {}: {error}", path.display())),
};
let text: Vec<u8> = match std::fs::read(MOUNTINFO_PATH) {
Ok(bytes) => bytes,
Err(error) => return Err(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(is_octal_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;