Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6783b52
feat: implement `--one-file-system` for `rm` to prevent recursive rem…
mattsu2020 Nov 20, 2025
7ac5f04
refactor: update string formatting to use f-string like syntax
mattsu2020 Nov 20, 2025
efb91a3
style: Improve code readability by reformatting function calls, argum…
mattsu2020 Nov 20, 2025
e86426b
refactor: update `hdiutil` command arguments to use arrays directly a…
mattsu2020 Nov 20, 2025
185fe36
chore: Add 'volname' to spell-checker ignore list.
mattsu2020 Nov 20, 2025
d9ea830
refactor: conditionally compile `SkippingDirectoryOnDifferentDevice` …
mattsu2020 Nov 20, 2025
1ad8a2c
refactor: remove unused variable prefix for `t_path` in `rm` test
mattsu2020 Nov 20, 2025
2058f02
refactor(test): move 't' and 't_path' vars into Linux-specific block …
mattsu2020 Nov 20, 2025
1317285
refactor(test_rm)!: conditionally compile test_one_file_system for Li…
mattsu2020 Nov 20, 2025
ca5707f
test(rm): Remove early return in test_one_file_system for unsupported OS
mattsu2020 Nov 20, 2025
7203d62
feat: implement --one-file-system and --preserve-root=all for rm
mattsu2020 Nov 20, 2025
e226f50
feat(rm/linux): add conditional import for Unix MetadataExt
mattsu2020 Nov 20, 2025
cb71eb3
feat: add PreserveRootAllInEffect error and conditional dev ID handli…
mattsu2020 Nov 20, 2025
ac13e6a
refactor(linux): simplify conditional expression and reorder imports
mattsu2020 Nov 20, 2025
37a462d
fix(locales): remove redundant 'rm:' prefix from preserve-root error …
mattsu2020 Nov 20, 2025
474cd3b
Merge branch 'main' into rm_compatibility
mattsu2020 Nov 20, 2025
8c1ca94
feat: add French localization and fix --one-file-system tests for rm
mattsu2020 Nov 21, 2025
4b1cba8
refactor(rm): extract next_parent_dev_id calculation to avoid duplica…
mattsu2020 Nov 22, 2025
70298a8
Merge branch 'main' into rm_compatibility
mattsu2020 Nov 22, 2025
08b401e
Merge branch 'main' into rm_compatibility
mattsu2020 Dec 1, 2025
a03bf3f
Merge branch 'main' into rm_compatibility
mattsu2020 Dec 2, 2025
01ac5f3
Merge branch 'main' into rm_compatibility
mattsu2020 Dec 15, 2025
1525f6a
Merge branch 'main' into rm_compatibility
mattsu2020 Dec 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/uu/rm/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ rm-help-prompt-once = prompt once before removing more than three files, or when
rm-help-interactive = prompt according to WHEN: never, once (-I), or always (-i). Without WHEN,
prompts always
rm-help-one-file-system = when removing a hierarchy recursively, skip any directory that is on a file
system different from that of the corresponding command line argument (NOT
IMPLEMENTED)
system different from that of the corresponding command line argument
rm-help-no-preserve-root = do not treat '/' specially
rm-help-preserve-root = do not remove '/' (default)
rm-help-recursive = remove directories and their contents recursively
Expand All @@ -42,6 +41,8 @@ rm-error-cannot-remove-is-directory = cannot remove {$file}: Is a directory
rm-error-dangerous-recursive-operation = it is dangerous to operate recursively on '/'
rm-error-use-no-preserve-root = use --no-preserve-root to override this failsafe
rm-error-refusing-to-remove-directory = refusing to remove '.' or '..' directory: skipping '{$path}'
rm-error-skipping-directory-on-different-device = skipping '{$path}', since it's on a different device
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't in the french file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

rm-error-preserve-root-all = and --preserve-root=all is in effect
rm-error-cannot-remove = cannot remove {$file}

# Verbose messages
Expand Down
5 changes: 3 additions & 2 deletions src/uu/rm/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ rm-help-prompt-once = demander une fois avant de supprimer plus de trois fichier
rm-help-interactive = demander selon QUAND : never, once (-I), ou always (-i). Sans QUAND,
demande toujours
rm-help-one-file-system = lors de la suppression récursive d'une hiérarchie, ignorer tout répertoire situé sur un
système de fichiers différent de celui de l'argument de ligne de commande correspondant (NON
IMPLÉMENTÉ)
système de fichiers différent de celui de l'argument de ligne de commande correspondant
rm-help-no-preserve-root = ne pas traiter '/' spécialement
rm-help-preserve-root = ne pas supprimer '/' (par défaut)
rm-help-recursive = supprimer les répertoires et leur contenu récursivement
Expand All @@ -42,6 +41,8 @@ rm-error-cannot-remove-is-directory = impossible de supprimer {$file} : C'est un
rm-error-dangerous-recursive-operation = il est dangereux d'opérer récursivement sur '/'
rm-error-use-no-preserve-root = utilisez --no-preserve-root pour outrepasser cette protection
rm-error-refusing-to-remove-directory = refus de supprimer le répertoire '.' ou '..' : ignorer '{$path}'
rm-error-skipping-directory-on-different-device = ignorer '{$path}', car il se trouve sur un périphérique différent
rm-error-preserve-root-all = et --preserve-root=all est actif
rm-error-cannot-remove = impossible de supprimer {$file}

