diff --git a/Cargo.lock b/Cargo.lock index c03ee4c..7b2ed23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,7 @@ dependencies = [ "glob 0.2.11 (git+https://github.com/mesalock-linux/glob)", "globset 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index c083b2c..2a72c1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,12 @@ loginutils = [ "getty" ] +mount = ["libmesabox/mount"] # FIXME: this is only like this because we can't rename dependencies and we use the same macro in # the tests that libmesabox uses to build tar_util = ["libmesabox/tar_util"] lsb = [ + "mount", "tar_util" ] diff --git a/libmesabox/Cargo.toml b/libmesabox/Cargo.toml index eab9d34..89c3e0a 100644 --- a/libmesabox/Cargo.toml +++ b/libmesabox/Cargo.toml @@ -21,9 +21,11 @@ loginutils = [ "getty" ] +mount = ["libc", "lazy_static"] # XXX: temporary until renaming dependencies is supported tar_util = ["tar", "globset"] lsb = [ + "mount", "tar" ] @@ -100,6 +102,7 @@ kernel32-sys = "0.2.2" winapi = { version = "0.3.5", features = ["namedpipeapi"] } nix = "0.10.0" +lazy_static = { version = "1.0.1", optional = true } libc = { version = "0.2.40", optional = true } platform-info = { version = "0.0.1", optional = true } uucore = { git = "https://github.com/uutils/coreutils", features = ["encoding", "fs", "mode", "parse_time"], optional = true } diff --git a/libmesabox/src/lib.rs b/libmesabox/src/lib.rs index 4ddef16..ca61f9a 100644 --- a/libmesabox/src/lib.rs +++ b/libmesabox/src/lib.rs @@ -31,6 +31,9 @@ extern crate fnv; extern crate globset; #[cfg(feature = "libc")] extern crate libc; +#[cfg(feature = "lazy_static")] +#[macro_use] +extern crate lazy_static; #[cfg(feature = "mio")] extern crate mio; #[cfg(feature = "pnet")] diff --git a/libmesabox/src/lsb/mount/mod.rs b/libmesabox/src/lsb/mount/mod.rs new file mode 100644 index 0000000..efa8309 --- /dev/null +++ b/libmesabox/src/lsb/mount/mod.rs @@ -0,0 +1,862 @@ +// +// Copyright (c) 2018, The MesaLock Linux Project Contributors +// All rights reserved. +// +// This work is licensed under the terms of the BSD 3-Clause License. +// For a copy, see the LICENSE file. +// + +extern crate clap; +extern crate lazy_static; +extern crate libc; +extern crate nix; + +use clap::{App, Arg}; +use libc::{c_long, c_ulong}; +use nix::mount::MsFlags; +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::{OsStr, OsString}; +use std::fs::{self, File}; +use std::io::{self, BufRead, BufReader, Write}; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::thread; +use {ArgsIter, LockError, Result, UtilSetup, UtilWrite}; + +pub(crate) const NAME: &str = "mount"; +pub(crate) const DESCRIPTION: &str = "Mount file systems"; + +type MountResult<T> = ::std::result::Result<T, MountError>; + +#[derive(Debug, Fail)] +enum MountError { + #[fail(display = "{}", _0)] + Output(#[cause] io::Error), + #[fail(display = "Cannot open file: {}", _0)] + OpenFileError(String), + #[fail(display = "Bad format while reading {}", _0)] + FSDescFileFormatError(String), + #[fail(display = "Unsupported filesystem type")] + UnsupportedFSType, + #[fail(display = "unknown filesystem type '{}'.", _0)] + UnknownFSType(String), + #[fail(display = "Unsupported option")] + UnsupportedOption, + #[fail(display = "Only root can do that")] + PermissionDenied, + #[fail(display = "Invalid argument")] + InvalidArgument, + #[fail(display = "Cannot support {} on your system", _0)] + UuidLabelNotSupportedError(String), + #[fail(display = "Cannot find {}=\"{}\"", _0, _1)] + UuidLabelNotFoundError(String, String), + #[fail(display = "{}: mount point does not exist.", _0)] + MountPointNotExist(String), + #[fail(display = "{}: mount point not mounted or bad option.", _0)] + MountPointNotMounted(String), + #[fail(display = "{}: special device {} does not exist.", _0, _1)] + DeviceNotExist(String, String), + // Denotes an error caused by one of stdin, stdout, or stderr failing to lock + #[fail(display = "{}", _0)] + Lock(#[cause] LockError), +} + +impl From<LockError> for MountError { + fn from(err: LockError) -> Self { + MountError::Lock(err) + } +} + +impl From<io::Error> for MountError { + fn from(err: io::Error) -> Self { + MountError::Output(err) + } +} + +enum MountCore { + ShowMountPoints(ShowMountPoints), + CreateMountPoint(CreateMountPoint), + Remount(Remount), +} + +/// There are several types of mount task, all of them implement Mountable trait +trait Mountable { + // Sometimes mount prints messages, so it returns a string + fn run(&mut self) -> MountResult<String>; +} + +impl Mountable for MountCore { + fn run(&mut self) -> MountResult<String> { + match *self { + MountCore::ShowMountPoints(ref mut mount) => mount.run(), + MountCore::CreateMountPoint(ref mut mount) => mount.run(), + MountCore::Remount(ref mut mount) => mount.run(), + } + } +} + +/// Store information that we need to execute a mount command +struct MountOptions { + multi_thread: bool, + // Mount command might mount a lot of devices at the same time, so we cache them in a list + mount_list: Vec<MountCore>, +} + +/// Source devices may be represented as an UUID or Label +enum SourceType { + Uuid, + Label, +} + +/// Help to convert an UUID or Label to the corresponding device path +struct SourceHelper { + source_type: SourceType, + path_map: HashMap<OsString, PathBuf>, +} + +impl SourceHelper { + fn new_uuid() -> MountResult<Self> { + Ok(Self { + source_type: SourceType::Uuid, + path_map: Self::get_path_map("/dev/disk/by-uuid", SourceType::Uuid)?, + }) + } + + fn new_label() -> MountResult<Self> { + Ok(Self { + source_type: SourceType::Label, + path_map: Self::get_path_map("/dev/disk/by-label", SourceType::Label)?, + }) + } + + fn get_device_path(&self, input: &OsString) -> MountResult<&Path> { + let input_bytes = input.as_bytes(); + let clipped_input; + let err_msg; + match self.source_type { + SourceType::Uuid => { + err_msg = "UUID".to_string(); + if input_bytes.starts_with(b"UUID=") { + clipped_input = OsStr::from_bytes(&input_bytes[5..]); + } else { + clipped_input = input; + } + } + SourceType::Label => { + err_msg = "Label".to_string(); + if input_bytes.starts_with(b"Label=") { + clipped_input = OsStr::from_bytes(&input_bytes[6..]); + } else { + clipped_input = input; + } + } + } + Ok(self + .path_map + .get(clipped_input) + .ok_or(MountError::UuidLabelNotFoundError( + err_msg, + clipped_input.to_string_lossy().to_string(), + ))?.as_path()) + } + + fn get_path_map( + read_path: &str, + source_type: SourceType, + ) -> MountResult<HashMap<OsString, PathBuf>> { + let mut path_map: HashMap<OsString, PathBuf> = HashMap::new(); + let err_msg = match source_type { + SourceType::Uuid => "UUID", + SourceType::Label => "Label", + }; + let dir = fs::read_dir(read_path) + .or_else(|_| Err(MountError::UuidLabelNotSupportedError(err_msg.to_string())))?; + for symlink in dir { + let link = symlink + .or_else(|_| Err(MountError::UuidLabelNotSupportedError(err_msg.to_string())))?; + path_map.insert( + link.file_name(), + link.path().canonicalize().or_else(|_| { + Err(MountError::UuidLabelNotSupportedError(err_msg.to_string())) + })?, + ); + } + Ok(path_map) + } +} + +lazy_static! { + static ref OPTION_MAP: HashMap<Cow<'static ,OsStr>, c_ulong> = { + let mut option_map = HashMap::new(); + option_map.insert(Cow::Borrowed(OsStr::new("auto")), 0); // ignored + option_map.insert(Cow::Borrowed(OsStr::new("noauto")), 0); // ignored + option_map.insert(Cow::Borrowed(OsStr::new("defaults")), 0); // ignored + option_map.insert(Cow::Borrowed(OsStr::new("nouser")), 0); // ignored + option_map.insert(Cow::Borrowed(OsStr::new("user")), 0); // ignored + option_map.insert(Cow::Borrowed(OsStr::new("async")), !libc::MS_SYNCHRONOUS); + option_map.insert(Cow::Borrowed(OsStr::new("atime")), !libc::MS_NOATIME); + option_map.insert(Cow::Borrowed(OsStr::new("dev")), !libc::MS_NODEV); + option_map.insert(Cow::Borrowed(OsStr::new("exec")), !libc::MS_NOEXEC); + option_map.insert(Cow::Borrowed(OsStr::new("noatime")), libc::MS_NOATIME); + option_map.insert(Cow::Borrowed(OsStr::new("nodev")), libc::MS_NODEV); + option_map.insert(Cow::Borrowed(OsStr::new("noexec")), libc::MS_NOEXEC); + option_map.insert(Cow::Borrowed(OsStr::new("nosuid")), libc::MS_NOSUID); + option_map.insert(Cow::Borrowed(OsStr::new("remount")), libc::MS_REMOUNT); + option_map.insert(Cow::Borrowed(OsStr::new("ro")), libc::MS_RDONLY); + option_map.insert(Cow::Borrowed(OsStr::new("rw")), !libc::MS_RDONLY); + option_map.insert(Cow::Borrowed(OsStr::new("suid")), !libc::MS_NOSUID); + option_map.insert(Cow::Borrowed(OsStr::new("sync")), libc::MS_SYNCHRONOUS); + option_map + }; +} + +struct Flag { + flag: MsFlags, +} + +impl Default for Flag { + fn default() -> Self { + Self { + flag: MsFlags::MS_SILENT, + } + } +} + +impl Flag { + fn from<'a>(options: &mut Vec<Cow<'a, OsStr>>) -> MountResult<MsFlags> { + let mut flag = Self::default().flag.bits(); + if options.contains(&Cow::Borrowed(OsStr::new("default"))) { + options.extend_from_slice(&[ + Cow::Borrowed(OsStr::new("rw")), + Cow::Borrowed(OsStr::new("suid")), + Cow::Borrowed(OsStr::new("dev")), + Cow::Borrowed(OsStr::new("exec")), + Cow::Borrowed(OsStr::new("auto")), + Cow::Borrowed(OsStr::new("nouser")), + Cow::Borrowed(OsStr::new("async")), + ]); + } + for opt in options { + let f = *OPTION_MAP.get(opt).ok_or(MountError::UnsupportedOption)?; + if (f as c_long) < 0 { + flag &= f; + } else { + flag |= f; + } + } + Ok(MsFlags::from_bits(flag).ok_or(MountError::UnsupportedOption)?) + } +} + +struct MntEnt { + mnt_fsname: OsString, + mnt_dir: OsString, + mnt_type: OsString, + mnt_opts: OsString, +} + +/// This is used to read Filesystem Description File +/// e.g. /etc/fstab and /etc/mtab +struct FSDescFile { + entries: Vec<MntEnt>, +} + +impl FSDescFile { + fn new(path: &Path) -> MountResult<Self> { + // All of these files should exist and can be read, but just in case + let file = File::open(path) + .or_else(|_| Err(MountError::OpenFileError(path.display().to_string())))?; + let mut entries = vec![]; + let mut reader = BufReader::new(file); + let mut line_bytes = vec![]; + while let Ok(n) = reader.read_until(b'\n', &mut line_bytes) { + // Break if EOF + if n == 0 { + break; + } + // Skip empty lines and commends + if line_bytes.is_empty() || line_bytes[0] == b'#' { + continue; + } + let line = OsStr::from_bytes(&line_bytes).to_os_string(); + line_bytes.clear(); + + // We need the first 4 columns + let mut splitted_line = line.split_whitespace(); + let mnt_fsname = splitted_line.next(); + let mnt_dir = splitted_line.next(); + let mnt_type = splitted_line.next(); + let mnt_opts = splitted_line.next(); + // There should be 2 columns remaining + if splitted_line.count() != 2 { + return Err(MountError::FSDescFileFormatError( + path.display().to_string(), + )); + } + let mnt = MntEnt { + mnt_fsname: OsString::from(mnt_fsname.unwrap()), + mnt_dir: OsString::from(mnt_dir.unwrap()), + mnt_type: OsString::from(mnt_type.unwrap()), + mnt_opts: OsString::from(mnt_opts.unwrap()), + }; + entries.push(mnt) + } + + Ok(Self { entries: entries }) + } + + fn get_output(&self, fs_type: &Option<OsString>) -> String { + let mut ret = String::new(); + for item in &self.entries { + match *fs_type { + Some(ref t) => { + if *t != item.mnt_type { + continue; + } + } + None => {} + } + ret.push_str( + format!( + "{} on {} type {} ({})\n", + item.mnt_fsname.to_string_lossy(), + item.mnt_dir.to_string_lossy(), + item.mnt_type.to_string_lossy(), + item.mnt_opts.to_string_lossy(), + ).as_str(), + ); + } + ret + } +} + +/// Define some special requirements +struct Property { + fake: bool, + use_uuid: bool, + use_label: bool, + // TODO user mountable devices +} + +impl Default for Property { + fn default() -> Self { + Self { + fake: false, + use_uuid: false, + use_label: false, + } + } +} + +/// OsString has limited methods, implement them +trait OsStringExtend { + fn starts_with(&self, pat: &str) -> bool; + fn contains(&self, pat: &str) -> bool; + fn split_whitespace<'a>(&'a self) -> Box<Iterator<Item = &'a OsStr> + 'a>; +} + +impl OsStringExtend for OsString { + fn starts_with(&self, pat: &str) -> bool { + self.to_string_lossy().starts_with(pat) + } + + fn contains(&self, pat: &str) -> bool { + self.to_string_lossy().contains(pat) + } + + fn split_whitespace<'a>(&'a self) -> Box<Iterator<Item = &'a OsStr> + 'a> { + Box::new( + self.as_bytes() + .split(|ch| ch.is_ascii_whitespace()) + .filter(|s| !s.is_empty()) + .map(|s| OsStr::from_bytes(s)), + ) + } +} + +impl MountOptions { + fn from_matches(matches: &clap::ArgMatches) -> MountResult<Self> { + let mut mount_list: Vec<MountCore> = vec![]; + + // If -a exists, mount all the entries in /etc/fstab, except for those who contain "noauto" + if matches.is_present("a") { + let fstab = FSDescFile::new(&Path::new("/etc/fstab"))?; + for item in fstab.entries { + if item.mnt_opts.contains("noauto") { + continue; + } + + // Split the comma separated option string into a vector, also convert &str into OsString + let opts: Vec<Cow<'static, OsStr>> = item + .mnt_opts + .to_string_lossy() + .split(",") + .map(|i| Cow::Owned(OsString::from(i))) + .collect(); + + // In this case, all the mounts are of type "CreateMountPoint" + let m = CreateMountPoint::new( + Property::default(), + item.mnt_fsname, + PathBuf::from(item.mnt_dir), + Some(item.mnt_type), + Some(opts), + OsString::new(), + )?; + + mount_list.push(MountCore::CreateMountPoint(m)); + } + } + // If -a doesn't exist, read arguments from command line, and find out the mount type + else { + let mut arg1 = matches.value_of("arg1"); + let mut arg2 = matches.value_of("arg2"); + let fs_type = matches.value_of("t"); + + let options: Option<Vec<Cow<'static, OsStr>>> = matches + .values_of("o") + .map(|i| i.collect()) + .map(|i: Vec<&str>| { + i.into_iter() + .map(|s| Cow::Owned(OsString::from(s))) + .collect() + }); + + let property = Property { + fake: matches.is_present("f"), + use_uuid: matches.is_present("U"), + use_label: matches.is_present("L"), + }; + + // If -U exists, then use UUID as source + if let Some(uuid) = matches.value_of("U") { + arg2 = arg1; + arg1 = Some(uuid); + } + + // If -L exists, then use Label as source + if let Some(label) = matches.value_of("L") { + arg2 = arg1; + arg1 = Some(label); + } + + // Find out the exact mount type + match arg1 { + Some(arg1) => { + match arg2 { + // Two arguments, the type must be "CreateMountPoint" + Some(arg2) => { + let m = CreateMountPoint::new( + property, + OsString::from(arg1), + PathBuf::from(arg2), + fs_type.map(|t| OsString::from(t)), + options, + OsString::new(), + )?; + mount_list.push(MountCore::CreateMountPoint(m)); + } + // One argument + None => match options { + // If there is a "remount" option, the type is "Remount" + Some(ref opts) + if opts.contains(&Cow::Borrowed(OsStr::new("remount"))) => + { + let m = Remount::new( + property, + OsString::from(arg1), + opts.to_vec(), + OsString::new(), + ); + mount_list.push(MountCore::Remount(m)); + } + // Otherwise, this device should be written in /etc/fstab + _ => { + let fstab = FSDescFile::new(Path::new("/etc/fstab"))?; + for item in fstab.entries { + if arg1 == item.mnt_fsname || arg1 == item.mnt_dir { + let m = CreateMountPoint::new( + property, + item.mnt_fsname, + PathBuf::from(item.mnt_dir), + Some(item.mnt_type), + options, + OsString::new(), + )?; + mount_list.push(MountCore::CreateMountPoint(m)); + break; + } + } + // If we cannot find anything about this device, return an error + if mount_list.len() == 0 { + return Err(MountError::InvalidArgument); + } + } + }, + } + } + // no argument, the type must be "ShowMountPoints" + None => { + let m = ShowMountPoints::new(fs_type); + mount_list.push(MountCore::ShowMountPoints(m)); + } + } + } + Ok(Self { + multi_thread: matches.is_present("F"), + mount_list: mount_list, + }) + } +} + +struct Mounter<O> +where + O: Write, +{ + output: O, +} + +impl<O> Mounter<O> +where + O: Write, +{ + fn new(output: O) -> Self { + Mounter { output } + } + + fn mount(&mut self, mut options: MountOptions) -> MountResult<()> { + if options.multi_thread { + let mut handle = vec![]; + for mut m in options.mount_list { + handle.push(thread::spawn(move || m.run())); + } + for h in handle { + if let Err(_) = h.join() { + return Err(MountError::from(io::Error::last_os_error())); + } + } + } else { + for m in &mut options.mount_list { + write!(self.output, "{}", m.run()?); + } + } + Ok(()) + } +} + +/// Show mount points from /proc/mounts +/// If -t is specified, only output mount points of this file system type +/// Usage examples: "mount", "mount -t ext4" +struct ShowMountPoints { + filesystem_type: Option<OsString>, +} + +impl ShowMountPoints { + fn new(filesystem_type: Option<&str>) -> Self { + //let t = match filesystem_type { + //Some(t) => OsString::from(t), + //None => OsString::new(), + //}; + Self { + filesystem_type: filesystem_type.map(|s| OsString::from(s)), + } + } +} + +impl Mountable for ShowMountPoints { + fn run(&mut self) -> MountResult<String> { + Ok(FSDescFile::new(Path::new("/proc/mounts"))?.get_output(&self.filesystem_type)) + } +} + +/// Create a new mount point +/// Usage examples: "mount /dev/sda1 /home/username/mnt" +struct CreateMountPoint { + property: Property, + source: PathBuf, + target: PathBuf, + filesystem_type: Option<OsString>, + mountflags: Option<Vec<Cow<'static, OsStr>>>, + data: OsString, +} + +impl CreateMountPoint { + fn new( + property: Property, + source: OsString, + target: PathBuf, + filesystem_type: Option<OsString>, + mountflags: Option<Vec<Cow<'static, OsStr>>>, + data: OsString, + ) -> MountResult<Self> { + // If source is an UUID or LABEL, get the corresponding device path + // If source is read from /etc/fstab, check its prefix + // If source is read from command line, check its property + let device_path; + if source.starts_with("UUID=") || property.use_uuid { + let uuid_helper = SourceHelper::new_uuid()?; + device_path = PathBuf::from(uuid_helper.get_device_path(&source)?); + } else if source.starts_with("Label=") || property.use_label { + let label_helper = SourceHelper::new_label()?; + device_path = PathBuf::from(label_helper.get_device_path(&source)?); + } else { + device_path = PathBuf::from(source); + } + Ok(Self { + property, + source: device_path, + target, + filesystem_type, + mountflags, + data, + }) + } +} + +impl Mountable for CreateMountPoint { + fn run(&mut self) -> MountResult<String> { + // check privilege + // getuid() is always successful, so it's ok to use it + if unsafe { libc::getuid() } != 0 { + return Err(MountError::PermissionDenied); + } + + // check if mount point exists + if !self.target.exists() { + return Err(MountError::MountPointNotExist( + self.target.to_string_lossy().to_string(), + )); + } + + // check if device exists + if !self.source.exists() { + let s = self.source.to_string_lossy().to_string(); + let t = self.target.to_string_lossy().to_string(); + return Err(MountError::DeviceNotExist(t, s)); + } + + // Get mountflags + let mountflags = match self.mountflags { + Some(ref mut mountflags) => Flag::from(mountflags)?, + None => Flag::default().flag, + }; + + // If type is not specified or "auto", automatically detect filesystem type + if Some(OsString::from("auto")) == self.filesystem_type { + self.filesystem_type = None; + } + match self.filesystem_type { + None => { + // Read all the filesystem types that we support + let file_name = "/proc/filesystems"; + let file = File::open(file_name) + .or_else(|_| Err(MountError::OpenFileError(String::from(file_name))))?; + for line in BufReader::new(file).lines() { + let line = line?; + match line.chars().next() { + Some(line) => { + if !line.is_whitespace() { + continue; // skip nodev devices + } + } + None => { + continue; // skip empty lines + } + } + // Empty lines are already skipped, so it is ok to read array from index 1 + let try_fs_type = &line[1..]; + if let Ok(_) = nix::mount::mount( + Some(self.source.as_os_str()), + self.target.as_os_str(), + Some(try_fs_type), + mountflags, + Some(self.data.as_os_str()), + ).or_else(|_| Err(MountError::from(io::Error::last_os_error()))) + { + return Ok(String::new()); + } + } + // Now we tried all the types that we support and none of them succeed + return Err(MountError::UnsupportedFSType); + } + + // If type is specified + Some(ref fs_type) => { + if !self.property.fake { + match nix::mount::mount( + Some(self.source.as_os_str()), + self.target.as_os_str(), + Some(fs_type.as_os_str()), + mountflags, + Some(self.data.as_os_str()), + ).or_else(|_| Err(MountError::from(io::Error::last_os_error()))) + { + Ok(_) => return Ok(String::new()), + Err(e) => { + // Error number 19 means "No such device" + // This happens if you provide a wrong filesystem type + if nix::errno::errno() == 19 { + return Err(MountError::UnknownFSType(String::from( + fs_type.to_string_lossy(), + ))); + } + // TODO: It's not known if there are other possible error numbers + return Err(e); + } + } + } + return Ok(String::new()); + } + } + } +} + +/// Remount an existing mount point +/// Usage examples: "mount -o remount /home/username/mnt" +struct Remount { + property: Property, + target: PathBuf, + mountflags: Vec<Cow<'static, OsStr>>, + data: OsString, +} + +impl Remount { + fn new( + property: Property, + target: OsString, + mountflags: Vec<Cow<'static, OsStr>>, + data: OsString, + ) -> Self { + Self { + property, + target: PathBuf::from(target), + mountflags, + data, + } + } +} + +impl Mountable for Remount { + fn run(&mut self) -> MountResult<String> { + // check privilege + // getuid() is always successful, so it's ok to use it + if unsafe { libc::getuid() } != 0 { + return Err(MountError::PermissionDenied); + } + + // Go through all the existing mount points, find the appropriate source & target + let existing_mounts = FSDescFile::new(Path::new("/proc/mounts"))?; + let mut source = OsStr::new(""); + let mut target = OsStr::new(""); + let mut filesystem_type = OsStr::new(""); + // We need to do this in reverse order + for item in existing_mounts.entries.iter().rev() { + if self.target == item.mnt_fsname || self.target == item.mnt_dir { + source = &item.mnt_fsname; + target = &item.mnt_dir; + filesystem_type = &item.mnt_type; + break; + } + } + + // If not found, the mount point is not mounted + if source == "" || target == "" { + return Err(MountError::MountPointNotMounted( + self.target.to_string_lossy().to_string(), + )); + } + + let mountflags = Flag::from(&mut self.mountflags)?; + + if !self.property.fake { + nix::mount::mount( + Some(source), + target, + Some(filesystem_type), + mountflags, + Some(self.data.as_os_str()), + ).or_else(|_| Err(MountError::from(io::Error::last_os_error())))? + } + + Ok(String::new()) + } +} + +fn create_app() -> App<'static, 'static> { + util_app!(NAME) + .author("Zhuohua Li <zhuohuali@baidu.com>") + .arg(Arg::with_name("arg1") + .index(1)) + .arg(Arg::with_name("arg2") + .index(2)) + .arg(Arg::with_name("v") // TODO: not supported yet + .short("v") + .help("invoke verbose mode. The mount command shall provide diagnostic messages on stdout.")) + .arg(Arg::with_name("a") + .short("a") + .help("mount all file systems (of the given types) mentioned in /etc/fstab.")) + .arg(Arg::with_name("F") + .short("F") + .requires("a") + .help("If the -a option is also present, fork a new incarnation of mount for each device to be mounted. This will do the mounts on different devices or different NFS servers in parallel.")) + .arg(Arg::with_name("f") + .short("f") + .help("cause everything to be done except for the actual system call; if it's not obvious, this `fakes' mounting the file system.")) + .arg(Arg::with_name("n") // TODO: not supported yet + .short("n") + .help("mount without writing in /etc/mtab. This is necessary for example when /etc is on a read-only file system.")) + .arg(Arg::with_name("s") // TODO: not supported yet + .short("s") + .help("ignore mount options not supported by a file system type. Not all file systems support this option.")) + .arg(Arg::with_name("r") + .short("r") + .conflicts_with("w") + .help("mount the file system read-only. A synonym is -o ro.")) + .arg(Arg::with_name("w") + .short("w") + .conflicts_with("r") + .help("mount the file system read/write. (default) A synonym is -o rw.")) + .arg(Arg::with_name("L") + .short("L") + .help("If the file /proc/partitions is supported, mount the partition that has the specified label.") + .takes_value(true) + .conflicts_with("U") + .value_name("label")) + .arg(Arg::with_name("U") + .short("U") + .help("If the file /proc/partitions is supported, mount the partition that has the specified uuid.") + .takes_value(true) + .conflicts_with("L") + .value_name("uuid")) + .arg(Arg::with_name("t") + .short("t") + .help("indicate a file system type of vfstype.{n}{n}More than one type may be specified in a comma separated list. The list of file system types can be prefixed with no to specify the file system types on which no action should be taken.") + .takes_value(true) + .value_name("vfstype")) + .arg(Arg::with_name("o") + .short("o") + .help("options are specified with a -o flag followed by a comma-separated string of options. Some of these options are only useful when they appear in the /etc/fstab file. The following options apply to any file system that is being mounted:{n}{n}async{n}\tperform all I/O to the file system asynchronously.{n}{n}atime{n}\tupdate inode access time for each access. (default){n}{n}auto{n}\tin /etc/fstab, indicate the device is mountable with -a.{n}{n}defaults{n}\tuse default options: rw, suid, dev, exec, auto, nouser, async.{n}{n}dev{n}\tinterpret character or block special devices on the file system.{n}{n}exec{n}\tpermit execution of binaries.{n}{n}noatime{n}\tdo not update file access times on this file system.{n}{n}noauto{n}\tin /etc/fstab, indicates the device is only explicitly mountable.{n}{n}nodev{n}\tdo not interpret character or block special devices on the file system.{n}{n}noexec{n}\tdo not allow execution of any binaries on the mounted file system.{n}{n}nosuid{n}\tdo not allow set-user-identifier or set-group-identifier bits to take effect.{n}{n}nouser{n}\tforbid an unprivileged user to mount the file system. (default){n}{n}remount{n}\tremount an already-mounted file system. This is commonly used to change the mount options for a file system, especially to make a read-only file system writable.{n}{n}ro{n}\tmount the file system read-only.{n}{n}rw{n}\tmount the file system read-write.{n}{n}suid{n}\tallow set-user-identifier or set-group-identifier bits to take effect.{n}{n}sync{n}\tdo all I/O to the file system synchronously.{n}{n}user{n}\tallow an unprivilieged user to mount the file system. This option implies the options noexec, nosuid, nodev unless overridden by subsequent options.") + .takes_value(true) + .value_name("options") + .use_delimiter(true) + .possible_values(&["async", "atime", "defaults", "dev", "exec", "noatime", "nodev", "noexec", "nosuid", "nouser", "remount", "ro", "rw", "suid", "sync", "user"]) + .hide_possible_values(true)) +} + +pub fn execute<S, T>(setup: &mut S, args: T) -> Result<()> +where + S: UtilSetup, + T: ArgsIter, +{ + let app = create_app(); + let matches = app.get_matches_from_safe(args)?; + let options = MountOptions::from_matches(&matches)?; + + let output = setup.output(); + let output = output.lock()?; + + let mut mounter = Mounter::new(output); + mounter.mount(options)?; + Ok(()) +} diff --git a/libmesabox/src/util_list.rs b/libmesabox/src/util_list.rs index fab9b1f..77cfe85 100644 --- a/libmesabox/src/util_list.rs +++ b/libmesabox/src/util_list.rs @@ -9,6 +9,7 @@ generate_fns! { (getty, "getty") }, lsb { + (mount, "mount"), (tar, "tar_util") }, networking { diff --git a/tests/lsb/mount.rs b/tests/lsb/mount.rs new file mode 100644 index 0000000..dbc7f62 --- /dev/null +++ b/tests/lsb/mount.rs @@ -0,0 +1,306 @@ +// +// Copyright (c) 2018, The MesaLock Linux Project Contributors +// All rights reserved. +// +// This work is licensed under the terms of the BSD 3-Clause License. +// For a copy, see the LICENSE file. +// + +// NOTE: some ignored tests need root privilege, and you must run these tests in serial +// use command `sudo cargo test root_test_mount -- --ignored --test-threads 1` + +use assert_cmd::prelude::*; +use assert_fs; +use assert_fs::prelude::*; +use predicates::prelude::*; +use std::fs; +use std::io::prelude::*; +use std::io::{BufRead, BufReader, Read}; +use std::process::Command; + +const NAME: &str = "mount"; + +struct MntEnt { + mnt_fsname: String, + mnt_dir: String, + mnt_type: String, + mnt_opts: String, +} + +#[test] +fn test_mount_no_arg() { + let file = fs::File::open("/proc/mounts").unwrap(); + let mut list = Vec::new(); + for line in BufReader::new(file).lines() { + let line = line.unwrap(); + match line.chars().next() { + None | Some('#') => continue, + Some(_) => {} + } + let mut iter = line.split_whitespace(); + let mnt_fsname = iter.next().unwrap(); + let mnt_dir = iter.next().unwrap(); + let mnt_type = iter.next().unwrap(); + let mnt_opts = iter.next().unwrap(); + let mnt = MntEnt { + mnt_fsname: String::from(mnt_fsname), + mnt_dir: String::from(mnt_dir), + mnt_type: String::from(mnt_type), + mnt_opts: String::from(mnt_opts), + }; + list.push(mnt) + } + + let mut output = String::new(); + for item in &list { + if item.mnt_type != "ext4" { + continue; + } + let s = format!( + "{} on {} type {} ({})", + item.mnt_fsname, item.mnt_dir, item.mnt_type, item.mnt_opts + ); + output.push_str((s + "\n").as_str()); + } + new_cmd!() + .args(&["-t", "ext4"]) + .assert() + .success() + .stdout(predicate::str::contains(output).from_utf8()) + .stderr(""); +} + +#[test] +fn test_mount_without_root() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mnt = temp_dir.path().to_str().unwrap(); + new_cmd!() + .args(&["/dev/loop0", mnt]) + .assert() + .failure() + .stdout("") + .stderr("mount: Only root can do that\n"); +} + +#[test] +#[ignore] +fn root_test_mount_nonexistent_dev() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mnt = temp_dir.path().to_str().unwrap(); + new_cmd!() + .args(&["/dev/this_device_should_not_exist", mnt]) + .assert() + .failure() + .stdout("") + .stderr( + predicate::str::contains( + "special device /dev/this_device_should_not_exist does not exist.", + ).from_utf8(), + ); +} + +#[test] +#[ignore] +fn root_test_mount_nonexistent_mount_point() { + new_cmd!() + .args(&["/dev/loop0", "this_target_should_not_exist"]) + .assert() + .failure() + .stdout("") + .stderr(predicate::str::contains("mount point does not exist.").from_utf8()); +} + +#[test] +#[ignore] +fn root_test_mount_unknown_filesystem_type() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mnt = temp_dir.path().to_str().unwrap(); + new_cmd!() + .args(&[ + "-t", + "this_filesystem_type_should_not_exist", + "/dev/loop0", + mnt, + ]) + .assert() + .failure() + .stdout("") + .stderr( + predicate::str::contains( + "unknown filesystem type 'this_filesystem_type_should_not_exist'.", + ).from_utf8(), + ); +} + +#[test] +#[ignore] +fn root_test_mount_unknown_uuid() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mnt = temp_dir.path().to_str().unwrap(); + new_cmd!() + .args(&["-U", "this_uuid_should_not_exist", mnt]) + .assert() + .failure() + .stdout("") + .stderr( + predicate::str::contains("Cannot find UUID=\"this_uuid_should_not_exist\"").from_utf8(), + ); +} + +#[test] +#[ignore] +fn root_test_mount_create_mount_point() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + // let device_image = temp_dir.child("device.img"); + let mount_point = temp_dir.child("mnt"); + let mount_point_path = mount_point.path(); + //let device_image_path = device_image.path(); + // create mount point directory + fs::create_dir(mount_point_path).unwrap(); + // create device image + Command::new("dd") + .current_dir(&temp_dir) + .args(&["if=/dev/zero", "of=device.img", "bs=128", "count=1024"]) + .assert() + .success(); + + Command::new("mkfs.ext4") + .current_dir(&temp_dir) + .args(&["device.img"]) + .assert() + .success(); + + Command::new("mount") + .current_dir(&temp_dir) + .args(&["device.img", "mnt"]) + .assert() + .success(); + let new_file_path = mount_point_path.join("new_file.txt"); + let mut file = fs::File::create(new_file_path).unwrap(); + file.write_all(b"Hello World!").unwrap(); + drop(file); + + Command::new("umount") + .current_dir(&temp_dir) + .args(&["mnt"]) + .assert() + .success(); + + Command::new("losetup") + .current_dir(&temp_dir) + .args(&["/dev/loop0", "device.img"]) + .assert() + .success() + .stdout("") + .stderr(""); + + new_cmd!() + .current_dir(&temp_dir) + .args(&["/dev/loop0", "mnt"]) + .assert() + .success(); + + let path = temp_dir.path().join("mnt/new_file.txt"); + let mut file = fs::File::open(path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + assert_eq!(contents, "Hello World!"); + drop(file); + + Command::new("umount") + .current_dir(&temp_dir) + .args(&["mnt"]) + .assert() + .success(); + + Command::new("losetup") + .current_dir(&temp_dir) + .args(&["-d", "/dev/loop0"]) + .assert() + .success() + .stdout("") + .stderr(""); +} + +#[test] +#[ignore] +fn root_test_mount_remount() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let mount_point = temp_dir.child("mnt"); + let mount_point_path = mount_point.path(); + // create mount point directory + fs::create_dir(mount_point_path).unwrap(); + // create device image + Command::new("dd") + .current_dir(&temp_dir) + .args(&["if=/dev/zero", "of=device.img", "bs=128", "count=1024"]) + .assert() + .success(); + + Command::new("mkfs.ext4") + .current_dir(&temp_dir) + .args(&["device.img"]) + .assert() + .success(); + + Command::new("losetup") + .current_dir(&temp_dir) + .args(&["/dev/loop1", "device.img"]) + .assert() + .success() + .stdout("") + .stderr(""); + + new_cmd!() + .current_dir(&temp_dir) + // mount it as read-write + .args(&["-o", "rw", "/dev/loop1", "mnt"]) + .assert() + .success(); + + new_cmd!() + .current_dir(&temp_dir) + // then remount it as read-only + .args(&["-o", "remount,ro", "/dev/loop1"]) + .assert() + .success(); + + let path = temp_dir.path().join("mnt/create_new_file.txt"); + match fs::File::create(path) { + // This operation should not succeed because it's read-only + Ok(_) => assert!(false), + // OS error 30 means "Read-only file system" + Err(e) => assert_eq!(e.raw_os_error(), Some(30)), + } + + Command::new("umount") + .current_dir(&temp_dir) + .args(&["mnt"]) + .assert() + .success() + .stdout("") + .stderr(""); + + Command::new("losetup") + .current_dir(&temp_dir) + .args(&["-d", "/dev/loop1"]) + .assert() + .success() + .stdout("") + .stderr(""); +} + +#[test] +#[ignore] +fn root_test_mount_remount_nonexistent_mount_point() { + new_cmd!() + .args(&["-o", "remount", "/dev/this_device_should_not_be_mounted"]) + .assert() + .failure() + .stdout("") + .stderr( + predicate::str::contains( + "mount point not mounted or bad option.", + ).from_utf8(), + ); +}