Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions src/uu/rm/src/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use clap::{Arg, ArgAction, Command, builder::ValueParser, parser::ValueSource};
use std::ffi::{OsStr, OsString};
use std::fs::{self, Metadata};
use std::io::{IsTerminal, stdin};
use std::ops::BitOr;
#[cfg(not(windows))]
use std::os::unix::ffi::OsStrExt;
Expand Down Expand Up @@ -68,6 +69,25 @@ pub struct Options {
pub dir: bool,
/// `-v`, `--verbose`
pub verbose: bool,
#[doc(hidden)]
/// `---presume-input-tty`
/// Always use `None`; GNU flag for testing use only
pub __presume_input_tty: Option<bool>,
}

impl Default for Options {
fn default() -> Self {
Self {
force: false,
interactive: InteractiveMode::PromptProtected,
one_fs: false,
preserve_root: true,
recursive: false,
dir: false,
verbose: false,
__presume_input_tty: None,
}
}
}

const ABOUT: &str = help_about!("rm.md");
Expand Down Expand Up @@ -145,6 +165,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
recursive: matches.get_flag(OPT_RECURSIVE),
dir: matches.get_flag(OPT_DIR),
verbose: matches.get_flag(OPT_VERBOSE),
__presume_input_tty: if matches.get_flag(PRESUME_INPUT_TTY) {
Some(true)
} else {
None
},
};
if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) {
let msg: String = format!(
Expand Down Expand Up @@ -608,40 +633,45 @@ fn prompt_file(path: &Path, options: &Options) -> bool {
prompt_yes!("remove file {}?", path.quote())
};
}
prompt_file_permission_readonly(path)
prompt_file_permission_readonly(path, options)
}

fn prompt_file_permission_readonly(path: &Path) -> bool {
match fs::metadata(path) {
Ok(_) if is_writable(path) => true,
Ok(metadata) if metadata.len() == 0 => prompt_yes!(
fn prompt_file_permission_readonly(path: &Path, options: &Options) -> 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!(
"remove write-protected regular empty file {}?",
path.quote()
),
_ => prompt_yes!("remove write-protected regular file {}?", path.quote()),
}
}

// For directories finding if they are writable or not is a hassle. In Unix we can use the built-in rust crate to to check mode bits. But other os don't have something similar afaik
// For directories finding if they are writable or not is a hassle. In Unix we can use the built-in rust crate to check mode bits. But other os don't have something similar afaik
// Most cases are covered by keep eye out for edge cases
#[cfg(unix)]
fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool {
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
match (
stdin_ok,
is_readable_metadata(metadata),
is_writable_metadata(metadata),
options.interactive,
) {
(false, false, _) => prompt_yes!(
(false, _, _, InteractiveMode::PromptProtected) => true,
(_, false, false, _) => prompt_yes!(
"attempt removal of inaccessible directory {}?",
path.quote()
),
(false, true, InteractiveMode::Always) => prompt_yes!(
(_, false, true, InteractiveMode::Always) => prompt_yes!(
"attempt removal of inaccessible directory {}?",
path.quote()
),
(true, false, _) => prompt_yes!("remove write-protected directory {}?", path.quote()),
(_, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()),
(_, _, _) => true,
(_, true, false, _) => prompt_yes!("remove write-protected directory {}?", path.quote()),
(_, _, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()),
(_, _, _, _) => true,
}
}

Expand All @@ -666,12 +696,12 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata
use std::os::windows::prelude::MetadataExt;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_READONLY;
let not_user_writable = (metadata.file_attributes() & FILE_ATTRIBUTE_READONLY) != 0;
if not_user_writable {
prompt_yes!("remove write-protected directory {}?", path.quote())
} else if options.interactive == InteractiveMode::Always {
prompt_yes!("remove directory {}?", path.quote())
} else {
true
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
match (stdin_ok, not_user_writable, options.interactive) {
(false, _, InteractiveMode::PromptProtected) => true,
(_, true, _) => prompt_yes!("remove write-protected directory {}?", path.quote()),
(_, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()),
(_, _, _) => true,
}
}

Expand Down
60 changes: 58 additions & 2 deletions tests/by-util/test_rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,50 @@ fn test_rm_prompts() {
assert!(!at.dir_exists("a"));
}

#[cfg(feature = "chmod")]
#[test]
fn test_rm_prompts_no_tty() {
// This test ensures InteractiveMode.PromptProtected proceeds silently with non-interactive stdin

use std::io::Write;

let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.mkdir("a/");

let file_1 = "a/empty";
let file_2 = "a/empty-no-write";
let file_3 = "a/f-no-write";

at.touch(file_1);
at.touch(file_2);
at.make_file(file_3)
.write_all(b"not-empty")
.expect("Couldn't write to a/f-no-write");

at.symlink_dir("a/empty-f", "a/slink");
at.symlink_dir(".", "a/slink-dot");

let dir_1 = "a/b/";
let dir_2 = "a/b-no-write/";

at.mkdir(dir_1);
at.mkdir(dir_2);

scene
.ccmd("chmod")
.arg("u-w")
.arg(file_3)
.arg(dir_2)
.arg(file_2)
.succeeds();

scene.ucmd().arg("-r").arg("a").succeeds().no_output();

assert!(!at.dir_exists("a"));
}

#[test]
fn test_rm_force_prompts_order() {
// Needed for talking with stdin on platforms where CRLF or LF matters
Expand Down Expand Up @@ -646,7 +690,13 @@ fn test_prompt_write_protected_yes() {

scene.ccmd("chmod").arg("0").arg(file_1).succeeds();

scene.ucmd().arg(file_1).pipe_in("y").succeeds();
scene
.ucmd()
.arg("---presume-input-tty")
.arg(file_1)
.pipe_in("y")
.succeeds()
.stderr_contains("rm: remove write-protected regular empty file");
assert!(!at.file_exists(file_1));
}

Expand All @@ -661,7 +711,13 @@ fn test_prompt_write_protected_no() {

scene.ccmd("chmod").arg("0").arg(file_2).succeeds();

scene.ucmd().arg(file_2).pipe_in("n").succeeds();
scene
.ucmd()
.arg("---presume-input-tty")
.arg(file_2)
.pipe_in("n")
.succeeds()
.stderr_contains("rm: remove write-protected regular empty file");
assert!(at.file_exists(file_2));
}

Expand Down
Loading