# Messages verbeux
Expand Down
60 changes: 55 additions & 5 deletions src/uu/rm/src/platform/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use indicatif::ProgressBar;
use std::ffi::OsStr;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use uucore::display::Quotable;
use uucore::error::FromIo;
Expand All @@ -18,8 +20,8 @@ use uucore::show_error;
use uucore::translate;

use super::super::{
InteractiveMode, Options, is_dir_empty, is_readable_metadata, prompt_descend, prompt_dir,
prompt_file, remove_file, show_permission_denied_error, show_removal_error,
InteractiveMode, Options, RmError, is_dir_empty, is_readable_metadata, prompt_descend,
prompt_dir, prompt_file, remove_file, show_permission_denied_error, show_removal_error,
verbose_removed_directory, verbose_removed_file,
};

Expand Down Expand Up @@ -193,6 +195,7 @@ pub fn safe_remove_dir_recursive(
path: &Path,
options: &Options,
progress_bar: Option<&ProgressBar>,
current_dev_id: Option<u64>,
) -> bool {
// Base case 1: this is a file or a symbolic link.
// Use lstat to avoid race condition between check and use
Expand All @@ -206,6 +209,16 @@ pub fn safe_remove_dir_recursive(
}
}

let check_device = options.one_fs || options.preserve_root_all;
#[cfg(unix)]
let current_dev_id = if check_device {
current_dev_id.or_else(|| path.symlink_metadata().ok().map(|m| m.dev()))
} else {
None
};
#[cfg(not(unix))]
let current_dev_id: Option<u64> = None;

// Try to open the directory using DirFd for secure traversal
let dir_fd = match DirFd::open(path) {
Ok(fd) => fd,
Expand All @@ -226,7 +239,12 @@ 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,
if check_device { current_dev_id } else { None },
);

// After processing all children, remove the directory itself
if error {
Expand Down Expand Up @@ -256,7 +274,12 @@ pub fn safe_remove_dir_recursive(
}
}

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_id: Option<u64>,
) -> bool {
// Read directory entries using safe traversal
let entries = match dir_fd.read_dir() {
Ok(entries) => entries,
Expand All @@ -272,6 +295,7 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
};

let mut error = false;
let check_device = options.one_fs || options.preserve_root_all;

// Process each entry
for entry_name in entries {
Expand All @@ -290,6 +314,23 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
let is_dir = (entry_stat.st_mode & libc::S_IFMT) == libc::S_IFDIR;

if is_dir {
if check_device {
if let Some(parent_dev_id) = parent_dev_id {
if entry_stat.st_dev != parent_dev_id {
show_error!(
"{}",
RmError::SkippingDirectoryOnDifferentDevice(
entry_path.as_os_str().to_os_string()
)
);
if options.preserve_root_all {
show_error!("{}", RmError::PreserveRootAllInEffect);
}
error = true;
continue;
}
}
}
// Ask user if they want to descend into this directory
if options.interactive == InteractiveMode::Always
&& !is_dir_empty(&entry_path)
Expand Down Expand Up @@ -318,7 +359,16 @@ 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);
let child_error = safe_remove_dir_recursive_impl(
&entry_path,
&child_dir_fd,
options,
if check_device {
Some(entry_stat.st_dev)
} else {
None
},
);
error = error || child_error;

// Ask user permission if needed for this subdirectory
Expand Down
88 changes: 80 additions & 8 deletions src/uu/rm/src/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::MAIN_SEPARATOR;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -45,6 +47,12 @@
UseNoPreserveRoot,
#[error("{}", translate!("rm-error-refusing-to-remove-directory", "path" => _0.to_string_lossy()))]
RefusingToRemoveDirectory(OsString),
#[error("{}", translate!("rm-error-skipping-directory-on-different-device", "path" => _0.to_string_lossy()))]
#[cfg(unix)]
SkippingDirectoryOnDifferentDevice(OsString),
#[cfg(unix)]
#[error("{}", translate!("rm-error-preserve-root-all"))]
PreserveRootAllInEffect,
}

impl UError for RmError {}
Expand Down Expand Up @@ -153,6 +161,8 @@
pub one_fs: bool,
/// `--preserve-root`/`--no-preserve-root`
pub preserve_root: bool,
/// `--preserve-root=all`
pub preserve_root_all: bool,
/// `-r`, `--recursive`
pub recursive: bool,
/// `-d`, `--dir`
Expand All @@ -174,6 +184,7 @@
interactive: InteractiveMode::PromptProtected,
one_fs: false,
preserve_root: true,
preserve_root_all: false,
recursive: false,
dir: false,
verbose: false,
Expand Down Expand Up @@ -243,6 +254,10 @@
},
one_fs: matches.get_flag(OPT_ONE_FILE_SYSTEM),
preserve_root: !matches.get_flag(OPT_NO_PRESERVE_ROOT),
preserve_root_all: matches

