use std::ffi::CString;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::{Error, Result};
const MNT_DETACH: libc::c_int = 2;
#[derive(Debug, Clone)]
pub struct MountPoint {
pub path: PathBuf,
pub fstype: String,
}
enum FusermountError {
NotFound,
Failed(String),
}
pub fn mounts_under(root: &Path) -> Result<Vec<MountPoint>> {
let root_canon = match root.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::IoError(e)),
};
let info = match std::fs::read_to_string("/proc/self/mountinfo") {
Ok(s) => s,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::IoError(e)),
};
let mut mounts: Vec<MountPoint> = info
.lines()
.filter_map(parse_mountinfo_line)
.filter(|mp| mp.path == root_canon || mp.path.starts_with(&root_canon))
.collect();
mounts.sort_by(|a, b| b.path.as_os_str().len().cmp(&a.path.as_os_str().len()));
Ok(mounts)
}
pub fn unmount_lazy(mp: &MountPoint) -> Result<()> {
match try_umount2_detach(&mp.path) {
Ok(()) => return Ok(()),
Err(e) if is_permission_error(&e) => {}
Err(e) => return Err(Error::IoError(e)),
}
if is_fuse(&mp.fstype) {
return match try_fusermount(&mp.path) {
Ok(()) => Ok(()),
Err(FusermountError::NotFound) => Err(Error::UnmountFailed {
path: mp.path.clone(),
fstype: mp.fstype.clone(),
reason: "fusermount3 not found; install fuse3 or run as root".into(),
}),
Err(FusermountError::Failed(msg)) => Err(Error::UnmountFailed {
path: mp.path.clone(),
fstype: mp.fstype.clone(),
reason: format!("fusermount3 failed: {msg}"),
}),
};
}
Err(Error::UnmountFailed {
path: mp.path.clone(),
fstype: mp.fstype.clone(),
reason: format!(
"filesystem type '{}' requires CAP_SYS_ADMIN to unmount",
mp.fstype
),
})
}
fn parse_mountinfo_line(line: &str) -> Option<MountPoint> {
let tokens: Vec<&str> = line.split(' ').collect();
let mountpoint_raw = tokens.get(4)?;
let sep_pos = tokens.iter().position(|&t| t == "-")?;
let fstype = tokens.get(sep_pos + 1)?.to_string();
let mountpoint = unescape_mountinfo(mountpoint_raw);
Some(MountPoint {
path: PathBuf::from(mountpoint),
fstype,
})
}
fn unescape_mountinfo(s: &str) -> String {
let mut out = Vec::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 3 < bytes.len() {
let oct: Option<u8> = (|| {
let a = (bytes[i + 1] as char).to_digit(8)? as u8;
let b = (bytes[i + 2] as char).to_digit(8)? as u8;
let c = (bytes[i + 3] as char).to_digit(8)? as u8;
Some((a << 6) | (b << 3) | c)
})();
if let Some(byte) = oct {
out.push(byte);
i += 4;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out.clone()).unwrap_or_else(|_| String::from_utf8_lossy(&out).into_owned())
}
fn try_umount2_detach(path: &Path) -> io::Result<()> {
let cpath = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
let ret = unsafe { libc::umount2(cpath.as_ptr(), MNT_DETACH) };
if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
fn is_permission_error(e: &io::Error) -> bool {
matches!(e.kind(), io::ErrorKind::PermissionDenied)
|| e.raw_os_error() == Some(libc::EPERM as i32)
}
fn is_fuse(fstype: &str) -> bool {
fstype == "fuse" || fstype == "fuseblk" || fstype.starts_with("fuse.")
}
fn try_fusermount(path: &Path) -> std::result::Result<(), FusermountError> {
for binary in &["fusermount3", "fusermount"] {
match Command::new(binary)
.args(["-u", "-z", "--"])
.arg(path)
.output()
{
Ok(out) if out.status.success() => return Ok(()),
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned();
let msg = if stderr.is_empty() { stdout } else { stderr };
return Err(FusermountError::Failed(format!(
"{binary}: {msg} (exit {})",
out.status
.code()
.map_or_else(|| "signal".into(), |c| c.to_string())
)));
}
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => return Err(FusermountError::Failed(e.to_string())),
}
}
Err(FusermountError::NotFound)
}
#[cfg(test)]
#[path = "unmount_tests.rs"]
mod tests;