From 8d0a8390767d5ca28d0a419919c6ddbc43e1bf21 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Sat, 6 Dec 2025 19:36:26 +0900 Subject: [PATCH 01/12] feat(rm): Implement --one-file-system and --preserved-root=all This comment enhances the safety of recursive deletion in `rm` by introducing two key features to prevent accidental data loss. `--one-file-system`: When specified, `rm` will not traverse into directories that are on a different file system from the one on which the traversal began, this prevents `rm -r` from accidentally deleting data on mounted volumes. `--preserve-root=all`: The `--preserve-root` option is enhanced to accept an optional argument, `all`. When set to `all`, `rm` will refuse to remove any directory that is a mount point. The default behavior (`--preserve-root` without an argument) remains, protecting only the root directory (`/`). --- src/uu/df/src/filesystem.rs | 14 +-- src/uu/rm/Cargo.toml | 9 +- src/uu/rm/src/rm.rs | 122 +++++++++++++++++++++-- src/uucore/src/lib/features/fsext.rs | 8 +- tests/by-util/test_rm.rs | 138 +++++++++++++++++++++++++++ util/build-gnu.sh | 5 + 6 files changed, 276 insertions(+), 20 deletions(-) diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 25743941db0..fd6b6420309 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -121,19 +121,15 @@ where impl Filesystem { // TODO: resolve uuid in `mount_info.dev_name` if exists pub(crate) fn new(mount_info: MountInfo, file: Option) -> Option { + #[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)] diff --git a/src/uu/rm/Cargo.toml b/src/uu/rm/Cargo.toml index ccf1bf93e28..7f0198e9730 100644 --- a/src/uu/rm/Cargo.toml +++ b/src/uu/rm/Cargo.toml @@ -18,11 +18,16 @@ workspace = true path = "src/rm.rs" [dependencies] -thiserror = { workspace = true } clap = { workspace = true } -uucore = { workspace = true, features = ["fs", "parser", "safe-traversal"] } fluent = { workspace = true } indicatif = { workspace = true } +thiserror = { workspace = true } +uucore = { workspace = true, features = [ + "fs", + "fsext", + "parser", + "safe-traversal", +] } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index a20a57d7f36..83d3010211b 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -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}; @@ -126,6 +127,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 @@ -152,7 +160,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` @@ -173,7 +181,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, @@ -242,7 +250,18 @@ 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::(OPT_PRESERVE_ROOT) + .unwrap() + .as_str() + { + "all" => PreserveRoot::YesAll, + _ => PreserveRoot::Default, + } + }, recursive: matches.get_flag(OPT_RECURSIVE), dir: matches.get_flag(OPT_DIR), verbose: matches.get_flag(OPT_VERBOSE), @@ -341,7 +360,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) @@ -460,7 +482,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. @@ -596,7 +617,19 @@ 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 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() + ); + 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) @@ -684,9 +717,82 @@ 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("")?.to_path_buf(); + + // Find mount points for child and parent + let child_mount = mount_for_path(&child_canon, &fs_list).ok_or("")?; + let parent_mount = mount_for_path(&parent_canon, &fs_list).ok_or("")?; + + // 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 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; + } + let path = clean_trailing_slashes(path); if path_is_current_or_parent_directory(path) { show_error!( @@ -697,9 +803,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); diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 78dfcceb23d..c8391a08a10 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -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, diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 38230f2ad36..6c0fb69b881 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -1150,6 +1150,144 @@ fn test_rm_directory_not_writable() { assert!(!at.dir_exists("b/d")); // Should be removed } +#[test] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] +fn test_rm_one_file_system() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Test must be run as root (or with `sudo -E`) + if scene.cmd("whoami").run().stdout_str() != "root\n" { + println!("Skipping test_rm_one_file_system: must be run as root"); + return; + } + + // Define paths for temporary files and directories + let img_path = "fs.img"; + let mount_point = "fs"; + let remove_dir = "a"; + let bind_mount_point = "a/b"; + + at.touch(img_path); + + // Create filesystem image + scene + .cmd("dd") + .args(&[ + "if=/dev/zero", + &format!("of={img_path}"), + "bs=1M", + "count=50", + ]) + .succeeds(); + + // Create ext4 filesystem + scene.cmd("/sbin/mkfs.ext4").arg(img_path).succeeds(); + + // Prepare directory structure + at.mkdir_all(mount_point); + at.mkdir_all(bind_mount_point); + + // Mount as loop device + scene + .cmd("mount") + .args(&["-o", "loop", img_path, mount_point]) + .succeeds(); + + // Create test directory + at.mkdir_all(&format!("{mount_point}/x")); + + // Create bind mount + scene + .cmd("mount") + .args(&["--bind", mount_point, bind_mount_point]) + .succeeds(); + + // Run the test + scene + .ucmd() + .args(&["--one-file-system", "-rf", remove_dir]) + .fails() + .stderr_contains(format!("rm: skipping '{bind_mount_point}'")); + + // Cleanup + let _ = scene.cmd("umount").arg(bind_mount_point).run(); + let _ = scene.cmd("umount").arg(mount_point).run(); + let _ = scene + .cmd("rm") + .args(&["-rf", mount_point, bind_mount_point]) + .run(); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] +fn test_rm_preserve_root() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Test must be run as root (or with `sudo -E`) + if scene.cmd("whoami").run().stdout_str() != "root\n" { + println!("Skipping test_rm_one_file_system: must be run as root"); + return; + } + + // Define paths for temporary files and directories + let img_path = "fs.img"; + let mount_point = "fs"; + let bind_mount_point = "a/b"; + + at.touch(img_path); + + // Create filesystem image + scene + .cmd("dd") + .args(&[ + "if=/dev/zero", + &format!("of={img_path}"), + "bs=1M", + "count=50", + ]) + .succeeds(); + + // Create ext4 filesystem + scene.cmd("/sbin/mkfs.ext4").arg(img_path).succeeds(); + + // Prepare directory structure + at.mkdir_all(mount_point); + at.mkdir_all(bind_mount_point); + + // Mount as loop device + scene + .cmd("mount") + .args(&["-o", "loop", img_path, mount_point]) + .succeeds(); + + // Create test directory + at.mkdir_all(&format!("{mount_point}/x")); + + // Create bind mount + scene + .cmd("mount") + .args(&["--bind", mount_point, bind_mount_point]) + .succeeds(); + + // Run the test + scene + .ucmd() + .args(&["--preserve-root=all", "-rf", bind_mount_point]) + .fails() + .stderr_contains(format!("rm: skipping '{bind_mount_point}'")) + .stderr_contains("rm: and --preserve-root=all is in effect"); + + // Cleanup + let _ = scene.cmd("umount").arg(bind_mount_point).run(); + let _ = scene.cmd("umount").arg(mount_point).run(); + let _ = scene + .cmd("rm") + .args(&["-rf", mount_point, bind_mount_point]) + .run(); +} + #[test] fn test_progress_flag_short() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 223fc895b6c..2a7506fc341 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -203,6 +203,11 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r "${SED} "${SED}" -i -e "s|removed directory 'a/'|removed directory 'a'|g" tests/rm/v-slash.sh +if test "$(grep -c 'rm: skipping ' tests/rm/one-file-system.sh)" -eq 1; then + # Do it only once. + sed -i -e "s/ >> exp/ > exp/g" -e "s|rm: and --preserve-root=all is in effect|rm: skipping 'a/b', since it's on a different device\nrm: and --preserve-root=all is in effect|g" tests/rm/one-file-system.sh +fi + # 'rel' doesn't exist. Our implementation is giving a better message. "${SED}" -i -e "s|rm: cannot remove 'rel': Permission denied|rm: cannot remove 'rel': No such file or directory|g" tests/rm/inaccessible.sh From a0df78a4e0ba51c486214701b00a417020fc9589 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 20 Jan 2026 10:04:11 +0900 Subject: [PATCH 02/12] refactor(rm): improve preserve-root parsing safety - Replace `.unwrap()` with `.map()` to handle options safely without risking panics. - Update the abbreviation check to compare against the `PreserveRoot` enum directly. --- src/uu/rm/src/rm.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index bf9c9efe811..c113359d9f8 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -258,10 +258,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { match matches .get_one::(OPT_PRESERVE_ROOT) - .unwrap() - .as_str() + .map(|s| s.as_str()) { - "all" => PreserveRoot::YesAll, + Some("all") => PreserveRoot::YesAll, _ => PreserveRoot::Default, } }, @@ -278,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()); } From 2617af7968cf11089244d5087ab2e61351a1fe13 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 20 Jan 2026 10:30:22 +0900 Subject: [PATCH 03/12] refactor(rm): improve error context in check_one_fs Replace empty error strings with descriptive messages when path parent or mount point resolution fails. This aids in debugging by identifying specifically which path caused the resolution failure during the one-file-system check. --- src/uu/rm/src/rm.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index c113359d9f8..134e1c8faeb 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -750,11 +750,16 @@ fn check_one_fs(path: &Path, options: &Options) -> Result<(), String> { .map_err(|err| format!("cannot canonicalize {}: {err}", path.quote()))?; // Get parent path, handling root case - let parent_canon = child_canon.parent().ok_or("")?.to_path_buf(); + 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("")?; - let parent_mount = mount_for_path(&parent_canon, &fs_list).ok_or("")?; + 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 { From f1c343640450e8fdb66ba199dd02395addd7cfc9 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 20 Jan 2026 10:44:11 +0900 Subject: [PATCH 04/12] refactor(rm): extract one-fs and root preservation checks to helper Introduce `check_and_report_one_fs` to encapsulate the logic for verifying filesystem boundaries (`-x`) and root preservation. This removes code duplication between `remove_dir_recursive` and `handle_dir`, ensuring consistent error reporting for "different device" skips and `--preserve-root=all` violations. --- src/uu/rm/src/rm.rs | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 134e1c8faeb..9f030ea1e17 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -586,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 @@ -608,14 +629,7 @@ fn remove_dir_recursive( } // Base case 2: check if a path is on the same file system - 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 check_and_report_one_fs(path, options) { return true; } @@ -772,19 +786,7 @@ fn check_one_fs(path: &Path, options: &Options) -> Result<(), String> { fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool { let mut had_err = false; - 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"); - } - + if check_and_report_one_fs(path, options) { return true; } From 6918d8432bb6fb37b9e69bf3b23342b930666293 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 20 Jan 2026 11:45:42 +0900 Subject: [PATCH 05/12] refactor(df): separate platform-specific logic for stat_path Split the initialization of `stat_path` in `Filesystem::new` into distinct `#[cfg(unix)]` and `#[cfg(windows)]` blocks. This improves readability and ensures that Windows builds consistently use the volume ID (`dev_id`). --- src/uu/df/src/filesystem.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index 69d4cd5ec90..5124c00f9c8 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -121,21 +121,15 @@ where impl Filesystem { // TODO: resolve uuid in `mount_info.dev_name` if exists pub(crate) fn new(mount_info: MountInfo, file: Option) -> Option { + #[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 + 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()?); From 351e8b2d73ce56bae2ae62427d1fc0c76d34f833 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Fri, 30 Jan 2026 00:10:04 +0900 Subject: [PATCH 06/12] feat(rm): implement one-file-system check for safe recursive removal - Track device IDs (st_dev) during safe directory traversal to respect `--one-file-system` and `--preserve-root=all` options. - Pass the parent device ID through recursive calls in `safe_remove_dir_recursive_impl`. - Refactor `show_one_fs_error` into a reusable function to unify error reporting. - Ensure the parent directory is not removed if any child removal fails or is skipped. --- src/uu/rm/src/platform/unix.rs | 62 ++++++++++++++++++++++++++-------- src/uu/rm/src/rm.rs | 20 ++++++----- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index e890ab15823..3b068b62c01 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -11,6 +11,7 @@ use indicatif::ProgressBar; use std::ffi::OsStr; use std::fs; use std::io::{IsTerminal, stdin}; +use std::os::unix::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; use std::path::Path; use uucore::display::Quotable; @@ -21,9 +22,9 @@ use uucore::show_error; use uucore::translate; use super::super::{ - InteractiveMode, Options, is_dir_empty, is_readable_metadata, prompt_descend, remove_file, - show_permission_denied_error, show_removal_error, verbose_removed_directory, - verbose_removed_file, + InteractiveMode, Options, PreserveRoot, is_dir_empty, is_readable_metadata, prompt_descend, + remove_file, show_one_fs_error, show_permission_denied_error, show_removal_error, + verbose_removed_directory, verbose_removed_file, }; #[inline] @@ -279,11 +280,11 @@ pub fn safe_remove_dir_recursive( ) -> bool { // Base case 1: this is a file or a symbolic link. // Use lstat to avoid race condition between check and use - let initial_mode = match fs::symlink_metadata(path) { + let (initial_mode, initial_dev) = match fs::symlink_metadata(path) { Ok(metadata) if !metadata.is_dir() => { return remove_file(path, options, progress_bar); } - Ok(metadata) => metadata.permissions().mode(), + Ok(metadata) => (metadata.permissions().mode(), metadata.dev()), Err(e) => { return show_removal_error(e, path); } @@ -309,7 +310,7 @@ pub fn safe_remove_dir_recursive( } }; - let error = safe_remove_dir_recursive_impl(path, &dir_fd, options); + let error = safe_remove_dir_recursive_impl(path, &dir_fd, options, Some(initial_dev)); // After processing all children, remove the directory itself if error { @@ -346,7 +347,12 @@ pub fn safe_remove_dir_recursive( } #[cfg(not(target_os = "redox"))] -pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options) -> bool { +pub fn safe_remove_dir_recursive_impl( + path: &Path, + dir_fd: &DirFd, + options: &Options, + parent_dev: Option, +) -> bool { // Read directory entries using safe traversal let entries = match dir_fd.read_dir() { Ok(entries) => entries, @@ -380,11 +386,22 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt let is_dir = ((entry_stat.st_mode as libc::mode_t) & libc::S_IFMT) == libc::S_IFDIR; if is_dir { + if options.one_fs || options.preserve_root == PreserveRoot::YesAll { + if let Some(p_dev) = parent_dev { + if entry_stat.st_dev as u64 != p_dev { + show_one_fs_error(&entry_path, options); + error = true; + continue; + } + } + } + // Ask user if they want to descend into this directory if options.interactive == InteractiveMode::Always && !is_dir_empty(&entry_path) && !prompt_descend(&entry_path) { + error = true; continue; } @@ -408,25 +425,35 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt } }; - let child_error = safe_remove_dir_recursive_impl(&entry_path, &child_dir_fd, options); - error |= child_error; + let child_error = safe_remove_dir_recursive_impl( + &entry_path, + &child_dir_fd, + options, + Some(entry_stat.st_dev as u64) + ); + + // If a child could not be removed, the parent directory cannot be removed either. + if child_error { + error = true; + continue; + } // Ask user permission if needed for this subdirectory - if !child_error - && options.interactive == InteractiveMode::Always + if options.interactive == InteractiveMode::Always && !prompt_dir_with_mode(&entry_path, entry_stat.st_mode as libc::mode_t, options) { + error = true; continue; } // Remove the now-empty subdirectory using safe unlinkat - if !child_error { - error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, true, options); - } + error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, true, options); } else { // Remove file - check if user wants to remove it first if prompt_file_with_stat(&entry_path, &entry_stat, options) { error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, false, options); + } else { + error = true; } } } @@ -435,7 +462,12 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt } #[cfg(target_os = "redox")] -pub fn safe_remove_dir_recursive_impl(_path: &Path, _dir_fd: &DirFd, _options: &Options) -> bool { +pub fn safe_remove_dir_recursive_impl( + _path: &Path, + _dir_fd: &DirFd, + _options: &Options, + _parent_dev: Option, +) -> bool { // safe_traversal stat_at is not supported on Redox // This shouldn't be called on Redox, but provide a stub for compilation true // Return error diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 9f030ea1e17..002d8ea461c 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -586,6 +586,17 @@ fn is_writable_metadata(_metadata: &Metadata) -> bool { true } +pub(crate) fn show_one_fs_error(path: &Path, options: &Options) { + show_error!( + "skipping {}, since it's on a different device", + path.quote() + ); + + if !options.one_fs && options.preserve_root == PreserveRoot::YesAll { + show_error!("and --preserve-root=all is in effect"); + } +} + /// 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 { @@ -593,14 +604,7 @@ fn check_and_report_one_fs(path: &Path, options: &Options) -> bool { 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"); - } + show_one_fs_error(path, options); return true; } From b6a138a802c416bc82348e07307f5866e3d8e3e0 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Fri, 30 Jan 2026 11:16:19 +0900 Subject: [PATCH 07/12] fix(rm): fix error reporting when user declines interactive removal Previously, `safe_remove_dir_recursive` used a single boolean to track both system errors and skipped files. This meant that if a user declined to remove a file interactively, it was treated internally as an error. This change splits the state into `cmd_error` and `child_remains`. Now, declining a file prevents the parent directory from being removed (because it is not empty) but does not propagate a failure exit code. --- src/uu/rm/src/platform/unix.rs | 58 +++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index 3b068b62c01..9a7cce51c3c 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -310,11 +310,12 @@ pub fn safe_remove_dir_recursive( } }; - let error = safe_remove_dir_recursive_impl(path, &dir_fd, options, Some(initial_dev)); + let (cmd_error, child_remains) = + safe_remove_dir_recursive_impl(path, &dir_fd, options, Some(initial_dev)); // After processing all children, remove the directory itself - if error { - error + if cmd_error || child_remains { + cmd_error } else { // Ask user permission if needed if options.interactive == InteractiveMode::Always @@ -341,7 +342,7 @@ pub fn safe_remove_dir_recursive( if let Some(result) = safe_remove_empty_dir(path, options, progress_bar) { result } else { - remove_dir_with_special_cases(path, options, error) + remove_dir_with_special_cases(path, options, false) } } } @@ -352,7 +353,7 @@ pub fn safe_remove_dir_recursive_impl( dir_fd: &DirFd, options: &Options, parent_dev: Option, -) -> bool { +) -> (bool, bool) { // Read directory entries using safe traversal let entries = match dir_fd.read_dir() { Ok(entries) => entries, @@ -360,14 +361,15 @@ pub fn safe_remove_dir_recursive_impl( if !options.force { show_permission_denied_error(path); } - return !options.force; + return (!options.force, true); } Err(e) => { - return handle_error_with_force(e, path, options); + return (handle_error_with_force(e, path, options), true); } }; - let mut error = false; + let mut cmd_error = false; + let mut child_remains = false; // Process each entry for entry_name in entries { @@ -377,7 +379,8 @@ pub fn safe_remove_dir_recursive_impl( let entry_stat = match dir_fd.stat_at(&entry_name, false) { Ok(stat) => stat, Err(e) => { - error |= handle_error_with_force(e, &entry_path, options); + cmd_error |= handle_error_with_force(e, &entry_path, options); + child_remains = true; continue; } }; @@ -390,7 +393,8 @@ pub fn safe_remove_dir_recursive_impl( if let Some(p_dev) = parent_dev { if entry_stat.st_dev as u64 != p_dev { show_one_fs_error(&entry_path, options); - error = true; + cmd_error = true; + child_remains = true; continue; } } @@ -401,7 +405,7 @@ pub fn safe_remove_dir_recursive_impl( && !is_dir_empty(&entry_path) && !prompt_descend(&entry_path) { - error = true; + child_remains = true; continue; } @@ -412,29 +416,30 @@ pub fn safe_remove_dir_recursive_impl( // If we can't open the subdirectory for safe traversal, // try to handle it as best we can with safe operations if e.kind() == std::io::ErrorKind::PermissionDenied { - error |= handle_permission_denied( + cmd_error |= handle_permission_denied( dir_fd, entry_name.as_ref(), &entry_path, options, ); } else { - error |= handle_error_with_force(e, &entry_path, options); + cmd_error |= handle_error_with_force(e, &entry_path, options); } + child_remains = true; continue; } }; - let child_error = safe_remove_dir_recursive_impl( + let (c_error, c_remains) = safe_remove_dir_recursive_impl( &entry_path, &child_dir_fd, options, - Some(entry_stat.st_dev as u64) + Some(entry_stat.st_dev as u64), ); + cmd_error |= c_error; + child_remains |= c_remains; - // If a child could not be removed, the parent directory cannot be removed either. - if child_error { - error = true; + if c_error || c_remains { continue; } @@ -442,23 +447,26 @@ pub fn safe_remove_dir_recursive_impl( if options.interactive == InteractiveMode::Always && !prompt_dir_with_mode(&entry_path, entry_stat.st_mode as libc::mode_t, options) { - error = true; + child_remains = true; continue; } // Remove the now-empty subdirectory using safe unlinkat - error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, true, options); + cmd_error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, true, options); } else { // Remove file - check if user wants to remove it first if prompt_file_with_stat(&entry_path, &entry_stat, options) { - error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, false, options); + if handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, false, options) { + cmd_error = true; + child_remains = true; + } } else { - error = true; + child_remains = true; } } } - error + (cmd_error, child_remains) } #[cfg(target_os = "redox")] @@ -467,8 +475,8 @@ pub fn safe_remove_dir_recursive_impl( _dir_fd: &DirFd, _options: &Options, _parent_dev: Option, -) -> bool { +) -> (bool, bool) { // safe_traversal stat_at is not supported on Redox // This shouldn't be called on Redox, but provide a stub for compilation - true // Return error + (true, true) // Return error } From 71185474221b9310fdd97e47c1669ce482db2666 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Fri, 30 Jan 2026 12:02:20 +0900 Subject: [PATCH 08/12] fix(rm): set child_remains only when subdirectory removal actually fails This refines the recursive removal logic on Unix to avoid incorrectly preventing parent directory removal. Previously, any failure to open a subdirectory (e.g., due to EACCES) would unconditionally set `child_remains = true`. However, if the directory is unreadable but empty, `handle_permission_denied` may successfully unlink it. Now, we only set `child_remains` and `cmd_error` if the direct removal attempt (as a fallback for the open failure) actually fails. This fixes the `test_inaccessible_dir_recursive` test case where a parent directory remained even though its unreadable child was successfully removed. --- src/uu/rm/src/platform/unix.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index 9a7cce51c3c..34a4bb2699d 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -415,17 +415,16 @@ pub fn safe_remove_dir_recursive_impl( Err(e) => { // If we can't open the subdirectory for safe traversal, // try to handle it as best we can with safe operations - if e.kind() == std::io::ErrorKind::PermissionDenied { - cmd_error |= handle_permission_denied( - dir_fd, - entry_name.as_ref(), - &entry_path, - options, - ); + let failed = if e.kind() == std::io::ErrorKind::PermissionDenied { + handle_permission_denied(dir_fd, entry_name.as_ref(), &entry_path, options) } else { - cmd_error |= handle_error_with_force(e, &entry_path, options); + handle_error_with_force(e, &entry_path, options) + }; + + if failed { + cmd_error = true; + child_remains = true; } - child_remains = true; continue; } }; From 6c188dab28053296883c56eb5c80cbb4ae258b99 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 3 Feb 2026 09:54:38 +0900 Subject: [PATCH 09/12] refactor(rm): rename variables in unix platform for clarity Rename `cmd_error` to `had_command_error` and `child_remains` to `has_remaining_children` in `platform/unix.rs`. These new names better reflect the boolean nature of the variables and improve the readability of the control flow. --- src/uu/rm/src/platform/unix.rs | 41 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/uu/rm/src/platform/unix.rs b/src/uu/rm/src/platform/unix.rs index 34a4bb2699d..db4d2e1860b 100644 --- a/src/uu/rm/src/platform/unix.rs +++ b/src/uu/rm/src/platform/unix.rs @@ -310,12 +310,12 @@ pub fn safe_remove_dir_recursive( } }; - let (cmd_error, child_remains) = + let (had_command_error, has_remaining_children) = safe_remove_dir_recursive_impl(path, &dir_fd, options, Some(initial_dev)); // After processing all children, remove the directory itself - if cmd_error || child_remains { - cmd_error + if had_command_error || has_remaining_children { + had_command_error } else { // Ask user permission if needed if options.interactive == InteractiveMode::Always @@ -368,8 +368,8 @@ pub fn safe_remove_dir_recursive_impl( } }; - let mut cmd_error = false; - let mut child_remains = false; + let mut had_command_error = false; + let mut has_remaining_children = false; // Process each entry for entry_name in entries { @@ -379,8 +379,8 @@ pub fn safe_remove_dir_recursive_impl( let entry_stat = match dir_fd.stat_at(&entry_name, false) { Ok(stat) => stat, Err(e) => { - cmd_error |= handle_error_with_force(e, &entry_path, options); - child_remains = true; + had_command_error |= handle_error_with_force(e, &entry_path, options); + has_remaining_children = true; continue; } }; @@ -393,8 +393,8 @@ pub fn safe_remove_dir_recursive_impl( if let Some(p_dev) = parent_dev { if entry_stat.st_dev as u64 != p_dev { show_one_fs_error(&entry_path, options); - cmd_error = true; - child_remains = true; + had_command_error = true; + has_remaining_children = true; continue; } } @@ -405,7 +405,7 @@ pub fn safe_remove_dir_recursive_impl( && !is_dir_empty(&entry_path) && !prompt_descend(&entry_path) { - child_remains = true; + has_remaining_children = true; continue; } @@ -422,8 +422,8 @@ pub fn safe_remove_dir_recursive_impl( }; if failed { - cmd_error = true; - child_remains = true; + had_command_error = true; + has_remaining_children = true; } continue; } @@ -435,8 +435,8 @@ pub fn safe_remove_dir_recursive_impl( options, Some(entry_stat.st_dev as u64), ); - cmd_error |= c_error; - child_remains |= c_remains; + had_command_error |= c_error; + has_remaining_children |= c_remains; if c_error || c_remains { continue; @@ -446,26 +446,27 @@ pub fn safe_remove_dir_recursive_impl( if options.interactive == InteractiveMode::Always && !prompt_dir_with_mode(&entry_path, entry_stat.st_mode as libc::mode_t, options) { - child_remains = true; + has_remaining_children = true; continue; } // Remove the now-empty subdirectory using safe unlinkat - cmd_error |= handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, true, options); + had_command_error |= + handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, true, options); } else { // Remove file - check if user wants to remove it first if prompt_file_with_stat(&entry_path, &entry_stat, options) { if handle_unlink(dir_fd, entry_name.as_ref(), &entry_path, false, options) { - cmd_error = true; - child_remains = true; + had_command_error = true; + has_remaining_children = true; } } else { - child_remains = true; + has_remaining_children = true; } } } - (cmd_error, child_remains) + (had_command_error, has_remaining_children) } #[cfg(target_os = "redox")] From 7d1ba2e6ebfa935cd68f67fd8cbf0b6be0abb0e3 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 3 Feb 2026 11:10:16 +0900 Subject: [PATCH 10/12] refactor(rm): --one-file-system to use metadata-based device ID check - Replace path canonicalization and mount point scanning with a more efficient stat-based (device ID) check. - Add Windows support using volume_serial_number. - Introduce OneFsError enum for clearer error reporting. - Remove unused mount_for_path and read_fs_list. --- src/uu/rm/src/rm.rs | 119 +++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 002d8ea461c..d0672a4575a 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -15,13 +15,17 @@ use std::ops::BitOr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; #[cfg(unix)] +use std::os::unix::fs::MetadataExt; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; use std::path::MAIN_SEPARATOR; 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::fsext::MountInfo; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error}; @@ -597,20 +601,6 @@ pub(crate) fn show_one_fs_error(path: &Path, options: &Options) { } } -/// 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_one_fs_error(path, options); - - 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 @@ -725,63 +715,78 @@ 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; +#[derive(Debug)] +enum OneFsError { + /// The path is on a different device or file system (mount point boundary). + CrossDevice, + + /// Failed to retrieve metadata for the path or its parent. + StatFailed(String), +} - // Each `MountInfo` has a `mount_dir` that we compare. - for mi in mounts { - if mi.mount_dir.is_empty() { - continue; +/// 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 { + match check_one_fs(path, options) { + Ok(()) => false, + Err(OneFsError::StatFailed(msg)) => { + show_error!("{}", msg); + show_one_fs_error(path, options); + true } - 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)); - } + Err(OneFsError::CrossDevice) => { + show_one_fs_error(path, options); + true } } - - 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> { +fn check_one_fs(path: &Path, options: &Options) -> Result<(), OneFsError> { // 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()); + let child_meta = path + .symlink_metadata() + .map_err(|err| OneFsError::StatFailed(format!("cannot stat {}: {}", path.quote(), err)))?; + + let parent_path = match path.parent() { + Some(p) if !p.as_os_str().is_empty() => p, + _ => Path::new("."), + }; + + let parent_meta = parent_path.symlink_metadata().map_err(|err| { + OneFsError::StatFailed(format!("cannot stat parent of {}: {}", path.quote(), err)) + })?; + + let is_different = { + #[cfg(unix)] + { + child_meta.dev() != parent_meta.dev() + } + #[cfg(windows)] + { + match ( + child_meta.volume_serial_number(), + parent_meta.volume_serial_number(), + ) { + (Some(c), Some(p)) => c != p, + _ => false, + } + } + #[cfg(not(any(unix, windows)))] + { + false + } + }; + + if is_different { + return Err(OneFsError::CrossDevice); } Ok(()) From c6b28bd7f2c3ed6eb20f765726c68a93a6843b42 Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 3 Feb 2026 11:32:42 +0900 Subject: [PATCH 11/12] fix(rm): Windows build by using stable path prefix for device check - Replace unstable volume_serial_number() with std::path::Component::Prefix. This allows the filesystem boundary check to work on stable Rust while correctly identifying drive crossings on Windows. - Remove unused uucore::fsext::MountInfo import. --- src/uu/rm/src/rm.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index d0672a4575a..111908b1797 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -25,7 +25,6 @@ use std::path::{Path, PathBuf}; use thiserror::Error; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult}; -use uucore::fsext::MountInfo; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::translate; use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error}; @@ -771,10 +770,18 @@ fn check_one_fs(path: &Path, options: &Options) -> Result<(), OneFsError> { } #[cfg(windows)] { - match ( - child_meta.volume_serial_number(), - parent_meta.volume_serial_number(), - ) { + fn get_drive_prefix(p: &Path) -> Option<&std::ffi::OsStr> { + use std::path::Component; + p.components().next().and_then(|c| match c { + Component::Prefix(prefix) => Some(prefix.as_os_str()), + _ => None, + }) + } + + let child_drive = get_drive_prefix(path); + let parent_drive = get_drive_prefix(parent_path); + + match (child_drive, parent_drive) { (Some(c), Some(p)) => c != p, _ => false, } From 61e17bb03ea7fa7cc7fa69d2f277a23aabc7fe6f Mon Sep 17 00:00:00 2001 From: shskwmt Date: Tue, 3 Feb 2026 13:16:27 +0900 Subject: [PATCH 12/12] rm: fix unused metadata and enum variant warnings on Windows This patch fixes several warnings on Windows by: - Moving Unix-specific metadata retrieval into the `#[cfg(unix)]` block. - Guarding the `StatFailed` error variant and its handling with `#[cfg(unix)]`. - Removing the unused `MetadataExt` import for Windows. --- src/uu/rm/src/rm.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 111908b1797..c97f95f23e1 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -18,8 +18,6 @@ use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::MetadataExt; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; use std::path::MAIN_SEPARATOR; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -720,6 +718,7 @@ enum OneFsError { CrossDevice, /// Failed to retrieve metadata for the path or its parent. + #[cfg(unix)] StatFailed(String), } @@ -728,6 +727,7 @@ enum OneFsError { fn check_and_report_one_fs(path: &Path, options: &Options) -> bool { match check_one_fs(path, options) { Ok(()) => false, + #[cfg(unix)] Err(OneFsError::StatFailed(msg)) => { show_error!("{}", msg); show_one_fs_error(path, options); @@ -750,22 +750,22 @@ fn check_one_fs(path: &Path, options: &Options) -> Result<(), OneFsError> { return Ok(()); } - let child_meta = path - .symlink_metadata() - .map_err(|err| OneFsError::StatFailed(format!("cannot stat {}: {}", path.quote(), err)))?; - let parent_path = match path.parent() { Some(p) if !p.as_os_str().is_empty() => p, _ => Path::new("."), }; - let parent_meta = parent_path.symlink_metadata().map_err(|err| { - OneFsError::StatFailed(format!("cannot stat parent of {}: {}", path.quote(), err)) - })?; - let is_different = { #[cfg(unix)] { + let child_meta = path.symlink_metadata().map_err(|err| { + OneFsError::StatFailed(format!("cannot stat {}: {}", path.quote(), err)) + })?; + + let parent_meta = parent_path.symlink_metadata().map_err(|err| { + OneFsError::StatFailed(format!("cannot stat parent of {}: {}", path.quote(), err)) + })?; + child_meta.dev() != parent_meta.dev() } #[cfg(windows)]