Skip to content
Closed
15 changes: 6 additions & 9 deletions src/uu/df/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,16 @@ where
impl Filesystem {
// TODO: resolve uuid in `mount_info.dev_name` if exists
pub(crate) fn new(mount_info: MountInfo, file: Option<OsString>) -> Option<Self> {
#[cfg(unix)]
let stat_path = if mount_info.mount_dir.is_empty() {
#[cfg(unix)]
{
mount_info.dev_name.clone().into()
}
#[cfg(windows)]
{
// On windows, we expect the volume id
mount_info.dev_id.clone().into()
}
mount_info.dev_name.clone().into()
} else {
mount_info.mount_dir.clone()
};

#[cfg(windows)]
let stat_path = mount_info.dev_id.clone(); // On windows, we expect the volume id

#[cfg(unix)]
let usage = FsUsage::new(statfs(&stat_path).ok()?);
#[cfg(windows)]
Expand Down
9 changes: 7 additions & 2 deletions src/uu/rm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ workspace = true
path = "src/rm.rs"

[dependencies]
thiserror = { workspace = true }
clap = { workspace = true }
uucore = { workspace = true, features = ["fs", "parser"] }
fluent = { workspace = true }
indicatif = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = [
"fs",
"fsext",
"parser",
"safe-traversal",
] }

[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
uucore = { workspace = true, features = ["safe-traversal"] }
Expand Down
132 changes: 123 additions & 9 deletions src/uu/rm/src/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use std::path::{Path, PathBuf};
use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult};
use uucore::fsext::{MountInfo, read_fs_list};
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
use uucore::translate;
use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error};
Expand Down Expand Up @@ -128,6 +129,13 @@ impl From<&str> for InteractiveMode {
}
}

#[derive(PartialEq)]
pub enum PreserveRoot {
Default,
YesAll,
No,
}

