@@ -21,6 +21,7 @@ use std::path::{Path, PathBuf};
2121use thiserror:: Error ;
2222use uucore:: display:: Quotable ;
2323use uucore:: error:: { FromIo , UError , UResult } ;
24+ use uucore:: fsext:: { MountInfo , read_fs_list} ;
2425use uucore:: parser:: shortcut_value_parser:: ShortcutValueParser ;
2526use uucore:: translate;
2627use uucore:: { format_usage, os_str_as_bytes, prompt_yes, show_error} ;
@@ -126,6 +127,13 @@ impl From<&str> for InteractiveMode {
126127 }
127128}
128129
130+ #[ derive( PartialEq ) ]
131+ pub enum PreserveRoot {
132+ Default ,
133+ YesAll ,
134+ No ,
135+ }
136+
129137/// Options for the `rm` command
130138///
131139/// All options are public so that the options can be programmatically
@@ -152,7 +160,7 @@ pub struct Options {
152160 /// `--one-file-system`
153161 pub one_fs : bool ,
154162 /// `--preserve-root`/`--no-preserve-root`
155- pub preserve_root : bool ,
163+ pub preserve_root : PreserveRoot ,
156164 /// `-r`, `--recursive`
157165 pub recursive : bool ,
158166 /// `-d`, `--dir`
@@ -173,7 +181,7 @@ impl Default for Options {
173181 force : false ,
174182 interactive : InteractiveMode :: PromptProtected ,
175183 one_fs : false ,
176- preserve_root : true ,
184+ preserve_root : PreserveRoot :: Default ,
177185 recursive : false ,
178186 dir : false ,
179187 verbose : false ,
@@ -242,7 +250,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
242250 }
243251 } ,
244252 one_fs : matches. get_flag ( OPT_ONE_FILE_SYSTEM ) ,
245- preserve_root : !matches. get_flag ( OPT_NO_PRESERVE_ROOT ) ,
253+ preserve_root : if matches. get_flag ( OPT_NO_PRESERVE_ROOT ) {
254+ PreserveRoot :: No
255+ } else {
256+ match matches
257+ . get_one :: < String > ( OPT_PRESERVE_ROOT )
258+ . unwrap ( )
259+ . as_str ( )
260+ {
261+ "all" => PreserveRoot :: YesAll ,
262+ _ => PreserveRoot :: Default ,
263+ }
264+ } ,
246265 recursive : matches. get_flag ( OPT_RECURSIVE ) ,
247266 dir : matches. get_flag ( OPT_DIR ) ,
248267 verbose : matches. get_flag ( OPT_VERBOSE ) ,
@@ -341,7 +360,10 @@ pub fn uu_app() -> Command {
341360 Arg :: new ( OPT_PRESERVE_ROOT )
342361 . long ( OPT_PRESERVE_ROOT )
343362 . help ( translate ! ( "rm-help-preserve-root" ) )
344- . action ( ArgAction :: SetTrue ) ,
363+ . value_parser ( [ "all" ] )
364+ . default_value ( "all" )
365+ . default_missing_value ( "all" )
366+ . hide_default_value ( true ) ,
345367 )
346368 . arg (
347369 Arg :: new ( OPT_RECURSIVE )
@@ -460,7 +482,6 @@ fn count_files_in_directory(p: &Path) -> u64 {
460482 1 + entries_count
461483}
462484
463- // TODO: implement one-file-system (this may get partially implemented in walkdir)
464485/// Remove (or unlink) the given files
465486///
466487/// Returns true if it has encountered an error.
@@ -596,7 +617,19 @@ fn remove_dir_recursive(
596617 return remove_file ( path, options, progress_bar) ;
597618 }
598619
599- // Base case 2: this is a non-empty directory, but the user
620+ // Base case 2: check if a path is on the same file system
621+ if let Err ( additional_reason) = check_one_fs ( path, options) {
622+ if !additional_reason. is_empty ( ) {
623+ show_error ! ( "{}" , additional_reason) ;
624+ }
625+ show_error ! (
626+ "skipping {}, since it's on a different device" ,
627+ path. quote( )
628+ ) ;
629+ return true ;
630+ }
631+
632+ // Base case 3: this is a non-empty directory, but the user
600633 // doesn't want to descend into it.
601634 if options. interactive == InteractiveMode :: Always
602635 && !is_dir_empty ( path)
@@ -684,9 +717,82 @@ fn remove_dir_recursive(
684717 }
685718}
686719
720+ /// Return a reference to the best matching `MountInfo` whose `mount_dir`
721+ /// is a prefix of the canonicalized `path`.
722+ fn mount_for_path < ' a > ( path : & Path , mounts : & ' a [ MountInfo ] ) -> Option < & ' a MountInfo > {
723+ let canonical = path. canonicalize ( ) . ok ( ) ?;
724+ let mut best: Option < ( & MountInfo , usize ) > = None ;
725+
726+ // Each `MountInfo` has a `mount_dir` that we compare.
727+ for mi in mounts {
728+ if mi. mount_dir . is_empty ( ) {
729+ continue ;
730+ }
731+ let mount_dir = PathBuf :: from ( & mi. mount_dir ) ;
732+ if canonical. starts_with ( & mount_dir) {
733+ let len = mount_dir. as_os_str ( ) . len ( ) ;
734+ // Pick the mount with the longest matching prefix.
735+ if best. is_none ( ) || len > best. as_ref ( ) . unwrap ( ) . 1 {
736+ best = Some ( ( mi, len) ) ;
737+ }
738+ }
739+ }
740+
741+ best. map ( |( mi, _len) | mi)
742+ }
743+
744+ /// Check if a path is on the same file system when `--one-file-system` or `--preserve-root=all` options are enabled.
745+ /// Return `OK(())` if the path is on the same file system,
746+ /// or an additional error describing why it should be skipped.
747+ fn check_one_fs ( path : & Path , options : & Options ) -> Result < ( ) , String > {
748+ // If neither `--one-file-system` nor `--preserve-root=all` is active,
749+ // always proceed
750+ if !options. one_fs && options. preserve_root != PreserveRoot :: YesAll {
751+ return Ok ( ( ) ) ;
752+ }
753+
754+ // Read mount information
755+ let fs_list = read_fs_list ( ) . map_err ( |err| format ! ( "cannot read mount info: {err}" ) ) ?;
756+
757+ // Canonicalize the path
758+ let child_canon = path
759+ . canonicalize ( )
760+ . map_err ( |err| format ! ( "cannot canonicalize {}: {err}" , path. quote( ) ) ) ?;
761+
762+ // Get parent path, handling root case
763+ let parent_canon = child_canon. parent ( ) . ok_or ( "" ) ?. to_path_buf ( ) ;
764+
765+ // Find mount points for child and parent
766+ let child_mount = mount_for_path ( & child_canon, & fs_list) . ok_or ( "" ) ?;
767+ let parent_mount = mount_for_path ( & parent_canon, & fs_list) . ok_or ( "" ) ?;
768+
769+ // Check if child and parent are on the same device
770+ if child_mount. dev_id != parent_mount. dev_id {
771+ return Err ( String :: new ( ) ) ;
772+ }
773+
774+ Ok ( ( ) )
775+ }
776+
687777fn handle_dir ( path : & Path , options : & Options , progress_bar : Option < & ProgressBar > ) -> bool {
688778 let mut had_err = false ;
689779
780+ if let Err ( additional_reason) = check_one_fs ( path, options) {
781+ if !additional_reason. is_empty ( ) {
782+ show_error ! ( "{}" , additional_reason) ;
783+ }
784+ show_error ! (
785+ "skipping {}, since it's on a different device" ,
786+ path. quote( )
787+ ) ;
788+
789+ if options. preserve_root == PreserveRoot :: YesAll {
790+ show_error ! ( "and --preserve-root=all is in effect" ) ;
791+ }
792+
793+ return true ;
794+ }
795+
690796 let path = clean_trailing_slashes ( path) ;
691797 if path_is_current_or_parent_directory ( path) {
692798 show_error ! (
@@ -697,9 +803,9 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
697803 }
698804
699805 let is_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
700- if options. recursive && ( !is_root || ! options. preserve_root ) {
806+ if options. recursive && ( !is_root || options. preserve_root == PreserveRoot :: No ) {
701807 had_err = remove_dir_recursive ( path, options, progress_bar) ;
702- } else if options. dir && ( !is_root || ! options. preserve_root ) {
808+ } else if options. dir && ( !is_root || options. preserve_root == PreserveRoot :: No ) {
703809 had_err = remove_dir ( path, options, progress_bar) . bitor ( had_err) ;
704810 } else if options. recursive {
705811 show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
0 commit comments