6
6
// spell-checker:ignore (path) eacces inacc rm-r4
7
7
8
8
use clap:: { builder:: ValueParser , crate_version, parser:: ValueSource , Arg , ArgAction , Command } ;
9
- use std:: collections:: VecDeque ;
10
9
use std:: ffi:: { OsStr , OsString } ;
11
10
use std:: fs:: { self , Metadata } ;
12
11
use std:: ops:: BitOr ;
@@ -21,7 +20,6 @@ use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
21
20
use uucore:: {
22
21
format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error,
23
22
} ;
24
- use walkdir:: { DirEntry , WalkDir } ;
25
23
26
24
#[ derive( Eq , PartialEq , Clone , Copy ) ]
27
25
/// Enum, determining when the `rm` will prompt the user about the file deletion
@@ -341,6 +339,24 @@ fn is_dir_empty(path: &Path) -> bool {
341
339
}
342
340
}
343
341
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
+
344
360
/// Whether the given file or directory is writable.
345
361
#[ cfg( unix) ]
346
362
fn is_writable ( path : & Path ) -> bool {
@@ -360,7 +376,106 @@ fn is_writable(_path: &Path) -> bool {
360
376
true
361
377
}
362
378
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
+
364
479
fn handle_dir ( path : & Path , options : & Options ) -> bool {
365
480
let mut had_err = false ;
366
481
@@ -375,71 +490,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
375
490
376
491
let is_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
377
492
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)
443
494
} else if options. dir && ( !is_root || !options. preserve_root ) {
444
495
had_err = remove_dir ( path, options) . bitor ( had_err) ;
445
496
} else if options. recursive {
0 commit comments