33// For the full copyright and license information, please view the LICENSE
44// file that was distributed with this source code.
55
6- // spell-checker:ignore (path) eacces inacc rm-r4 unlinkat fstatat
6+ // spell-checker:ignore (path) eacces inacc rm-r4 unlinkat fstatat rootlink
77
88use clap:: builder:: { PossibleValue , ValueParser } ;
99use clap:: { Arg , ArgAction , Command , parser:: ValueSource } ;
@@ -17,7 +17,7 @@ use std::os::unix::ffi::OsStrExt;
1717#[ cfg( unix) ]
1818use std:: os:: unix:: fs:: PermissionsExt ;
1919use std:: path:: MAIN_SEPARATOR ;
20- use std:: path:: { Path , PathBuf } ;
20+ use std:: path:: Path ;
2121use thiserror:: Error ;
2222use uucore:: display:: Quotable ;
2323use uucore:: error:: { FromIo , UError , UResult } ;
@@ -56,7 +56,7 @@ fn verbose_removed_file(path: &Path, options: &Options) {
5656 if options. verbose {
5757 println ! (
5858 "{}" ,
59- translate!( "rm-verbose-removed" , "file" => normalize ( path) . quote( ) )
59+ translate!( "rm-verbose-removed" , "file" => uucore :: fs :: normalize_path ( path) . quote( ) )
6060 ) ;
6161 }
6262}
@@ -66,7 +66,7 @@ fn verbose_removed_directory(path: &Path, options: &Options) {
6666 if options. verbose {
6767 println ! (
6868 "{}" ,
69- translate!( "rm-verbose-removed-directory" , "file" => normalize ( path) . quote( ) )
69+ translate!( "rm-verbose-removed-directory" , "file" => uucore :: fs :: normalize_path ( path) . quote( ) )
7070 ) ;
7171 }
7272}
@@ -229,6 +229,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
229229 } )
230230 } ;
231231
232+ let preserve_root = !matches. get_flag ( OPT_NO_PRESERVE_ROOT ) ;
233+ let recursive = matches. get_flag ( OPT_RECURSIVE ) ;
234+
232235 let options = Options {
233236 force : force_flag,
234237 interactive : {
@@ -245,8 +248,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
245248 }
246249 } ,
247250 one_fs : matches. get_flag ( OPT_ONE_FILE_SYSTEM ) ,
248- preserve_root : !matches . get_flag ( OPT_NO_PRESERVE_ROOT ) ,
249- recursive : matches . get_flag ( OPT_RECURSIVE ) ,
251+ preserve_root,
252+ recursive,
250253 dir : matches. get_flag ( OPT_DIR ) ,
251254 verbose : matches. get_flag ( OPT_VERBOSE ) ,
252255 progress : matches. get_flag ( OPT_PROGRESS ) ,
@@ -482,6 +485,19 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
482485 for filename in files {
483486 let file = Path :: new ( filename) ;
484487
488+ // Check if the path (potentially with trailing slash) resolves to root
489+ // This needs to happen before symlink_metadata to catch cases like "rootlink/"
490+ // where rootlink is a symlink to root.
491+ if uucore:: fs:: path_ends_with_terminator ( file)
492+ && options. recursive
493+ && options. preserve_root
494+ && is_root_path ( file)
495+ {
496+ show_preserve_root_error ( file) ;
497+ had_err = true ;
498+ continue ;
499+ }
500+
485501 had_err = match file. symlink_metadata ( ) {
486502 Ok ( metadata) => {
487503 // Create progress bar on first successful file metadata read
@@ -668,6 +684,40 @@ fn remove_dir_recursive(
668684 }
669685}
670686
687+ /// Check if a path resolves to the root directory.
688+ /// Returns true if the path is root, false otherwise.
689+ fn is_root_path ( path : & Path ) -> bool {
690+ // Check simple case: literal "/" path
691+ if path. has_root ( ) && path. parent ( ) . is_none ( ) {
692+ return true ;
693+ }
694+
695+ // Check if path resolves to "/" after following symlinks
696+ if let Ok ( canonical) = path. canonicalize ( ) {
697+ canonical. has_root ( ) && canonical. parent ( ) . is_none ( )
698+ } else {
699+ false
700+ }
701+ }
702+
703+ /// Show error message for attempting to remove root.
704+ fn show_preserve_root_error ( path : & Path ) {
705+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
706+
707+ if path_looks_like_root {
708+ // Path is literally "/"
709+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
710+ } else {
711+ // Path resolves to root but isn't literally "/" (e.g., symlink to /)
712+ show_error ! (
713+ "{}" ,
714+ translate!( "rm-error-dangerous-recursive-operation-same-as-root" ,
715+ "path" => path. display( ) )
716+ ) ;
717+ }
718+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
719+ }
720+
671721fn handle_dir ( path : & Path , options : & Options , progress_bar : Option < & ProgressBar > ) -> bool {
672722 let mut had_err = false ;
673723
@@ -680,14 +730,13 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
680730 return true ;
681731 }
682732
683- let is_root = path . has_root ( ) && path. parent ( ) . is_none ( ) ;
733+ let is_root = is_root_path ( path) ;
684734 if options. recursive && ( !is_root || !options. preserve_root ) {
685735 had_err = remove_dir_recursive ( path, options, progress_bar) ;
686736 } else if options. dir && ( !is_root || !options. preserve_root ) {
687737 had_err = remove_dir ( path, options, progress_bar) . bitor ( had_err) ;
688738 } else if options. recursive {
689- show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
690- show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
739+ show_preserve_root_error ( path) ;
691740 had_err = true ;
692741 } else {
693742 show_error ! (
@@ -935,14 +984,6 @@ fn prompt_descend(path: &Path) -> bool {
935984 prompt_yes ! ( "descend into directory {}?" , path. quote( ) )
936985}
937986
938- fn normalize ( path : & Path ) -> PathBuf {
939- // copied from https://github.com/rust-lang/cargo/blob/2e4cfc2b7d43328b207879228a2ca7d427d188bb/src/cargo/util/paths.rs#L65-L90
940- // both projects are MIT https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT
941- // for std impl progress see rfc https://github.com/rust-lang/rfcs/issues/2208
942- // TODO: replace this once that lands
943- uucore:: fs:: normalize_path ( path)
944- }
945-
946987#[ cfg( not( windows) ) ]
947988fn is_symlink_dir ( _metadata : & Metadata ) -> bool {
948989 false
0 commit comments