use std::{
collections::BTreeMap,
io::{BufRead, BufReader, Read as _},
};
const LINE: usize = 64 << 10;
const TYPE: usize = 255;
const FDINFO: u64 = 64 << 10;
type Found = Result<Vec<u8>, String>;
pub(crate) struct Mounts(Result<BTreeMap<u64, Found>, String>);
impl Mounts {
pub(crate) fn read() -> Self {
let cannot =
|error: std::io::Error| format!("/proc/self/mountinfo could not be read ({error})");
Self(
std::fs::File::open("/proc/self/mountinfo")
.map_err(cannot)
.and_then(|file| scan(BufReader::new(file)).map_err(cannot)),
)
}
#[cfg(test)]
pub(crate) fn parsed(bytes: &[u8]) -> Self {
Self(Ok(scan(BufReader::new(bytes)).unwrap()))
}
#[cfg(test)]
pub(crate) fn failed(reason: &str) -> Self {
Self(Err(reason.to_owned()))
}
pub(crate) fn unreadable(&self) -> Option<&str> {
self.0.as_ref().err().map(String::as_str)
}
pub(crate) fn type_of(&self, id: u64) -> Result<&[u8], String> {
let mounts = self.0.as_ref().map_err(Clone::clone)?;
match mounts.get(&id) {
Some(Ok(found)) => Ok(found),
Some(Err(reason)) => Err(reason.clone()),
None => Err(format!("its mount, {id}, is not in /proc/self/mountinfo")),
}
}
}
pub(crate) fn scan(mut reader: impl BufRead) -> std::io::Result<BTreeMap<u64, Found>> {
let mut mounts = BTreeMap::new();
let mut line = Vec::new();
while let Some(long) = read_line(&mut reader, &mut line)? {
if let Some((id, found)) = entry(&line, long)
&& mounts.insert(id, found).is_some()
{
mounts.insert(
id,
Err(format!(
"its mount, {id}, is listed twice in /proc/self/mountinfo"
)),
);
}
}
Ok(mounts)
}
fn read_line(reader: &mut impl BufRead, line: &mut Vec<u8>) -> std::io::Result<Option<bool>> {
line.clear();
let mut long = false;
let mut any = false;
loop {
let buffer = reader.fill_buf()?;
if buffer.is_empty() {
return Ok(any.then_some(long));
}
any = true;
let (taken, ended) = match buffer.iter().position(|byte| *byte == b'\n') {
Some(end) => (end, true),
None => (buffer.len(), false),
};
let room = LINE.saturating_sub(line.len());
long |= taken > room;
line.extend_from_slice(&buffer[..taken.min(room)]);
reader.consume(taken + usize::from(ended));
if ended {
return Ok(Some(long));
}
}
}
fn entry(line: &[u8], long: bool) -> Option<(u64, Found)> {
let digits = line.split(|byte| *byte == b' ').next()?;
if digits.is_empty() || digits.len() > 20 || !digits.iter().all(u8::is_ascii_digit) {
return None;
}
let id = std::str::from_utf8(digits).ok()?.parse::<u64>().ok()?;
if long {
return Some((
id,
Err(format!(
"its line in /proc/self/mountinfo is longer than {LINE} bytes"
)),
));
}
let fields: Vec<&[u8]> = line.split(|byte| *byte == b' ').collect();
let found = fields
.iter()
.skip(6)
.position(|field| *field == b"-")
.and_then(|position| fields.get(position + 7))
.and_then(|field| unescape(field));
Some((
id,
found.ok_or_else(|| "its line in /proc/self/mountinfo is malformed".to_owned()),
))
}
fn unescape(field: &[u8]) -> Option<Vec<u8>> {
if field.is_empty() || field.len() > TYPE * 4 {
return None;
}
let mut bytes = Vec::with_capacity(field.len());
let mut rest = field;
while let Some((&byte, after)) = rest.split_first() {
if byte == b'\\' {
let digits = after.get(..3)?;
if !digits.iter().all(|digit| (b'0'..=b'7').contains(digit)) {
return None;
}
let value = digits
.iter()
.fold(0_u32, |value, digit| value * 8 + u32::from(digit - b'0'));
bytes.push(u8::try_from(value).ok()?);
rest = &after[3..];
} else {
bytes.push(byte);
rest = after;
}
}
(bytes.len() <= TYPE).then_some(bytes)
}
pub(crate) fn fdinfo_mount_id(fdinfo: &str, fd: std::os::fd::RawFd) -> Result<Option<u64>, String> {
let mut bytes = Vec::new();
std::fs::File::open(format!("{fdinfo}/{fd}"))
.and_then(|file| file.take(FDINFO).read_to_end(&mut bytes))
.map_err(|error| format!("/proc/self/fdinfo could not be read ({error})"))?;
Ok(bytes.split(|byte| *byte == b'\n').find_map(|line| {
let value = line.strip_prefix(b"mnt_id:")?;
std::str::from_utf8(value).ok()?.trim().parse::<u64>().ok()
}))
}