use clap::{Arg, ArgAction, Command};
use color_eyre::Result;
use color_eyre::eyre::eyre;
use nix::mount::{mount, MsFlags};
use std::fs::File;
use std::io::{BufRead, BufReader};
use libc;
#[derive(Debug)]
enum MountMode {
List,
MountAll,
SingleMount,
}
#[allow(dead_code)]
pub struct MountOptions {
mode: MountMode,
source: Option<String>,
target: Option<String>,
fstype: Option<String>,
flags: MsFlags,
data: Option<String>,
read_only: bool,
fake: bool,
verbose: bool,
no_mtab: bool,
bind: bool,
rbind: bool,
move_mount: bool,
remount: bool,
opt_match: Option<String>,
fstab: Option<String>,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<MountOptions> {
let source = matches.get_one::<String>("source").map(|s| s.to_string());
let target = matches.get_one::<String>("target").map(|s| s.to_string());
let fstype = matches.get_one::<String>("fstype").map(|s| s.to_string());
let options = matches.get_one::<String>("options").map(|s| s.to_string());
let read_only = matches.get_flag("readonly");
let fake = matches.get_flag("fake");
let verbose = matches.get_flag("verbose");
let no_mtab = matches.get_flag("no-mtab");
let bind = matches.get_flag("bind");
let rbind = matches.get_flag("rbind");
let move_mount = matches.get_flag("move");
let remount = matches.get_flag("remount");
let mount_all = matches.get_flag("all");
let opt_match = matches.get_one::<String>("match").map(|s| s.to_string());
let fstab = matches.get_one::<String>("fstab").map(|s| s.to_string());
let mode = if mount_all {
MountMode::MountAll
} else if source.is_none() && target.is_none() {
MountMode::List
} else {
MountMode::SingleMount
};
let (flags, data) = parse_mount_options(options.as_deref())?;
let mut final_flags = flags;
if read_only {
final_flags |= MsFlags::MS_RDONLY;
}
if bind {
final_flags |= MsFlags::MS_BIND;
}
if rbind {
final_flags |= MsFlags::MS_BIND | MsFlags::MS_REC;
}
if move_mount {
final_flags |= MsFlags::MS_MOVE;
}
if remount {
final_flags |= MsFlags::MS_REMOUNT;
}
Ok(MountOptions {
mode,
source,
target,
fstype,
flags: final_flags,
data,
read_only,
fake,
verbose,
no_mtab,
bind,
rbind,
move_mount,
remount,
opt_match,
fstab,
})
}
pub fn parse_mount_options(opts: Option<&str>) -> Result<(MsFlags, Option<String>)> {
let mut flags = MsFlags::empty();
let mut data_parts = Vec::new();
if let Some(opts_str) = opts {
for opt in opts_str.split(',') {
match opt {
"ro" => flags |= MsFlags::MS_RDONLY,
"rw" => flags &= !MsFlags::MS_RDONLY,
"nosuid" => flags |= MsFlags::MS_NOSUID,
"suid" => flags &= !MsFlags::MS_NOSUID,
"nodev" => flags |= MsFlags::MS_NODEV,
"dev" => flags &= !MsFlags::MS_NODEV,
"noexec" => flags |= MsFlags::MS_NOEXEC,
"exec" => flags &= !MsFlags::MS_NOEXEC,
"sync" => flags |= MsFlags::MS_SYNCHRONOUS,
"async" => flags &= !MsFlags::MS_SYNCHRONOUS,
"remount" => flags |= MsFlags::MS_REMOUNT,
"bind" => flags |= MsFlags::MS_BIND,
"rbind" => flags |= MsFlags::MS_BIND | MsFlags::MS_REC,
"recursive" => flags |= MsFlags::MS_REC,
"move" => flags |= MsFlags::MS_MOVE,
"silent" => flags |= MsFlags::from_bits_truncate(libc::MS_SILENT),
"loud" => flags &= !MsFlags::from_bits_truncate(libc::MS_SILENT),
"shared" => flags |= MsFlags::from_bits_truncate(libc::MS_SHARED),
"slave" => flags |= MsFlags::from_bits_truncate(libc::MS_SLAVE),
"private" => flags |= MsFlags::MS_PRIVATE,
"unbindable" => flags |= MsFlags::from_bits_truncate(libc::MS_UNBINDABLE),
"rshared" => flags |= MsFlags::from_bits_truncate(libc::MS_REC | libc::MS_SHARED),
"rslave" => flags |= MsFlags::from_bits_truncate(libc::MS_REC | libc::MS_SLAVE),
"rprivate" => flags |= MsFlags::MS_REC | MsFlags::MS_PRIVATE,
"runbindable" => flags |= MsFlags::from_bits_truncate(libc::MS_REC | libc::MS_UNBINDABLE),
"relatime" => flags |= MsFlags::from_bits_truncate(libc::MS_RELATIME),
"norelatime" => flags &= !MsFlags::from_bits_truncate(libc::MS_RELATIME),
"strictatime" => flags |= MsFlags::from_bits_truncate(libc::MS_STRICTATIME),
"nostrictatime" => flags &= !MsFlags::from_bits_truncate(libc::MS_STRICTATIME),
"nodiratime" => flags |= MsFlags::from_bits_truncate(libc::MS_NODIRATIME),
"diratime" => flags &= !MsFlags::from_bits_truncate(libc::MS_NODIRATIME),
"noatime" => flags |= MsFlags::from_bits_truncate(libc::MS_NOATIME),
"atime" => flags &= !MsFlags::from_bits_truncate(libc::MS_NOATIME),
"lazytime" => flags |= MsFlags::from_bits_truncate(libc::MS_LAZYTIME),
"nolazytime" => flags &= !MsFlags::from_bits_truncate(libc::MS_LAZYTIME),
"dirsync" => flags |= MsFlags::from_bits_truncate(libc::MS_DIRSYNC),
"mand" => flags |= MsFlags::from_bits_truncate(libc::MS_MANDLOCK),
"mandlock" => flags |= MsFlags::from_bits_truncate(libc::MS_MANDLOCK),
"nomand" => flags &= !MsFlags::from_bits_truncate(libc::MS_MANDLOCK),
"nosymfollow" => flags |= MsFlags::from_bits_truncate(libc::MS_NOSYMFOLLOW),
"symfollow" => flags &= !MsFlags::from_bits_truncate(libc::MS_NOSYMFOLLOW),
"posixacl" => flags |= MsFlags::from_bits_truncate(libc::MS_POSIXACL),
"iversion" => flags |= MsFlags::from_bits_truncate(libc::MS_I_VERSION),
"noiversion" => flags &= !MsFlags::from_bits_truncate(libc::MS_I_VERSION),
"defaults" | "auto" | "noauto" | "nouser" | "user" | "users" | "owner" | "group" | "nofail" | "_netdev" | "sw" | "swap" | "loop" => (),
opt if opt.starts_with("X-") || opt.starts_with("x-") => (),
opt if opt.starts_with("comment=") => (),
opt if opt.starts_with("loop=") => (),
opt if opt.starts_with("offset=") => (),
opt if opt.starts_with("sizelimit=") => (),
opt if opt.starts_with("encryption=") => (),
opt if opt.starts_with("loinit=") => (),
_ => data_parts.push(opt.to_string()),
}
}
}
let data = if data_parts.is_empty() {
None
} else {
Some(data_parts.join(","))
};
Ok((flags, data))
}pub fn command() -> Command {
Command::new("mount")
.about("Mount a filesystem")
.arg(
Arg::new("source")
.help("Device, directory, or file to mount")
.index(1)
)
.arg(
Arg::new("target")
.help("Directory to mount onto")
.index(2)
)
.arg(
Arg::new("fstype")
.short('t')
.long("type")
.help("Filesystem type")
)
.arg(
Arg::new("options")
.short('o')
.help("Mount options (comma-separated)")
)
.arg(
Arg::new("readonly")
.short('r')
.help("Mount read-only")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("fake")
.short('f')
.help("Fake mount (dry run)")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("verbose")
.short('v')
.help("Verbose output")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("no-mtab")
.short('n')
.help("Do not update /etc/mtab")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("bind")
.long("bind")
.help("Bind mount (make a subtree visible elsewhere)")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("rbind")
.long("rbind")
.help("Recursive bind mount")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("move")
.long("move")
.help("Move a mounted tree to another location")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("remount")
.long("remount")
.help("Remount an already mounted filesystem")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("all")
.short('a')
.help("Mount all filesystems in /etc/fstab")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("match")
.short('O')
.help("Mount only filesystems with option OPT (for -a)")
)
.arg(
Arg::new("fstab")
.short('T')
.help("Use alternate fstab file")
)
}
pub fn run(options: MountOptions) -> Result<()> {
match options.mode {
MountMode::List => list_mounts(options.fstype.as_deref()),
MountMode::MountAll => mount_all(options),
MountMode::SingleMount => single_mount(options),
}
}
fn list_mounts(fstype: Option<&str>) -> Result<()> {
let file = File::open("/proc/mounts").or_else(|_| File::open("/etc/mtab"))?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let curr_fstype = parts[2];
if fstype.is_none() || fstype == Some(curr_fstype) {
println!("{} on {} type {} ({})", parts[0], parts[1], curr_fstype, parts.get(3).unwrap_or(&""));
}
}
}
Ok(())
}
fn mount_all(_options: MountOptions) -> Result<()> {
Err(eyre!("mount -a not yet implemented"))
}
fn single_mount(options: MountOptions) -> Result<()> {
if options.fake {
println!("Would mount {:?} to {:?} with type {:?} flags {:?} data {:?}",
options.source, options.target, options.fstype, options.flags, options.data);
return Ok(());
}
let target = options.target.ok_or_else(|| eyre!("Target directory required"))?;
let source = options.source;
let fstype = options.fstype;
let data = options.data;
mount(
source.as_deref(),
target.as_str(),
fstype.as_deref(),
options.flags,
data.as_deref(),
).map_err(|e| eyre!("mount failed: {}", e))?;
if options.verbose {
println!("Mounted {:?} to {}", source, target);
}
Ok(())
}