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 ;
1312#[ cfg( not( windows) ) ]
1413use std:: os:: unix:: ffi:: OsStrExt ;
14+ #[ cfg( unix) ]
15+ use std:: os:: unix:: fs:: PermissionsExt ;
1516use std:: path:: MAIN_SEPARATOR ;
1617use std:: path:: { Path , PathBuf } ;
1718use uucore:: display:: Quotable ;
1819use uucore:: error:: { FromIo , UResult , USimpleError , UUsageError } ;
1920use uucore:: {
2021 format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error,
2122} ;
22- use walkdir:: { DirEntry , WalkDir } ;
2323
2424#[ derive( Eq , PartialEq , Clone , Copy ) ]
2525/// Enum, determining when the `rm` will prompt the user about the file deletion
@@ -328,7 +328,154 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
328328 had_err
329329}
330330
331- #[ allow( clippy:: cognitive_complexity) ]
331+ /// Whether the given directory is empty.
332+ ///
333+ /// `path` must be a directory. If there is an error reading the
334+ /// contents of the directory, this returns `false`.
335+ fn is_dir_empty ( path : & Path ) -> bool {
336+ match std:: fs:: read_dir ( path) {
337+ Err ( _) => false ,
338+ Ok ( iter) => iter. count ( ) == 0 ,
339+ }
340+ }
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+
360+ /// Whether the given file or directory is writable.
361+ #[ cfg( unix) ]
362+ fn is_writable ( path : & Path ) -> bool {
363+ match std:: fs:: metadata ( path) {
364+ Err ( _) => false ,
365+ Ok ( metadata) => {
366+ let mode = metadata. permissions ( ) . mode ( ) ;
367+ ( mode & 0o200 ) > 0
368+ }
369+ }
370+ }
371+
372+ /// Whether the given file or directory is writable.
373+ #[ cfg( not( unix) ) ]
374+ fn is_writable ( _path : & Path ) -> bool {
375+ // TODO Not yet implemented.
376+ true
377+ }
378+
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+
332479fn handle_dir ( path : & Path , options : & Options ) -> bool {
333480 let mut had_err = false ;
334481
@@ -343,71 +490,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
343490
344491 let is_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
345492 if options. recursive && ( !is_root || !options. preserve_root ) {
346- if options. interactive != InteractiveMode :: Always && !options. verbose {
347- if let Err ( e) = fs:: remove_dir_all ( path) {
348- // GNU compatibility (rm/empty-inacc.sh)
349- // remove_dir_all failed. maybe it is because of the permissions
350- // but if the directory is empty, remove_dir might work.
351- // So, let's try that before failing for real
352- if fs:: remove_dir ( path) . is_err ( ) {
353- had_err = true ;
354- if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
355- // GNU compatibility (rm/fail-eacces.sh)
356- // here, GNU doesn't use some kind of remove_dir_all
357- // It will show directory+file
358- show_error ! ( "cannot remove {}: {}" , path. quote( ) , "Permission denied" ) ;
359- } else {
360- show_error ! ( "cannot remove {}: {}" , path. quote( ) , e) ;
361- }
362- }
363- }
364- } else {
365- let mut dirs: VecDeque < DirEntry > = VecDeque :: new ( ) ;
366- // The Paths to not descend into. We need to this because WalkDir doesn't have a way, afaik, to not descend into a directory
367- // So we have to just ignore paths as they come up if they start with a path we aren't descending into
368- let mut not_descended: Vec < PathBuf > = Vec :: new ( ) ;
369-
370- ' outer: for entry in WalkDir :: new ( path) {
371- match entry {
372- Ok ( entry) => {
373- if options. interactive == InteractiveMode :: Always {
374- for not_descend in & not_descended {
375- if entry. path ( ) . starts_with ( not_descend) {
376- // 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
377- continue ' outer;
378- }
379- }
380- }
381- let file_type = entry. file_type ( ) ;
382- if file_type. is_dir ( ) {
383- // 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
384- if options. interactive == InteractiveMode :: Always
385- && fs:: read_dir ( entry. path ( ) ) . unwrap ( ) . count ( ) != 0
386- {
387- // If we don't descend we push this directory onto our not_descended vector else we push this directory onto dirs vector
388- if prompt_descend ( entry. path ( ) ) {
389- dirs. push_back ( entry) ;
390- } else {
391- not_descended. push ( entry. path ( ) . to_path_buf ( ) ) ;
392- }
393- } else {
394- dirs. push_back ( entry) ;
395- }
396- } else {
397- had_err = remove_file ( entry. path ( ) , options) . bitor ( had_err) ;
398- }
399- }
400- Err ( e) => {
401- had_err = true ;
402- show_error ! ( "recursing in {}: {}" , path. quote( ) , e) ;
403- }
404- }
405- }
406-
407- for dir in dirs. iter ( ) . rev ( ) {
408- had_err = remove_dir ( dir. path ( ) , options) . bitor ( had_err) ;
409- }
410- }
493+ had_err = remove_dir_recursive ( path, options)
411494 } else if options. dir && ( !is_root || !options. preserve_root ) {
412495 had_err = remove_dir ( path, options) . bitor ( had_err) ;
413496 } else if options. recursive {
@@ -515,7 +598,7 @@ fn prompt_file(path: &Path, options: &Options) -> bool {
515598 return true ;
516599 } ;
517600
518- if options. interactive == InteractiveMode :: Always && !metadata . permissions ( ) . readonly ( ) {
601+ if options. interactive == InteractiveMode :: Always && is_writable ( path ) {
519602 return if metadata. len ( ) == 0 {
520603 prompt_yes ! ( "remove regular empty file {}?" , path. quote( ) )
521604 } else {
@@ -527,7 +610,7 @@ fn prompt_file(path: &Path, options: &Options) -> bool {
527610
528611fn prompt_file_permission_readonly ( path : & Path ) -> bool {
529612 match fs:: metadata ( path) {
530- Ok ( metadata ) if !metadata . permissions ( ) . readonly ( ) => true ,
613+ Ok ( _ ) if is_writable ( path ) => true ,
531614 Ok ( metadata) if metadata. len ( ) == 0 => prompt_yes ! (
532615 "remove write-protected regular empty file {}?" ,
533616 path. quote( )
0 commit comments