-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
rm: Implement --one-file-system and --preserve-root=all #7569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
src/uu/rm/src/rm.rs
Outdated
| match validate_single_filesystem(path) { | ||
| Ok(()) => true, | ||
| Err(additional_reason) => { | ||
| 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"); | ||
| } | ||
| false | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can probably be simplified with something like:
| match validate_single_filesystem(path) { | |
| Ok(()) => true, | |
| Err(additional_reason) => { | |
| 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"); | |
| } | |
| false | |
| } | |
| } | |
| let result = validate_single_filesystem(path); | |
| if result.is_ok() { | |
| return true; | |
| } | |
| if let Err(additional_reason) = result { | |
| 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"); | |
| } | |
| false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for pointing that out! I've implemented the suggested simplification.
|
GNU testsuite comparison: |
|
some jobs are failing :) |
|
GNU testsuite comparison: |
|
GNU testsuite comparison: |
|
does |
|
GNU testsuite comparison: |
|
The following is the test result. It has passed. |
|
I tried again and it failed, so I will fix it. |
|
GNU testsuite comparison: |
|
GNU testsuite comparison: |
2b9e697 to
3882f38
Compare
|
GNU testsuite comparison: |
|
Sorry, i think i made a mistake with the conflict resolution :( could you please fix it? thanks |
|
No warries, I'll fix it. |
5e09b83 to
42a24fb
Compare
|
GNU testsuite comparison: |
|
sorry, i missed it :( |
42a24fb to
f4adf86
Compare
|
GNU testsuite comparison: |
f4adf86 to
5dd9c19
Compare
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 (`/`).
5dd9c19 to
8d0a839
Compare
|
GNU testsuite comparison: |
|
I've finished rebasing the branch. |
src/uu/rm/src/rm.rs
Outdated
| } else { | ||
| match matches | ||
| .get_one::<String>(OPT_PRESERVE_ROOT) | ||
| .unwrap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please remove the unwrap()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed:
a0df78a
src/uu/rm/src/rm.rs
Outdated
| .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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we avoid the empty strings in ok_or(") ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed:
2617af7
src/uu/rm/src/rm.rs
Outdated
| 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same code as in line 621, please dedup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed:
f1c3436
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.
|
GNU testsuite comparison: |
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`).
|
GNU testsuite comparison: |
|
GNU testsuite comparison: |
|
any idea why we don't get "Congrats! The gnu test tests/rm/one-file-system is no longer failing!" anymore? thanks |
|
It appears that one of the error messages is returning differently in the test: |
|
I'm taking a deeper look into the implementation and I'm curious about the decision to use the mount tables. The when using stat on the files you are able to get the device id from the st_dev value. When reading from the mount tables it brings in the potential for a race condition and that appears to be whats causing the GNU tests to fail. |
|
Would you be interested if I cherry-pick your PR and just modify the mount part to swap with the stat? Sorry that the feedback is getting there so much later than when you made the PR, but was wondering if this was something you were still interested in following up on? |
|
@ChrisDryden I actually have a fix ready locally, but I haven't been able to push it yet as I'm still in the middle of testing. It's a bit tough for me to find time during the week, so I'll likely get to it over the weekend. For reference, here is the current diff I'm working on. If this is urgent, please feel free to cherry-pick and modify it as you suggested! |
- 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.
|
GNU testsuite comparison: |
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.
|
GNU testsuite comparison: |
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.
|
GNU testsuite comparison: |
| path.quote() | ||
| ); | ||
|
|
||
| if !options.one_fs && options.preserve_root == PreserveRoot::YesAll { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preserve-root message should show when preserve_root == PreserveRoot::YesAll, not when both conditions are false
no ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, removing !options.one_fs causes the one-file-system.sh test to fail.
The test expects ONLY the first line when --one-file-system is present.
Since preserve-root defaults to all in our implementation, the second line would be printed even for a standard --one-file-system call if we only check for YesAll.
src/uu/rm/src/rm.rs
Outdated
| 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please replace with safer pattern matching to avoid the unwrap?
src/uu/rm/src/rm.rs
Outdated
| } | ||
|
|
||
| // Read mount information | ||
| let fs_list = read_fs_list().map_err(|err| format!("cannot read mount info: {err}"))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading mount table for every file check introduces race conditions and poor performance
src/uu/rm/src/rm.rs
Outdated
| let fs_list = read_fs_list().map_err(|err| format!("cannot read mount info: {err}"))?; | ||
|
|
||
| // Canonicalize the path | ||
| let child_canon = path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary canonicalization - using stat() device IDs would be more reliable and faster
no?
src/uu/rm/src/platform/unix.rs
Outdated
| }; | ||
|
|
||
| if failed { | ||
| cmd_error = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename it to had_command_error
src/uu/rm/src/platform/unix.rs
Outdated
|
|
||
| if failed { | ||
| cmd_error = true; | ||
| child_remains = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename it to has_remaining_children
src/uu/rm/src/rm.rs
Outdated
|
|
||
| // Check if child and parent are on the same device | ||
| if child_mount.dev_id != parent_mount.dev_id { | ||
| return Err(String::new()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning empty string as error is not clear - please consider using a specific error type or enum variant
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.
- 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.
- 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.
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.
Fixes #7011
Implement
--one-file-systemand--preserve-root=alloptions for thermcommand.