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
6+ // spell-checker:ignore (path) eacces inacc rm-r4
77
88use clap:: { builder:: ValueParser , crate_version, parser:: ValueSource , Arg , ArgAction , Command } ;
99use std:: collections:: VecDeque ;
1010use std:: ffi:: { OsStr , OsString } ;
1111use std:: fs:: { self , File , Metadata } ;
1212use std:: io:: ErrorKind ;
1313use std:: ops:: BitOr ;
14+ #[ cfg( not( windows) ) ]
15+ use std:: os:: unix:: ffi:: OsStrExt ;
16+ use std:: path:: MAIN_SEPARATOR ;
1417use std:: path:: { Path , PathBuf } ;
1518use uucore:: display:: Quotable ;
1619use uucore:: error:: { UResult , USimpleError , UUsageError } ;
17- use uucore:: { format_usage, help_about, help_section, help_usage, prompt_yes, show_error} ;
20+ use uucore:: {
21+ format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error,
22+ } ;
1823use walkdir:: { DirEntry , WalkDir } ;
1924
2025#[ derive( Eq , PartialEq , Clone , Copy ) ]
@@ -290,6 +295,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
290295
291296 for filename in files {
292297 let file = Path :: new ( filename) ;
298+
293299 had_err = match file. symlink_metadata ( ) {
294300 Ok ( metadata) => {
295301 if metadata. is_dir ( ) {
@@ -300,6 +306,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
300306 remove_file ( file, options)
301307 }
302308 }
309+
303310 Err ( _e) => {
304311 // TODO: actually print out the specific error
305312 // TODO: When the error is not about missing files
@@ -326,6 +333,15 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
326333fn handle_dir ( path : & Path , options : & Options ) -> bool {
327334 let mut had_err = false ;
328335
336+ let path = clean_trailing_slashes ( path) ;
337+ if path_is_current_or_parent_directory ( path) {
338+ show_error ! (
339+ "refusing to remove '.' or '..' directory: skipping '{}'" ,
340+ path. display( )
341+ ) ;
342+ return true ;
343+ }
344+
329345 let is_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
330346 if options. recursive && ( !is_root || !options. preserve_root ) {
331347 if options. interactive != InteractiveMode :: Always && !options. verbose {
@@ -396,7 +412,11 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
396412 } else if options. dir && ( !is_root || !options. preserve_root ) {
397413 had_err = remove_dir ( path, options) . bitor ( had_err) ;
398414 } else if options. recursive {
399- show_error ! ( "could not remove directory {}" , path. quote( ) ) ;
415+ show_error ! (
416+ "it is dangerous to operate recursively on '{}'" ,
417+ MAIN_SEPARATOR
418+ ) ;
419+ show_error ! ( "use --no-preserve-root to override this failsafe" ) ;
400420 had_err = true ;
401421 } else {
402422 show_error ! (
@@ -559,6 +579,20 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata
559579 true
560580 }
561581}
582+ /// Checks if the path is referring to current or parent directory , if it is referring to current or any parent directory in the file tree e.g '/../..' , '../..'
583+ fn path_is_current_or_parent_directory ( path : & Path ) -> bool {
584+ let path_str = os_str_as_bytes ( path. as_os_str ( ) ) ;
585+ let dir_separator = MAIN_SEPARATOR as u8 ;
586+ if let Ok ( path_bytes) = path_str {
587+ return path_bytes == ( [ b'.' ] )
588+ || path_bytes == ( [ b'.' , b'.' ] )
589+ || path_bytes. ends_with ( & [ dir_separator, b'.' ] )
590+ || path_bytes. ends_with ( & [ dir_separator, b'.' , b'.' ] )
591+ || path_bytes. ends_with ( & [ dir_separator, b'.' , dir_separator] )
592+ || path_bytes. ends_with ( & [ dir_separator, b'.' , b'.' , dir_separator] ) ;
593+ }
594+ false
595+ }
562596
563597// For windows we can use windows metadata trait and file attributes to see if a directory is readonly
564598#[ cfg( windows) ]
@@ -586,6 +620,40 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata
586620 }
587621}
588622
623+ /// Removes trailing slashes, for example 'd/../////' yield 'd/../' required to fix rm-r4 GNU test
624+ fn clean_trailing_slashes ( path : & Path ) -> & Path {
625+ let path_str = os_str_as_bytes ( path. as_os_str ( ) ) ;
626+ let dir_separator = MAIN_SEPARATOR as u8 ;
627+
628+ if let Ok ( path_bytes) = path_str {
629+ let mut idx = if path_bytes. len ( ) > 1 {
630+ path_bytes. len ( ) - 1
631+ } else {
632+ return path;
633+ } ;
634+ // Checks if element at the end is a '/'
635+ if path_bytes[ idx] == dir_separator {
636+ for i in ( 1 ..path_bytes. len ( ) ) . rev ( ) {
637+ // Will break at the start of the continuous sequence of '/', eg: "abc//////" , will break at
638+ // "abc/", this will clean ////// to the root '/', so we have to be careful to not
639+ // delete the root.
640+ if path_bytes[ i - 1 ] != dir_separator {
641+ idx = i;
642+ break ;
643+ }
644+ }
645+ #[ cfg( unix) ]
646+ return Path :: new ( OsStr :: from_bytes ( & path_bytes[ 0 ..=idx] ) ) ;
647+
648+ #[ cfg( not( unix) ) ]
649+ // Unwrapping is fine here as os_str_as_bytes() would return an error on non unix
650+ // systems with non utf-8 characters and thus bypass the if let Ok branch
651+ return Path :: new ( std:: str:: from_utf8 ( & path_bytes[ 0 ..=idx] ) . unwrap ( ) ) ;
652+ }
653+ }
654+ path
655+ }
656+
589657fn prompt_descend ( path : & Path ) -> bool {
590658 prompt_yes ! ( "descend into directory {}?" , path. quote( ) )
591659}
@@ -611,3 +679,17 @@ fn is_symlink_dir(metadata: &Metadata) -> bool {
611679 metadata. file_type ( ) . is_symlink ( )
612680 && ( ( metadata. file_attributes ( ) & FILE_ATTRIBUTE_DIRECTORY ) != 0 )
613681}
682+
683+ mod tests {
684+
685+ #[ test]
686+ // Testing whether path the `/////` collapses to `/`
687+ fn test_collapsible_slash_path ( ) {
688+ use std:: path:: Path ;
689+
690+ use crate :: clean_trailing_slashes;
691+ let path = Path :: new ( "/////" ) ;
692+
693+ assert_eq ! ( Path :: new( "/" ) , clean_trailing_slashes( path) ) ;
694+ }
695+ }
0 commit comments