use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub fn mount_source(dir: &Path) -> Option<String> {
mount_table().remove(dir)
}
#[cfg(target_os = "linux")]
pub fn mount_table() -> HashMap<PathBuf, String> {
let table = std::fs::read_to_string("/proc/self/mountinfo").unwrap_or_default();
let mut sources = HashMap::new();
for line in table.lines() {
let fields: Vec<&str> = line.split(' ').collect();
let Some(sep) = fields.iter().position(|f| *f == "-") else { continue };
let (Some(point), Some(fstype), Some(source)) = (fields.get(4), fields.get(sep + 1), fields.get(sep + 2))
else {
continue;
};
if fstype.starts_with("fuse") {
sources.insert(PathBuf::from(unescape(point)), unescape(source));
}
}
sources
}
#[cfg(target_os = "macos")]
pub fn mount_table() -> HashMap<PathBuf, String> {
read_mount_table().unwrap_or_default()
}
#[cfg(target_os = "macos")]
pub fn read_mount_table() -> Option<HashMap<PathBuf, String>> {
let output = std::process::Command::new("/sbin/mount").output().ok().filter(|output| output.status.success())?;
let table = String::from_utf8_lossy(&output.stdout);
Some(table.lines().filter_map(parse_mount_line).map(|(source, point)| (PathBuf::from(point), source)).collect())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn mount_table() -> HashMap<PathBuf, String> {
HashMap::new()
}
#[cfg(any(target_os = "macos", test))]
fn parse_mount_line(line: &str) -> Option<(String, &str)> {
let (source, rest) = line.split_once(" on ")?;
let (point, options) = rest.rsplit_once(" (")?;
let fstype = options.split([',', ')']).next()?;
let source = match source.strip_prefix("localhost:/sandfs/") {
Some(export) if fstype == "nfs" => format!("sandfs:{}", export.split('/').next().unwrap_or(export)),
_ => source.to_owned(),
};
Some((source, point))
}
#[cfg(any(target_os = "linux", test))]
fn unescape(field: &str) -> String {
let bytes = field.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 3 < bytes.len() && bytes[i + 1..i + 4].iter().all(u8::is_ascii_digit) {
let code = std::str::from_utf8(&bytes[i + 1..i + 4]).ok().and_then(|s| u8::from_str_radix(s, 8).ok());
if let Some(code) = code {
out.push(code);
i += 4;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(unix)]
pub fn alive(pid: u32) -> bool {
use nix::errno::Errno;
use nix::sys::signal::kill;
use nix::unistd::Pid;
let Ok(pid) = i32::try_from(pid) else { return false };
matches!(kill(Pid::from_raw(pid), None), Ok(()) | Err(Errno::EPERM))
}
#[cfg(not(unix))]
pub fn alive(_pid: u32) -> bool {
false
}
#[cfg(mount)]
pub fn raise_open_files() {
use nix::sys::resource::{Resource, getrlimit, setrlimit};
let Ok((soft, hard)) = getrlimit(Resource::RLIMIT_NOFILE) else { return };
for wanted in [hard, 1 << 20, 1 << 16, 10_240] {
if wanted <= soft {
return;
}
if setrlimit(Resource::RLIMIT_NOFILE, wanted, hard).is_ok() {
return;
}
}
}
#[cfg(target_os = "linux")]
pub fn memory_maps_left(spare: usize) -> Option<(usize, usize)> {
let max: usize = std::fs::read_to_string("/proc/sys/vm/max_map_count").ok()?.trim().parse().ok()?;
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let threads: usize = status.lines().find_map(|line| line.strip_prefix("Threads:"))?.trim().parse().ok()?;
if threads * 5 + spare * 2 < max {
return Some((max - threads * 5, max));
}
let used = std::fs::read("/proc/self/maps").ok()?.iter().filter(|&&byte| byte == b'\n').count();
Some((max.saturating_sub(used), max))
}
#[cfg(fuse)]
pub fn mount_options(name: &str) -> Vec<fuser::MountOption> {
use fuser::MountOption;
vec![MountOption::FSName(format!("sandfs:{name}")), MountOption::Subtype("sandfs".to_owned())]
}
#[cfg(fuse)]
pub fn mount_acl() -> fuser::SessionACL {
let conf = std::fs::read_to_string("/etc/fuse.conf").unwrap_or_default();
if nix::unistd::geteuid().is_root() || allows_other(&conf) {
fuser::SessionACL::RootAndOwner
} else {
fuser::SessionACL::Owner
}
}
#[cfg(any(fuse, test))]
fn allows_other(conf: &str) -> bool {
conf.lines().any(|line| line.split('#').next().unwrap_or("").trim() == "user_allow_other")
}
#[cfg(nfs)]
const UNMOUNT: (&[&[&str]], &str) = (&[&["/sbin/umount"]], "/sbin/umount is missing");
#[cfg(fuse)]
const UNMOUNT: (&[&[&str]], &str) =
(&[&["fusermount3", "-u"], &["fusermount", "-u"]], "fusermount3 is not installed; install fuse3");
#[cfg(mount)]
const FORCED_UNMOUNT: (&[&[&str]], &str) = (&[&["/sbin/umount", "-f"]], "/sbin/umount is missing");
#[cfg(mount)]
pub fn unmount(dir: &Path, served_by_nobody: bool) -> anyhow::Result<()> {
let (commands, missing) = if cfg!(nfs) && served_by_nobody { FORCED_UNMOUNT } else { UNMOUNT };
let ran = commands.iter().find_map(|cmd| std::process::Command::new(cmd[0]).args(&cmd[1..]).arg(dir).status().ok());
match ran {
Some(status) if status.success() => Ok(()),
Some(status) => anyhow::bail!("unmounting {} failed ({status})", dir.display()),
None => anyhow::bail!("{missing}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fuse_conf_allows_other_only_when_uncommented() {
assert!(allows_other("# comment\nuser_allow_other\n"));
assert!(allows_other("user_allow_other"));
assert!(allows_other(" user_allow_other # for containers\n"));
assert!(!allows_other("#user_allow_other\n"));
assert!(!allows_other("mount_max = 1000\n"));
assert!(!allows_other(""));
}
#[test]
fn mountinfo_escapes_decode() {
assert_eq!(unescape(r"/home/me/my\040ws"), "/home/me/my ws");
assert_eq!(unescape(r"/plain"), "/plain");
}
#[test]
fn macos_mount_lines_parse() {
let line = "localhost:/sandfs/ws/0f3a on /private/tmp/my ws (nfs, nodev, nosuid, mounted by me)";
assert_eq!(parse_mount_line(line), Some(("sandfs:ws".to_owned(), "/private/tmp/my ws")));
let other = "localhost:/elsewhere on /Volumes/x (nfs, nodev, nosuid, mounted by me)";
assert_eq!(parse_mount_line(other), Some(("localhost:/elsewhere".to_owned(), "/Volumes/x")));
let disk = "/dev/disk3s1 on / (apfs, sealed, local, read-only, journaled)";
assert_eq!(parse_mount_line(disk), Some(("/dev/disk3s1".to_owned(), "/")));
}
#[cfg(unix)]
#[test]
fn this_process_is_alive() {
assert!(alive(std::process::id()));
}
}