@@ -431,14 +431,46 @@ fn is_writable(_path: &Path) -> bool {
431431 true
432432}
433433
434+ /// Non-safe recursive directory removal using traditional fs operations
434435#[ cfg( target_os = "linux" ) ]
435- fn safe_remove_dir_recursive ( path : & Path , options : & Options ) -> bool {
436- // Try to open the directory using DirFd for secure traversal
437- let dir_fd = match DirFd :: open ( path) {
438- Ok ( fd) => fd,
439- Err ( e) => {
440- // If we can't open the directory for safe traversal, try removing it as empty directory
441- // This handles the case where it's an empty directory with no read permissions
436+ /// Used as fallback when safe traversal is not possible
437+ fn unsafe_remove_dir_recursive ( path : & Path , options : & Options ) -> bool {
438+ // Base case 1: this is a file or a symbolic link.
439+ if !path. is_dir ( ) || path. is_symlink ( ) {
440+ return remove_file ( path, options) ;
441+ }
442+
443+ // Base case 2: this is a non-empty directory, but the user
444+ // doesn't want to descend into it.
445+ if options. interactive == InteractiveMode :: Always
446+ && !is_dir_empty ( path)
447+ && !prompt_descend ( path)
448+ {
449+ return false ;
450+ }
451+
452+ // Use the traditional non-safe approach (similar to non-Linux implementation)
453+ if let Some ( s) = path. to_str ( ) {
454+ if s. len ( ) > 1000 {
455+ match fs:: remove_dir_all ( path) {
456+ Ok ( _) => return false ,
457+ Err ( e) => {
458+ let e = e. map_err_context (
459+ || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ,
460+ ) ;
461+ show_error ! ( "{e}" ) ;
462+ return true ;
463+ }
464+ }
465+ }
466+ }
467+
468+ // Recursive case: this is a directory.
469+ let mut error = false ;
470+ match fs:: read_dir ( path) {
471+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied => {
472+ // Can't read directory due to permissions. Try to remove it directly.
473+ // If it's empty, this should succeed. If not, we'll get a different error.
442474 match fs:: remove_dir ( path) {
443475 Ok ( _) => {
444476 if options. verbose {
@@ -447,20 +479,97 @@ fn safe_remove_dir_recursive(path: &Path, options: &Options) -> bool {
447479 translate!( "rm-verbose-removed-directory" , "file" => normalize( path) . quote( ) )
448480 ) ;
449481 }
450- return false ;
482+ return false ; // Success
451483 }
452- Err ( _) => {
453- // If we can't remove it as empty dir either, report the original open error
454- show_error ! (
455- "{}" ,
456- e. map_err_context(
457- || translate!( "rm-error-cannot-remove" , "file" => path. quote( ) )
458- )
484+ Err ( _remove_err) => {
485+ // Could not remove the directory. Always show the original permission denied error
486+ // since this indicates a fundamental access issue, even with force flag
487+ let e = e. map_err_context (
488+ || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ,
459489 ) ;
460- return true ;
490+ show_error ! ( "{}" , e) ;
491+ error = true ;
492+ }
493+ }
494+ }
495+ Err ( _) => error = true ,
496+ Ok ( iter) => {
497+ for entry in iter {
498+ match entry {
499+ Err ( _) => error = true ,
500+ Ok ( entry) => {
501+ let child_error = unsafe_remove_dir_recursive ( & entry. path ( ) , options) ;
502+ error = error || child_error;
503+ }
461504 }
462505 }
463506 }
507+ }
508+
509+ // Ask the user whether to remove the current directory.
510+ if options. interactive == InteractiveMode :: Always && !prompt_dir ( path, options) {
511+ return false ;
512+ }
513+
514+ // Try removing the directory itself.
515+ match fs:: remove_dir ( path) {
516+ Err ( _) if !error && path. is_dir ( ) => {
517+ // For unreadable directories, try to provide a meaningful error
518+ if path. read_dir ( ) . is_err ( ) {
519+ show_error ! (
520+ "{}" ,
521+ std:: io:: Error :: from( std:: io:: ErrorKind :: PermissionDenied ) . map_err_context(
522+ || translate!(
523+ "rm-error-cannot-remove-directory" ,
524+ "file" => path. quote( )
525+ )
526+ )
527+ ) ;
528+ }
529+ true
530+ }
531+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: DirectoryNotEmpty => {
532+ // This can happen when we're unable to remove all
533+ // the directory's entries.
534+ if !error {
535+ show_error ! (
536+ "{}" ,
537+ e. map_err_context( || translate!(
538+ "rm-error-cannot-remove-directory" ,
539+ "file" => path. quote( )
540+ ) )
541+ ) ;
542+ }
543+ true
544+ }
545+ Err ( e) => {
546+ let e =
547+ e. map_err_context ( || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ) ;
548+ show_error ! ( "{e}" ) ;
549+ true
550+ }
551+ Ok ( _) => {
552+ if options. verbose {
553+ println ! (
554+ "{}" ,
555+ translate!( "rm-verbose-removed-directory" , "file" => normalize( path) . quote( ) )
556+ ) ;
557+ }
558+ false
559+ }
560+ }
561+ }
562+
563+ #[ cfg( target_os = "linux" ) ]
564+ fn safe_remove_dir_recursive ( path : & Path , options : & Options ) -> bool {
565+ // Try to open the directory using DirFd for secure traversal
566+ let dir_fd = match DirFd :: open ( path) {
567+ Ok ( fd) => fd,
568+ Err ( _e) => {
569+ // If we can't open the directory for safe traversal,
570+ // fall back to the traditional unsafe recursive removal method
571+ return unsafe_remove_dir_recursive ( path, options) ;
572+ }
464573 } ;
465574
466575 let error = safe_remove_dir_recursive_impl ( path, & dir_fd, options) ;
@@ -513,11 +622,15 @@ fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options
513622 return false ;
514623 }
515624 Err ( e) => {
516- show_error ! (
517- "{}" ,
518- e. map_err_context( || translate!( "rm-error-cannot-remove" , "file" => path. quote( ) ) )
519- ) ;
520- return true ;
625+ if !options. force {
626+ show_error ! (
627+ "{}" ,
628+ e. map_err_context(
629+ || translate!( "rm-error-cannot-remove" , "file" => path. quote( ) )
630+ )
631+ ) ;
632+ }
633+ return !options. force ;
521634 }
522635 } ;
523636
@@ -531,11 +644,13 @@ fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options
531644 let entry_stat = match dir_fd. stat_at ( & entry_name, false ) {
532645 Ok ( stat) => stat,
533646 Err ( e) => {
534- let e = e. map_err_context (
535- || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
536- ) ;
537- show_error ! ( "{e}" ) ;
538- error = true ;
647+ if !options. force {
648+ let e = e. map_err_context (
649+ || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
650+ ) ;
651+ show_error ! ( "{e}" ) ;
652+ }
653+ error = !options. force ;
539654 continue ;
540655 }
541656 } ;
@@ -544,9 +659,45 @@ fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options
544659 let is_dir = ( entry_stat. st_mode & libc:: S_IFMT ) == libc:: S_IFDIR ;
545660
546661 if is_dir {
547- // Recursively remove subdirectory - handle in the style of the non-Linux version
548- let child_error = remove_dir_recursive ( & entry_path, options) ;
662+ // Recursively remove subdirectory using safe traversal
663+ let child_dir_fd = match dir_fd. open_subdir ( & entry_name) {
664+ Ok ( fd) => fd,
665+ Err ( _e) => {
666+ // If we can't open the subdirectory for safe traversal,
667+ // fall back to the non-safe recursive removal method
668+ // This preserves original error messages and handles complex permission scenarios
669+ let child_error = unsafe_remove_dir_recursive ( & entry_path, options) ;
670+ error = error || child_error;
671+ continue ;
672+ }
673+ } ;
674+
675+ let child_error = safe_remove_dir_recursive_impl ( & entry_path, & child_dir_fd, options) ;
549676 error = error || child_error;
677+
678+ // Ask user permission if needed for this subdirectory
679+ if !child_error
680+ && options. interactive == InteractiveMode :: Always
681+ && !prompt_dir ( & entry_path, options)
682+ {
683+ continue ;
684+ }
685+
686+ // Remove the now-empty subdirectory using safe unlinkat
687+ if !child_error {
688+ if let Err ( e) = dir_fd. unlink_at ( & entry_name, true ) {
689+ let e = e. map_err_context (
690+ || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
691+ ) ;
692+ show_error ! ( "{e}" ) ;
693+ error = true ;
694+ } else if options. verbose {
695+ println ! (
696+ "{}" ,
697+ translate!( "rm-verbose-removed-directory" , "file" => normalize( & entry_path) . quote( ) )
698+ ) ;
699+ }
700+ }
550701 } else {
551702 // Remove file - check if user wants to remove it first
552703 if prompt_file ( & entry_path, options) {
@@ -727,7 +878,39 @@ fn remove_dir(path: &Path, options: &Options) -> bool {
727878 return true ;
728879 }
729880
730- // Try to remove the directory.
881+ // Use safe traversal on Linux for empty directory removal
882+ #[ cfg( target_os = "linux" ) ]
883+ {
884+ if let Some ( parent) = path. parent ( ) {
885+ if let Some ( dir_name) = path. file_name ( ) {
886+ match DirFd :: open ( parent) {
887+ Ok ( dir_fd) => match dir_fd. unlink_at ( dir_name, true ) {
888+ Ok ( _) => {
889+ if options. verbose {
890+ println ! (
891+ "{}" ,
892+ translate!( "rm-verbose-removed-directory" , "file" => normalize( path) . quote( ) )
893+ ) ;
894+ }
895+ return false ;
896+ }
897+ Err ( e) => {
898+ let e = e. map_err_context (
899+ || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ,
900+ ) ;
901+ show_error ! ( "{e}" ) ;
902+ return true ;
903+ }
904+ } ,
905+ Err ( _) => {
906+ // Fallback to standard method if safe traversal fails
907+ }
908+ }
909+ }
910+ }
911+ }
912+
913+ // Fallback method for non-Linux or when safe traversal is unavailable
731914 match fs:: remove_dir ( path) {
732915 Ok ( _) => {
733916 if options. verbose {
@@ -749,6 +932,48 @@ fn remove_dir(path: &Path, options: &Options) -> bool {
749932
750933fn remove_file ( path : & Path , options : & Options ) -> bool {
751934 if prompt_file ( path, options) {
935+ // Use safe traversal on Linux for individual file removal
936+ #[ cfg( target_os = "linux" ) ]
937+ {
938+ if let Some ( parent) = path. parent ( ) {
939+ if let Some ( file_name) = path. file_name ( ) {
940+ match DirFd :: open ( parent) {
941+ Ok ( dir_fd) => {
942+ match dir_fd. unlink_at ( file_name, false ) {
943+ Ok ( _) => {
944+ if options. verbose {
945+ println ! (
946+ "{}" ,
947+ translate!( "rm-verbose-removed" , "file" => normalize( path) . quote( ) )
948+ ) ;
949+ }
950+ return false ;
951+ }
952+ Err ( e) => {
953+ if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
954+ // GNU compatibility (rm/fail-eacces.sh)
955+ show_error ! (
956+ "{}" ,
957+ RmError :: CannotRemovePermissionDenied (
958+ path. as_os_str( ) . to_os_string( )
959+ )
960+ ) ;
961+ } else {
962+ show_error ! ( "cannot remove {}: {e}" , path. quote( ) ) ;
963+ }
964+ return true ;
965+ }
966+ }
967+ }
968+ Err ( _) => {
969+ // Fallback to standard method if safe traversal fails
970+ }
971+ }
972+ }
973+ }
974+ }
975+
976+ // Fallback method for non-Linux or when safe traversal is unavailable
752977 match fs:: remove_file ( path) {
753978 Ok ( _) => {
754979 if options. verbose {
@@ -859,6 +1084,7 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata
8591084 options. interactive ,
8601085 ) {
8611086 ( false , _, _, InteractiveMode :: PromptProtected ) => true ,
1087+ ( false , false , false , InteractiveMode :: Never ) => true , // Don't prompt when interactive is never
8621088 ( _, false , false , _) => prompt_yes ! (
8631089 "attempt removal of inaccessible directory {}?" ,
8641090 path. quote( )
0 commit comments