use linuxutils_common::man::ManContent;
pub const MAN: ManContent = ManContent::empty();
use clap::Parser;
use rustix::fs::{FileType, lstat, major, minor, stat};
use std::{
io::{self, BufRead},
path::{Path, PathBuf},
process::ExitCode,
};
const EXIT_NOTMOUNT: u8 = 32;
#[derive(Parser)]
#[command(
name = "mountpoint",
version,
about = "See if a directory or file is a mountpoint"
)]
pub struct Args {
#[arg(short = 'd', long = "fs-devno", conflicts_with = "devno")]
fs_devno: bool,
#[arg(short = 'q', long = "quiet")]
quiet: bool,
#[arg(long = "nofollow")]
nofollow: bool,
#[arg(short = 'x', long = "devno", conflicts_with_all = ["fs_devno", "quiet", "nofollow"])]
devno: bool,
pub path: PathBuf,
}
pub fn run(args: Args) -> ExitCode {
if args.devno {
return show_devno(&args);
}
let st = match do_stat(&args.path, args.nofollow) {
Ok(s) => s,
Err(e) => {
if !args.quiet {
eprintln!("mountpoint: {}: {e}", args.path.display());
}
return ExitCode::from(1);
}
};
if args.fs_devno {
if let Some((maj, min)) = mountinfo_devno(&args.path) {
println!("{maj}:{min}");
} else {
println!("{}:{}", major(st.st_dev), minor(st.st_dev));
}
return ExitCode::SUCCESS;
}
let is_mount = is_mountpoint(&args.path);
if !args.quiet {
if is_mount {
println!("{} is a mountpoint", args.path.display());
} else {
println!("{} is not a mountpoint", args.path.display());
}
}
if is_mount {
ExitCode::SUCCESS
} else {
ExitCode::from(EXIT_NOTMOUNT)
}
}
fn do_stat(
path: &PathBuf,
nofollow: bool,
) -> Result<rustix::fs::Stat, rustix::io::Errno> {
if nofollow { lstat(path) } else { stat(path) }
}
fn show_devno(args: &Args) -> ExitCode {
let st = match do_stat(&args.path, false) {
Ok(s) => s,
Err(e) => {
if !args.quiet {
eprintln!("mountpoint: {}: {e}", args.path.display());
}
return ExitCode::from(1);
}
};
if !FileType::from_raw_mode(st.st_mode).is_block_device() {
if !args.quiet {
eprintln!(
"mountpoint: {}: not a block device",
args.path.display()
);
}
return ExitCode::from(EXIT_NOTMOUNT);
}
println!("{}:{}", major(st.st_rdev), minor(st.st_rdev));
ExitCode::SUCCESS
}
fn is_mountpoint(path: &PathBuf) -> bool {
if find_in_mountinfo(path).is_some() {
return true;
}
match stat(path) {
Ok(st) => is_mountpoint_by_stat(&st, path),
Err(_) => false,
}
}
fn find_in_mountinfo(path: &PathBuf) -> Option<(u32, u32)> {
let canonical = std::fs::canonicalize(path).ok()?;
let canonical = canonical.to_string_lossy();
let file = std::fs::File::open("/proc/self/mountinfo").ok()?;
let reader = io::BufReader::new(file);
for line in reader.lines() {
let Ok(line) = line else { continue };
let mut fields = line.split_whitespace();
let Some(_id) = fields.next() else { continue };
let Some(_parent) = fields.next() else {
continue;
};
let Some(devno) = fields.next() else { continue };
let Some(_root) = fields.next() else { continue };
let Some(mount_point) = fields.next() else {
continue;
};
let decoded = unescape_mountinfo(mount_point);
if *canonical == decoded {
let (maj_s, min_s) = devno.split_once(':')?;
let maj = maj_s.parse().ok()?;
let min = min_s.parse().ok()?;
return Some((maj, min));
}
}
None
}
fn mountinfo_devno(path: &PathBuf) -> Option<(u32, u32)> {
find_in_mountinfo(path)
}
fn is_mountpoint_by_stat(st: &rustix::fs::Stat, path: &Path) -> bool {
let parent = path.join("..");
match stat(&parent) {
Ok(parent_st) => st.st_dev != parent_st.st_dev,
Err(_) => false,
}
}
fn unescape_mountinfo(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
let oct: String = chars.by_ref().take(3).collect();
if let Ok(byte) = u8::from_str_radix(&oct, 8) {
result.push(byte as char);
} else {
result.push('\\');
result.push_str(&oct);
}
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unescape_simple() {
assert_eq!(unescape_mountinfo("/mnt/my\\040drive"), "/mnt/my drive");
}
#[test]
fn unescape_no_escapes() {
assert_eq!(unescape_mountinfo("/mnt/data"), "/mnt/data");
}
#[test]
fn root_is_mountpoint() {
assert!(is_mountpoint(&PathBuf::from("/")));
}
}