diff --git a/Cargo.lock b/Cargo.lock index 8dd26aba..3879426d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1511,6 +1511,7 @@ dependencies = [ "uu_lsmem", "uu_mcookie", "uu_mesg", + "uu_mount", "uu_mountpoint", "uu_nologin", "uu_renice", @@ -1669,6 +1670,16 @@ dependencies = [ "uucore 0.2.2", ] +[[package]] +name = "uu_mount" +version = "0.0.1" +dependencies = [ + "clap", + "libc", + "thiserror", + "uucore 0.2.2", +] + [[package]] name = "uu_mountpoint" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index b05af895..9eb55459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ feat_common_core = [ "lsmem", "mcookie", "mesg", + "mount", "mountpoint", "nologin", "renice", @@ -108,6 +109,7 @@ lslocks = { optional = true, version = "0.0.1", package = "uu_lslocks", path = " lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/uu/lsmem" } mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = "src/uu/mcookie" } mesg = { optional = true, version = "0.0.1", package = "uu_mesg", path = "src/uu/mesg" } +mount = { optional = true, version = "0.0.1", package = "uu_mount", path = "src/uu/mount" } mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" } nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = "src/uu/nologin" } renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } @@ -121,6 +123,7 @@ ctor = "0.6.0" # dmesg test require fixed-boot-time feature turned on. dmesg = { version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg", features = ["fixed-boot-time"] } libc = { workspace = true } +mount = { version = "0.0.1", package = "uu_mount", path = "src/uu/mount" } pretty_assertions = "1" rand = { workspace = true } regex = { workspace = true } diff --git a/src/uu/mount/Cargo.toml b/src/uu/mount/Cargo.toml new file mode 100644 index 00000000..98088242 --- /dev/null +++ b/src/uu/mount/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "uu_mount" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/mount.rs" + +[[bin]] +name = "mount" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +libc = { workspace = true } +thiserror = { workspace = true } +uucore = { workspace = true } diff --git a/src/uu/mount/mount.md b/src/uu/mount/mount.md new file mode 100644 index 00000000..3399590a --- /dev/null +++ b/src/uu/mount/mount.md @@ -0,0 +1,109 @@ +# mount + +``` +mount [options] +mount [options] +mount [options] | +mount [options] --source [--target ] +mount [options] [--source ] --target +mount [options] --make-{shared,slave,private,unbindable} +mount [options] --make-{rshared,rslave,rprivate,runbindable} +``` + +Mount a filesystem, or show all currently mounted filesystems. + +When called without arguments, or with `-t` but no source and target, mount +prints all currently mounted filesystems (read from `/proc/mounts` on Linux). +The `-t` option filters the listing to entries of the given type. `-l` requests +labels in listing output when they can be discovered from `/dev/disk/by-label`. + +When called with a source device and a target directory, mount attaches the +filesystem found on the source to the target directory. + +When called with a single positional argument, mount resolves that argument via +`/etc/fstab`. The argument may be either a source specifier or a mount point. +`--source` and `--target` can be used to disambiguate that lookup. + +When called with `--make-*`, mount applies mount-propagation changes to the +specified mountpoint. If a normal mount is also requested in the same command, +the propagation changes are applied after the mount succeeds. + +## Supported contract + +This implementation currently targets **Linux** and is intended to provide a +strong baseline `mount` with the core operational semantics in place: + +- no-argument listing from `/proc/mounts` +- direct mounts plus `--bind`, `--rbind`, and `--move` +- `--all` with already-mounted skipping, `-t` filtering, `-O` filtering, and + optional `--fork` +- `/etc/fstab`-driven single-argument resolution, including alternate files via + `-T` +- label/UUID/PARTLABEL/PARTUUID resolution +- merged `fstab` + CLI `-o` options for fstab-derived mounts +- optional mountpoint creation via `-m` +- propagation changes via `--make-*` + +This command is **not yet a full upstream-compatible replacement** for every +advanced `mount(8)` feature. The supported behavior is intentionally explicit so +reviewers can evaluate the current contract clearly. + +## Options + +- `-a`, `--all` — mount all filesystems listed in `/etc/fstab` (respects `noauto` + and `-t` / `-O` filters) +- `-B`, `--bind` — bind-mount a subtree at another location (`MS_BIND`) +- `-R`, `--rbind` — recursively bind-mount a subtree (`MS_BIND | MS_REC`) +- `-M`, `--move` — atomically move a mounted subtree to a new location + (`MS_MOVE`) +- `--make-shared` — mark a subtree as shared +- `--make-slave` — mark a subtree as slave +- `--make-private` — mark a subtree as private +- `--make-unbindable` — mark a subtree as unbindable +- `--make-rshared` — recursively mark a whole subtree as shared +- `--make-rslave` — recursively mark a whole subtree as slave +- `--make-rprivate` — recursively mark a whole subtree as private +- `--make-runbindable` — recursively mark a whole subtree as unbindable +- `-f`, `--fake` — dry run; parse arguments and resolve devices but skip the + actual `mount(2)` syscall +- `-F`, `--fork` — with `--all`, mount matching filesystems in separate worker + processes +- `-T`, `--fstab PATH` — use an alternate fstab file instead of `/etc/fstab` +- `-l`, `--show-labels` — show filesystem labels in listing output when + available +- `-m`, `--mkdir` — create the target mountpoint if it does not already exist +- `-n`, `--no-mtab` — do not write an entry to `/etc/mtab` +- `-o`, `--options LIST` — comma-separated list of mount options (e.g. + `ro,noatime,uid=1000`); for `/etc/fstab`-resolved mounts, CLI options are + appended after `fstab` options so later values win +- `-O`, `--test-opts LIST` — with `--all`, limit mounts to fstab entries whose + option field matches `LIST` +- `-r`, `--read-only` — mount read-only (same as `-o ro`) +- `-w`, `--read-write` — mount read-write, overriding a `ro` option from fstab +- `-t`, `--types LIST` — filesystem type filter; prefix a type with `no` to + exclude it (e.g. `-t noext4`) +- `-v`, `--verbose` — print a diagnostic line for each mount operation +- `-L`, `--label LABEL` — mount the device with the given filesystem label +- `-U`, `--uuid UUID` — mount the device with the given filesystem UUID +- `--partlabel LABEL` — mount the partition with the given partition label + (`PARTLABEL=`) +- `--partuuid UUID` — mount the partition with the given partition UUID + (`PARTUUID=`) +- `--source SOURCE` — explicitly specify the source side of the mount or the + single-argument fstab lookup key +- `--target DIRECTORY` — explicitly specify the target side of the mount or the + single-argument fstab lookup key + +## Notes + +- `--make-*` propagation operations are not combined with `--all`. +- Propagation changes do not read `/etc/fstab`; provide the target mountpoint + explicitly when using them directly. + +## Deferred features + +Notable items that remain outside the current supported contract include: + +- alternate `--options-mode` handling beyond the current append-style merge +- helper-specific behaviors outside this in-process Linux implementation +- additional advanced `mount(8)` compatibility options not yet implemented diff --git a/src/uu/mount/src/errors.rs b/src/uu/mount/src/errors.rs new file mode 100644 index 00000000..4a205501 --- /dev/null +++ b/src/uu/mount/src/errors.rs @@ -0,0 +1,66 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use thiserror::Error; +use uucore::error::UError; + +pub const EXIT_MOUNT_FAILED: i32 = 32; + +#[derive(Error, Debug)] +pub enum MountError { + #[error("cannot open /proc/mounts: {0}")] + ProcMounts(std::io::Error), + + #[error("cannot read {0}: {1}")] + FstabRead(String, std::io::Error), + + #[error("I/O error: {0}")] + Fstab(#[from] std::io::Error), + + #[error("cannot find device with label {0:?}")] + LabelNotFound(String), + + #[error("cannot find device with UUID {0:?}")] + UuidNotFound(String), + + #[error("cannot find mount entry for {0:?} in fstab")] + FstabEntryNotFound(String), + + #[error("no mount point specified and none found in fstab for {0}")] + NoMountPoint(String), + + #[error("cannot create mount point {0}: {1}")] + CreateMountPoint(String, std::io::Error), + + #[error("cannot fork mount worker: {0}")] + Fork(std::io::Error), + + #[error("cannot wait for mount worker: {0}")] + Wait(std::io::Error), + + #[error("invalid source path: {0}")] + InvalidSource(std::ffi::NulError), + + #[error("invalid target path: {0}")] + InvalidTarget(std::ffi::NulError), + + #[error("invalid filesystem type: {0}")] + InvalidFSType(std::ffi::NulError), + + #[error("invalid mount options: {0}")] + InvalidOptions(std::ffi::NulError), + + #[error("mount: {1} on {2}: {0}")] + MountFailed(std::io::Error, String, String), +} + +impl UError for MountError { + fn code(&self) -> i32 { + match self { + MountError::MountFailed(_, _, _) => EXIT_MOUNT_FAILED, + _ => 1, + } + } +} diff --git a/src/uu/mount/src/escape.rs b/src/uu/mount/src/escape.rs new file mode 100644 index 00000000..43239fb1 --- /dev/null +++ b/src/uu/mount/src/escape.rs @@ -0,0 +1,32 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +/// Expand octal escape sequences of the form `\NNN` used in mount-table style +/// files to encode whitespace and other special characters. +pub(crate) fn unescape_octal(s: &str) -> String { + let mut result: Vec = 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 (a, b, c) = (bytes[i + 1], bytes[i + 2], bytes[i + 3]); + if a.is_ascii_digit() + && a < b'8' + && b.is_ascii_digit() + && b < b'8' + && c.is_ascii_digit() + && c < b'8' + { + let value = (a - b'0') * 64 + (b - b'0') * 8 + (c - b'0'); + result.push(value); + i += 4; + continue; + } + } + result.push(bytes[i]); + i += 1; + } + String::from_utf8_lossy(&result).into_owned() +} diff --git a/src/uu/mount/src/fstab.rs b/src/uu/mount/src/fstab.rs new file mode 100644 index 00000000..7d17eb62 --- /dev/null +++ b/src/uu/mount/src/fstab.rs @@ -0,0 +1,419 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::errors::MountError; +use crate::escape::unescape_octal; +#[cfg(target_os = "linux")] +use std::path::Path; + +/// Case-insensitive prefix strip that preserves the original case of the +/// remaining slice (the prefix must be ASCII-only for correctness). +fn strip_ci_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { + if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) { + Some(&s[prefix.len()..]) + } else { + None + } +} + +/// A parsed entry from `/etc/fstab`. +#[derive(Debug, PartialEq, Clone)] +pub struct FsTabEntry { + /// Device or remote filesystem (e.g. `/dev/sda1`, `UUID=...`, `LABEL=...`). + pub fs_spec: String, + /// Mount point (e.g. `/`, `/boot`). + pub fs_file: String, + /// Filesystem type (e.g. `ext4`, `tmpfs`). + pub fs_vfstype: String, + /// Mount options (e.g. `defaults`, `ro,noatime`). + pub fs_mntops: String, + /// Dump frequency (0 = never backed up). + pub fs_freq: i32, + /// `fsck` pass order (0 = skip). + pub fs_passno: i32, +} + +/// The parameters needed to perform a mount, as resolved from a label or UUID. +#[derive(Debug, PartialEq)] +pub struct ResolvedMount { + /// Actual block device path (e.g. `/dev/sda1`). + pub source: String, + /// Mount point (e.g. `/boot`). + pub target: String, + /// Filesystem type, if known. + pub fs_type: Option, + /// Mount options string. + pub options: String, +} + +/// Parse the contents of an `/etc/fstab`-formatted string into a list of +/// entries. +/// +/// - Lines beginning with `#` and blank lines are ignored. +/// - Fields are separated by any amount of whitespace (spaces or tabs). +/// - Octal escape sequences (`\040`, `\011`, …) in field values are expanded. +/// - The `fs_freq` and `fs_passno` fields are optional and default to `0`. +/// - Lines with fewer than four fields are silently skipped. +pub fn parse_fstab_contents(contents: &str) -> Vec { + let mut entries = Vec::new(); + for line in contents.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 4 { + continue; + } + let fs_freq = fields.get(4).and_then(|s| s.parse().ok()).unwrap_or(0); + let fs_passno = fields.get(5).and_then(|s| s.parse().ok()).unwrap_or(0); + entries.push(FsTabEntry { + fs_spec: unescape_octal(fields[0]), + fs_file: unescape_octal(fields[1]), + fs_vfstype: fields[2].to_string(), + fs_mntops: fields[3].to_string(), + fs_freq, + fs_passno, + }); + } + entries +} + +/// Read and parse an fstab-formatted file. +#[cfg(target_os = "linux")] +pub fn parse_fstab_path(path: &Path) -> Result, MountError> { + let contents = std::fs::read_to_string(path) + .map_err(|err| MountError::FstabRead(path.display().to_string(), err))?; + Ok(parse_fstab_contents(&contents)) +} + +// ── Label / UUID resolution ────────────────────────────────────────────────── + +/// Build a [`ResolvedMount`] from a set of fstab `entries` where `fs_spec` +/// matches `spec` (compared case-insensitively). +/// +/// `display` is used in error messages (e.g. `"LABEL=boot"`). +fn resolve_from_entries( + spec: &str, + display: &str, + cli_target: Option<&str>, + entries: &[FsTabEntry], + not_found_err: MountError, +) -> Result { + let entry = entries + .iter() + .find(|e| e.fs_spec.eq_ignore_ascii_case(spec)) + .cloned() + .ok_or(not_found_err)?; + + let target = match cli_target { + Some(t) => t.to_string(), + None => entry.fs_file.clone(), + }; + + // Guard against accidentally trying to "mount" a swap entry with no + // meaningful mount point (fs_file is "none"). + if target == "none" && cli_target.is_none() { + return Err(MountError::NoMountPoint(display.to_string())); + } + + Ok(ResolvedMount { + source: entry.fs_spec.clone(), + target, + fs_type: Some(entry.fs_vfstype.clone()), + options: entry.fs_mntops.clone(), + }) +} + +/// Resolve a filesystem **label** to mount parameters using the supplied +/// pre-loaded fstab `entries`. +/// +/// Resolution order: +/// 1. `/dev/disk/by-label/