/// Options for the `rm` command
///
/// All options are public so that the options can be programmatically
Expand All @@ -154,7 +162,7 @@ pub struct Options {
/// `--one-file-system`
pub one_fs: bool,
/// `--preserve-root`/`--no-preserve-root`
pub preserve_root: bool,
pub preserve_root: PreserveRoot,
/// `-r`, `--recursive`
pub recursive: bool,
/// `-d`, `--dir`
Expand All @@ -175,7 +183,7 @@ impl Default for Options {
force: false,
interactive: InteractiveMode::PromptProtected,
one_fs: false,
preserve_root: true,
preserve_root: PreserveRoot::Default,
recursive: false,
dir: false,
verbose: false,
Expand Down Expand Up @@ -245,7 +253,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
},
one_fs: matches.get_flag(OPT_ONE_FILE_SYSTEM),
preserve_root: !matches.get_flag(OPT_NO_PRESERVE_ROOT),
preserve_root: if matches.get_flag(OPT_NO_PRESERVE_ROOT) {
PreserveRoot::No
} else {
match matches
.get_one::<String>(OPT_PRESERVE_ROOT)
.map(|s| s.as_str())
{
Some("all") => PreserveRoot::YesAll,
_ => PreserveRoot::Default,
}
},
recursive: matches.get_flag(OPT_RECURSIVE),
dir: matches.get_flag(OPT_DIR),
verbose: matches.get_flag(OPT_VERBOSE),
Expand All @@ -259,7 +277,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {

// manually parse all args to verify --no-preserve-root did not get abbreviated (clap does
// allow this)
if !options.preserve_root && !args.iter().any(|arg| arg == "--no-preserve-root") {
if options.preserve_root == PreserveRoot::No
&& !args.iter().any(|arg| arg == "--no-preserve-root")
{
return Err(RmError::MayNotAbbreviateNoPreserveRoot.into());
}

Expand Down Expand Up @@ -351,7 +371,10 @@ pub fn uu_app() -> Command {
Arg::new(OPT_PRESERVE_ROOT)
.long(OPT_PRESERVE_ROOT)
.help(translate!("rm-help-preserve-root"))
.action(ArgAction::SetTrue),
.value_parser(["all"])
.default_value("all")
.default_missing_value("all")
.hide_default_value(true),
)
.arg(
Arg::new(OPT_RECURSIVE)
Expand Down Expand Up @@ -470,7 +493,6 @@ fn count_files_in_directory(p: &Path) -> u64 {
1 + entries_count
}

// TODO: implement one-file-system (this may get partially implemented in walkdir)
/// Remove (or unlink) the given files
///
/// Returns true if it has encountered an error.
Expand Down Expand Up @@ -564,6 +586,27 @@ fn is_writable_metadata(_metadata: &Metadata) -> bool {
true
}

/// Helper function to check fs and report errors if necessary.
/// Returns true if the operation should be skipped/returned (i.e., on error).
fn check_and_report_one_fs(path: &Path, options: &Options) -> bool {
if let Err(additional_reason) = check_one_fs(path, options) {
if !additional_reason.is_empty() {
show_error!("{}", additional_reason);
}
show_error!(
"skipping {}, since it's on a different device",
path.quote()
);

if options.preserve_root == PreserveRoot::YesAll {
show_error!("and --preserve-root=all is in effect");
}

return true;
}
false
}

/// Recursively remove the directory tree rooted at the given path.
///
/// If `path` is a file or a symbolic link, just remove it. If it is a
Expand All @@ -585,7 +628,12 @@ fn remove_dir_recursive(
return remove_file(path, options, progress_bar);
}

// Base case 2: this is a non-empty directory, but the user
// Base case 2: check if a path is on the same file system
if check_and_report_one_fs(path, options) {
return true;
}

// Base case 3: this is a non-empty directory, but the user
// doesn't want to descend into it.
if options.interactive == InteractiveMode::Always
&& !is_dir_empty(path)
Expand Down Expand Up @@ -673,9 +721,75 @@ fn remove_dir_recursive(
}
}

/// Return a reference to the best matching `MountInfo` whose `mount_dir`
/// is a prefix of the canonicalized `path`.
fn mount_for_path<'a>(path: &Path, mounts: &'a [MountInfo]) -> Option<&'a MountInfo> {
let canonical = path.canonicalize().ok()?;
let mut best: Option<(&MountInfo, usize)> = None;

// Each `MountInfo` has a `mount_dir` that we compare.
for mi in mounts {
if mi.mount_dir.is_empty() {
continue;
}
let mount_dir = PathBuf::from(&mi.mount_dir);
if canonical.starts_with(&mount_dir) {
let len = mount_dir.as_os_str().len();
// Pick the mount with the longest matching prefix.
if best.is_none() || len > best.as_ref().unwrap().1 {
best = Some((mi, len));
}
}
}

best.map(|(mi, _len)| mi)
}

/// Check if a path is on the same file system when `--one-file-system` or `--preserve-root=all` options are enabled.
/// Return `OK(())` if the path is on the same file system,
/// or an additional error describing why it should be skipped.
fn check_one_fs(path: &Path, options: &Options) -> Result<(), String> {
// If neither `--one-file-system` nor `--preserve-root=all` is active,
// always proceed
if !options.one_fs && options.preserve_root != PreserveRoot::YesAll {
return Ok(());
}

// Read mount information
let fs_list = read_fs_list().map_err(|err| format!("cannot read mount info: {err}"))?;

// Canonicalize the path
let child_canon = path
.canonicalize()
.map_err(|err| format!("cannot canonicalize {}: {err}", path.quote()))?;

// Get parent path, handling root case
let parent_canon = child_canon
.parent()
.ok_or_else(|| format!("cannot get parent of {}", child_canon.quote()))?
.to_path_buf();

// Find mount points for child and parent
let child_mount = mount_for_path(&child_canon, &fs_list)
.ok_or_else(|| format!("cannot find mount point for {}", child_canon.quote()))?;
let parent_mount = mount_for_path(&parent_canon, &fs_list)
.ok_or_else(|| format!("cannot find mount point for {}", parent_canon.quote()))?;

// Check if child and parent are on the same device
if child_mount.dev_id != parent_mount.dev_id {
return Err(String::new());
}

Ok(())
}

fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
let mut had_err = false;

if check_and_report_one_fs(path, options) {
return true;
}

let path = clean_trailing_slashes(path);
if path_is_current_or_parent_directory(path) {
show_error!(
Expand All @@ -686,9 +800,9 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
}

let is_root = path.has_root() && path.parent().is_none();
if options.recursive && (!is_root || !options.preserve_root) {
if options.recursive && (!is_root || options.preserve_root == PreserveRoot::No) {
had_err = remove_dir_recursive(path, options, progress_bar);
} else if options.dir && (!is_root || !options.preserve_root) {
} else if options.dir && (!is_root || options.preserve_root == PreserveRoot::No) {
had_err = remove_dir(path, options, progress_bar).bitor(had_err);
} else if options.recursive {
show_error!("{}", RmError::DangerousRecursiveOperation);
Expand Down
8 changes: 7 additions & 1 deletion src/uucore/src/lib/features/fsext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,18 @@ impl MountInfo {
let mount_root = to_nul_terminated_wide_string(&mount_root);
GetDriveTypeW(mount_root.as_ptr())
};

let mount_dir = Path::new(&mount_root)
.canonicalize()
.unwrap_or_default()
.into_os_string();

Some(Self {
dev_id: volume_name,
dev_name,
fs_type: fs_type.unwrap_or_default(),
mount_root: mount_root.into(), // TODO: We should figure out how to keep an OsString here.
mount_dir: OsString::new(),
mount_dir,
mount_option: String::new(),
remote,
dummy: false,
Expand Down
Loading
Loading