Skip to content

Commit 5584b24

Browse files
committed
progress
1 parent 2ce39de commit 5584b24

File tree

1 file changed

+255
-29
lines changed

1 file changed

+255
-29
lines changed

src/uu/rm/src/rm.rs

Lines changed: 255 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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

750933
fn 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

Comments
 (0)