rm: don't treat symlinks as write-protected

GNU rm does not check for write-protection on symbolic links; it
instead prompts to "remove symbolic link" regardless of the link's
permissions or its target's status.

This change:
- Ensures `prompt_file` checks for symlinks specifically using
  `symlink_metadata`, avoiding the incorrect "write-protected" prompt.
- Refactors permission checks into `is_writable_metadata` to allow
  using the already-fetched metadata, which also optimizes performance
  by reducing redundant `stat` calls.
- Updates `prompt_file_permission_readonly` to operate on metadata directly.
This commit is contained in:
Gonçalo Gomes
2026-01-14 10:58:10 +00:00
committed by Sylvestre Ledru
parent f4ed162cf3
commit fc17efe7be
2 changed files with 32 additions and 28 deletions
+15 -28
View File
@@ -549,19 +549,8 @@ fn is_writable_metadata(metadata: &Metadata) -> bool {
(mode & 0o200) > 0
}
/// Whether the given file or directory is writable.
#[cfg(unix)]
fn is_writable(path: &Path) -> bool {
match fs::metadata(path) {
Err(_) => false,
Ok(metadata) => is_writable_metadata(&metadata),
}
}
/// Whether the given file or directory is writable.
#[cfg(not(unix))]
fn is_writable(_path: &Path) -> bool {
// TODO Not yet implemented.
fn is_writable_metadata(_metadata: &Metadata) -> bool {
true
}
@@ -799,35 +788,33 @@ fn prompt_file(path: &Path, options: &Options) -> bool {
if options.interactive == InteractiveMode::Never {
return true;
}
// If interactive is Always we want to check if the file is symlink to prompt the right message
if options.interactive == InteractiveMode::Always {
if let Ok(metadata) = fs::symlink_metadata(path) {
if metadata.is_symlink() {
return prompt_yes!("remove symbolic link {}?", path.quote());
}
}
}
let Ok(metadata) = fs::metadata(path) else {
let Ok(metadata) = fs::symlink_metadata(path) else {
return true;
};
if options.interactive == InteractiveMode::Always && is_writable(path) {
if metadata.is_symlink() {
return options.interactive != InteractiveMode::Always
|| prompt_yes!("remove symbolic link {}?", path.quote());
}
if options.interactive == InteractiveMode::Always && is_writable_metadata(&metadata) {
return if metadata.len() == 0 {
prompt_yes!("remove regular empty file {}?", path.quote())
} else {
prompt_yes!("remove file {}?", path.quote())
};
}
prompt_file_permission_readonly(path, options)
prompt_file_permission_readonly(path, options, &metadata)
}
fn prompt_file_permission_readonly(path: &Path, options: &Options) -> bool {
fn prompt_file_permission_readonly(path: &Path, options: &Options, metadata: &Metadata) -> bool {
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
match (stdin_ok, fs::metadata(path), options.interactive) {
(false, _, InteractiveMode::PromptProtected) => true,
(_, Ok(_), _) if is_writable(path) => true,
(_, Ok(metadata), _) if metadata.len() == 0 => prompt_yes!(
match (stdin_ok, options.interactive) {
(false, InteractiveMode::PromptProtected) => true,
_ if is_writable_metadata(metadata) => true,
_ if metadata.len() == 0 => prompt_yes!(
"remove write-protected regular empty file {}?",
path.quote()
),
+17
View File
@@ -1217,3 +1217,20 @@ fn test_progress_no_output_on_error() {
.stderr_contains("cannot remove")
.stderr_contains("No such file or directory");
}
#[cfg(unix)]
#[test]
fn test_symlink_to_readonly_no_prompt() {
let (at, mut ucmd) = at_and_ucmd!();
at.touch("foo");
at.set_mode("foo", 0o444);
at.symlink_file("foo", "bar");
ucmd.arg("---presume-input-tty")
.arg("bar")
.succeeds()
.no_stderr();
assert!(!at.symlink_exists("bar"));
}