Check failure on line 257 in src/uu/rm/src/rm.rs

View workflow job for this annotation

GitHub Actions / Style and Lint (ubuntu-24.04, unix)

ERROR: `cargo clippy`: called `map(<f>).unwrap_or(false)` on an `Option` value (file:'src/uu/rm/src/rm.rs', line:257)

Check failure on line 257 in src/uu/rm/src/rm.rs

View workflow job for this annotation

GitHub Actions / Style and Lint (unix)

ERROR: `cargo clippy`: called `map(<f>).unwrap_or(false)` on an `Option` value (file:'src/uu/rm/src/rm.rs', line:257)
.get_many::<String>(OPT_PRESERVE_ROOT)
.map(|values| values.into_iter().any(|v| v == "all"))
.unwrap_or(false),
recursive: matches.get_flag(OPT_RECURSIVE),
dir: matches.get_flag(OPT_DIR),
verbose: matches.get_flag(OPT_VERBOSE),
Expand Down Expand Up @@ -341,7 +356,10 @@
Arg::new(OPT_PRESERVE_ROOT)
.long(OPT_PRESERVE_ROOT)
.help(translate!("rm-help-preserve-root"))
.action(ArgAction::SetTrue),
.num_args(0..=1)
.require_equals(true)
.value_parser(ShortcutValueParser::new([PossibleValue::new("all")]))
.action(ArgAction::Append),
)
.arg(
Arg::new(OPT_RECURSIVE)
Expand Down Expand Up @@ -485,8 +503,20 @@
}

any_files_processed = true;

#[cfg(unix)]
let parent_dev_id = if options.one_fs || options.preserve_root_all {
file.parent()
.and_then(|p| p.symlink_metadata().ok())
.map(|m| m.dev())
} else {
None
};
#[cfg(not(unix))]
let parent_dev_id = None;

if metadata.is_dir() {
handle_dir(file, options, progress_bar.as_ref())
handle_dir(file, options, progress_bar.as_ref(), parent_dev_id)
} else if is_symlink_dir(&metadata) {
remove_dir(file, options, progress_bar.as_ref())
} else {
Expand Down Expand Up @@ -585,14 +615,23 @@
path: &Path,
options: &Options,
progress_bar: Option<&ProgressBar>,
_parent_dev_id: Option<u64>,
) -> bool {
#[cfg(unix)]
let parent_dev_id = _parent_dev_id;

let metadata = match path.symlink_metadata() {
Ok(metadata) => metadata,
Err(e) => return show_removal_error(e, path),
};

// Base case 1: this is a file or a symbolic link.
//
// The symbolic link case is important because it could be a link to
// a directory and we don't want to recurse. In particular, this
// avoids an infinite recursion in the case of a link to the current
// directory, like `ln -s . link`.
if !path.is_dir() || path.is_symlink() {
if !metadata.is_dir() || path.is_symlink() {
return remove_file(path, options, progress_bar);
}

Expand All @@ -605,10 +644,34 @@
return false;
}

// Base case 3: this is a directory on a different device
#[cfg(unix)]
if let Some(parent_dev_id) = parent_dev_id {
if metadata.dev() != parent_dev_id {
show_error!(
"{}",
RmError::SkippingDirectoryOnDifferentDevice(path.as_os_str().to_os_string())
);
if options.preserve_root_all {
show_error!("{}", RmError::PreserveRootAllInEffect);
}
return true;
}
}

#[cfg(unix)]
let next_parent_dev_id = if options.one_fs || options.preserve_root_all {
Some(metadata.dev())
} else {
None
};
#[cfg(not(unix))]
let next_parent_dev_id = None;

// Use secure traversal on Linux for all recursive directory removals
#[cfg(target_os = "linux")]
{
safe_remove_dir_recursive(path, options, progress_bar)
safe_remove_dir_recursive(path, options, progress_bar, next_parent_dev_id)
}

// Fallback for non-Linux or use fs::remove_dir_all for very long paths
Expand Down Expand Up @@ -641,8 +704,12 @@
match entry {
Err(_) => error = true,
Ok(entry) => {
let child_error =
remove_dir_recursive(&entry.path(), options, progress_bar);
let child_error = remove_dir_recursive(
&entry.path(),
options,
progress_bar,
next_parent_dev_id,
);
error = error || child_error;
}
}
Expand Down Expand Up @@ -684,7 +751,12 @@
}
}

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

let path = clean_trailing_slashes(path);
Expand All @@ -698,7 +770,7 @@

let is_root = path.has_root() && path.parent().is_none();
if options.recursive && (!is_root || !options.preserve_root) {
had_err = remove_dir_recursive(path, options, progress_bar);
had_err = remove_dir_recursive(path, options, progress_bar, parent_dev_id);
} else if options.dir && (!is_root || !options.preserve_root) {
had_err = remove_dir(path, options, progress_bar).bitor(had_err);
} else if options.recursive {
Expand Down
Loading
Loading