use super::Cmd; use clap::{Arg, ArgAction, Command}; use std::{ error::Error, fs::{self, File}, io::{self, BufRead, BufReader, ErrorKind}, os::linux::fs::MetadataExt, path::PathBuf, process, }; #[derive(Debug)] pub struct Mountpoint { name: &'static str, path: Option, } impl Default for Mountpoint { fn default() -> Self { Self { name: "mountpoint", path: Some(crate::Path::Bin), } } } impl Cmd for Mountpoint { fn name(&self) -> &str { self.name } fn cli(&self) -> clap::Command { Command::new(self.name) .about("see if a directory or file is a mountpoint") .author("Nathan Fisher") .after_long_help( "EXIT STATUS\n \ 0\n \ success: the directory is a mountpoint, or device is a block\ device on --devno\n \ 1\n \ failure: incorrect invocation, permissions or system error\ \n 32\n \ failure: the directory is not a mountpoint, or device is not a \ block device on --devno", ) .args([ Arg::new("fs-devno") .help( "Show the major/minor numbers of the device that is \ mounted on the given directory.", ) .short('d') .long("fs-devno") .action(ArgAction::SetTrue), Arg::new("devno") .help( "Show the major/minor numbers of the given blockdevice \ on standard output.", ) .short('x') .long("devno") .conflicts_with("fs-devno") .action(ArgAction::SetTrue), Arg::new("quiet") .help("Be quiet - don’t print anything.") .short('q') .long("quiet") .action(ArgAction::SetTrue), Arg::new("file").num_args(1).required(true), ]) } fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { let Some(matches) = matches else { return Err(io::Error::new(ErrorKind::Other, "no input").into()); }; let file = matches.get_one::("file").unwrap(); if matches.get_flag("fs-devno") { match fs_devno(file)? { Some(maj_min) => println!("{maj_min}"), None => { println!("{file} is not a mountpoint"); process::exit(32); } } } else if matches.get_flag("devno") { let devno = devno(file)?; println!("{}:{}", devno.0, devno.1); } else { let val = is_mountpoint(file)?; if val { if !matches.get_flag("quiet") { println!("{file} is a mountpoint"); } return Ok(()); } else { if !matches.get_flag("quiet") { println!("{file} is not a mountpoint"); } process::exit(32); } } Ok(()) } fn path(&self) -> Option { self.path } } fn is_mountpoint(path: &str) -> Result> { let fd = File::open("/proc/mounts")?; let reader = BufReader::new(fd); for line in reader.lines() { let line = line?; if let Some(mntpt) = line.split_whitespace().skip(1).next() { if mntpt == path { return Ok(true); } } } Ok(false) } fn fs_devno(path: &str) -> Result, Box> { let p = PathBuf::from(path); if !p.exists() { let msg = format!("mountpoint: {path}: No such file or directory"); return Err(Box::new(io::Error::new(ErrorKind::Other, msg))); } let fd = File::open("/proc/self/mountinfo")?; let reader = BufReader::new(fd); for line in reader.lines() { let line = line?; let mut line = line.split_whitespace().skip(2); if let Some(maj_min) = line.next() { if let Some(mntpt) = line.skip(1).next() { if mntpt == path { return Ok(Some(String::from(maj_min))); } } } } Ok(None) } fn devno(path: &str) -> Result<(u32, u32), Box> { let meta = fs::metadata(path)?; let rdev = meta.st_rdev(); if rdev == 0 { let msg = format!("Mountpoint: Error: {path} is not a character device"); return Err(Box::new(io::Error::new(ErrorKind::Other, msg))); } let major = unsafe { libc::major(rdev) }; let minor = unsafe { libc::minor(rdev) }; Ok((major, minor)) }