66// spell-checker:ignore (path) eacces inacc rm-r4
77
88use clap:: { builder:: ValueParser , crate_version, parser:: ValueSource , Arg , ArgAction , Command } ;
9- use std:: collections:: VecDeque ;
109use std:: ffi:: { OsStr , OsString } ;
1110use std:: fs:: { self , Metadata } ;
1211use std:: ops:: BitOr ;
@@ -21,7 +20,6 @@ use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
2120use uucore:: {
2221 format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error,
2322} ;
24- use walkdir:: { DirEntry , WalkDir } ;
2523
2624#[ derive( Eq , PartialEq , Clone , Copy ) ]
2725/// Enum, determining when the `rm` will prompt the user about the file deletion
@@ -341,6 +339,24 @@ fn is_dir_empty(path: &Path) -> bool {
341339 }
342340}
343341
342+ /// Whether the given file or directory is readable.
343+ #[ cfg( unix) ]
344+ fn is_readable ( path : & Path ) -> bool {
345+ match std:: fs:: metadata ( path) {
346+ Err ( _) => false ,
347+ Ok ( metadata) => {
348+ let mode = metadata. permissions ( ) . mode ( ) ;
349+ ( mode & 0o400 ) > 0
350+ }
351+ }
352+ }
353+
354+ /// Whether the given file or directory is readable.
355+ #[ cfg( not( unix) ) ]
356+ fn is_readable ( _path : & Path ) -> bool {
357+ true
358+ }
359+
344360/// Whether the given file or directory is writable.
345361#[ cfg( unix) ]
346362fn is_writable ( path : & Path ) -> bool {
@@ -360,7 +376,106 @@ fn is_writable(_path: &Path) -> bool {
360376 true
361377}
362378
363- #[ allow( clippy:: cognitive_complexity) ]
379+ /// Recursively remove the directory tree rooted at the given path.
380+ ///
381+ /// If `path` is a file or a symbolic link, just remove it. If it is a
382+ /// directory, remove all of its entries recursively and then remove the
383+ /// directory itself. In case of an error, print the error message to
384+ /// `stderr` and return `true`. If there were no errors, return `false`.
385+ fn remove_dir_recursive ( path : & Path , options : & Options ) -> bool {
386+ // Special case: if we cannot access the metadata because the
387+ // filename is too long, fall back to try
388+ // `std::fs::remove_dir_all()`.
389+ //
390+ // TODO This is a temporary bandage; we shouldn't need to do this
391+ // at all. Instead of using the full path like "x/y/z", which
392+ // causes a `InvalidFilename` error when trying to access the file
393+ // metadata, we should be able to use just the last part of the
394+ // path, "z", and know that it is relative to the parent, "x/y".
395+ if let Some ( s) = path. to_str ( ) {
396+ if s. len ( ) > 1000 {
397+ match std:: fs:: remove_dir_all ( path) {
398+ Ok ( _) => return false ,
399+ Err ( e) => {
400+ let e = e. map_err_context ( || format ! ( "cannot remove {}" , path. quote( ) ) ) ;
401+ show_error ! ( "{e}" ) ;
402+ return true ;
403+ }
404+ }
405+ }
406+ }
407+
408+ // Base case 1: this is a file or a symbolic link.
409+ //
410+ // The symbolic link case is important because it could be a link to
411+ // a directory and we don't want to recurse. In particular, this
412+ // avoids an infinite recursion in the case of a link to the current
413+ // directory, like `ln -s . link`.
414+ if !path. is_dir ( ) || path. is_symlink ( ) {
415+ return remove_file ( path, options) ;
416+ }
417+
418+ // Base case 2: this is a non-empty directory, but the user
419+ // doesn't want to descend into it.
420+ if options. interactive == InteractiveMode :: Always
421+ && !is_dir_empty ( path)
422+ && !prompt_descend ( path)
423+ {
424+ return false ;
425+ }
426+
427+ // Recursive case: this is a directory.
428+ let mut error = false ;
429+ match std:: fs:: read_dir ( path) {
430+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied => {
431+ // This is not considered an error.
432+ }
433+ Err ( _) => error = true ,
434+ Ok ( iter) => {
435+ for entry in iter {
436+ match entry {
437+ Err ( _) => error = true ,
438+ Ok ( entry) => {
439+ let child_error = remove_dir_recursive ( & entry. path ( ) , options) ;
440+ error = error || child_error;
441+ }
442+ }
443+ }
444+ }
445+ }
446+
447+ // Ask the user whether to remove the current directory.
448+ if options. interactive == InteractiveMode :: Always && !prompt_dir ( path, options) {
449+ return false ;
450+ }
451+
452+ // Try removing the directory itself.
453+ match std:: fs:: remove_dir ( path) {
454+ Err ( _) if !error && !is_readable ( path) => {
455+ // For compatibility with GNU test case
456+ // `tests/rm/unread2.sh`, show "Permission denied" in this
457+ // case instead of "Directory not empty".
458+ show_error ! ( "cannot remove {}: Permission denied" , path. quote( ) ) ;
459+ error = true ;
460+ }
461+ Err ( e) if !error => {
462+ let e = e. map_err_context ( || format ! ( "cannot remove {}" , path. quote( ) ) ) ;
463+ show_error ! ( "{e}" ) ;
464+ error = true ;
465+ }
466+ Err ( _) => {
467+ // If there has already been at least one error when
468+ // trying to remove the children, then there is no need to
469+ // show another error message as we return from each level
470+ // of the recursion.
471+ }
472+ Ok ( _) if options. verbose => println ! ( "removed directory {}" , normalize( path) . quote( ) ) ,
473+ Ok ( _) => { }
474+ }
475+
476+ error
477+ }
478+
364479fn handle_dir ( path : & Path , options : & Options ) -> bool {
365480 let mut had_err = false ;
366481
@@ -375,71 +490,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
375490
376491 let is_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
377492 if options. recursive && ( !is_root || !options. preserve_root ) {
378- if options. interactive != InteractiveMode :: Always && !options. verbose {
379- if let Err ( e) = fs:: remove_dir_all ( path) {
380- // GNU compatibility (rm/empty-inacc.sh)
381- // remove_dir_all failed. maybe it is because of the permissions
382- // but if the directory is empty, remove_dir might work.
383- // So, let's try that before failing for real
384- if fs:: remove_dir ( path) . is_err ( ) {
385- had_err = true ;
386- if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
387- // GNU compatibility (rm/fail-eacces.sh)
388- // here, GNU doesn't use some kind of remove_dir_all
389- // It will show directory+file
390- show_error ! ( "cannot remove {}: {}" , path. quote( ) , "Permission denied" ) ;
391- } else {
392- show_error ! ( "cannot remove {}: {}" , path. quote( ) , e) ;
393- }
394- }
395- }
396- } else {
397- let mut dirs: VecDeque < DirEntry > = VecDeque :: new ( ) ;
398- // The Paths to not descend into. We need to this because WalkDir doesn't have a way, afaik, to not descend into a directory
399- // So we have to just ignore paths as they come up if they start with a path we aren't descending into
400- let mut not_descended: Vec < PathBuf > = Vec :: new ( ) ;
401-
402- ' outer: for entry in WalkDir :: new ( path) {
403- match entry {
404- Ok ( entry) => {
405- if options. interactive == InteractiveMode :: Always {
406- for not_descend in & not_descended {
407- if entry. path ( ) . starts_with ( not_descend) {
408- // We don't need to continue the rest of code in this loop if we are in a directory we don't want to descend into
409- continue ' outer;
410- }
411- }
412- }
413- let file_type = entry. file_type ( ) ;
414- if file_type. is_dir ( ) {
415- // If we are in Interactive Mode Always and the directory isn't empty we ask if we should descend else we push this directory onto dirs vector
416- if options. interactive == InteractiveMode :: Always
417- && !is_dir_empty ( entry. path ( ) )
418- {
419- // If we don't descend we push this directory onto our not_descended vector else we push this directory onto dirs vector
420- if prompt_descend ( entry. path ( ) ) {
421- dirs. push_back ( entry) ;
422- } else {
423- not_descended. push ( entry. path ( ) . to_path_buf ( ) ) ;
424- }
425- } else {
426- dirs. push_back ( entry) ;
427- }
428- } else {
429- had_err = remove_file ( entry. path ( ) , options) . bitor ( had_err) ;
430- }
431- }
432- Err ( e) => {
433- had_err = true ;
434- show_error ! ( "recursing in {}: {}" , path. quote( ) , e) ;
435- }
436- }
437- }
438-
439- for dir in dirs. iter ( ) . rev ( ) {
440- had_err = remove_dir ( dir. path ( ) , options) . bitor ( had_err) ;
441- }
442- }
493+ had_err = remove_dir_recursive ( path, options)
443494 } else if options. dir && ( !is_root || !options. preserve_root ) {
444495 had_err = remove_dir ( path, options) . bitor ( had_err) ;
445496 } else if options. recursive {
0 commit comments