Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rm: rm3 now passes #4013

Merged
merged 35 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6245029
rm: rm3 now passes
palaster Oct 8, 2022
30adc8e
Added windows version of handle_writable_directory
palaster Oct 8, 2022
1f50df2
Fixed spelling error
palaster Oct 8, 2022
6856ce0
Fixed handle_writable_directory on windows
palaster Oct 8, 2022
50d2948
Fixed rm --force argument not forcing prompt to not show up
palaster Oct 9, 2022
3c39a57
Check force position in rm
palaster Oct 10, 2022
70bf4f3
Added test to check rm force prompts order
palaster Oct 10, 2022
0507270
Clippy is going to be the death of me
palaster Oct 10, 2022
3a10984
Small fix
palaster Oct 10, 2022
3551369
Fixed force rm
palaster Oct 11, 2022
e11dd50
Added comments
palaster Oct 12, 2022
a3f35a7
Add override for prompting
palaster Oct 12, 2022
fddc8c9
More readable unix write permissions for directory using libc
palaster Oct 13, 2022
e89d9d0
Forgot .lock
palaster Oct 13, 2022
21b066a
Why is S_IWUSR showing up as a u16 on macos
palaster Oct 13, 2022
7e62945
Merge branch 'main' into rm-correct-prompts
palaster Oct 15, 2022
4306521
Fixed merge conflicts
palaster Oct 15, 2022
1c507c6
Updated to clap4
palaster Oct 15, 2022
b3b90e4
Forgot fmt again
palaster Oct 15, 2022
b612ce5
Cleaner force_prompt_never
palaster Oct 16, 2022
03578a7
Fixed invert issue
palaster Oct 16, 2022
2e61580
Cleaner force_prompt_never
palaster Oct 17, 2022
87df47d
Merge branch 'main' into rm-correct-prompts
palaster Oct 17, 2022
79895a7
Merge branch 'main' into rm-correct-prompts
palaster Oct 18, 2022
00af775
Merge branch 'main' into rm-correct-prompts
palaster Oct 20, 2022
5968f53
Fixed merge conflict
palaster Oct 20, 2022
fd13ced
Merge branch 'main' into rm-correct-prompts
sylvestre Oct 22, 2022
99942b4
Merge branch 'main' into rm-correct-prompts
palaster Oct 22, 2022
744481c
Updated handle_writable_directory comment
palaster Oct 22, 2022
9069d48
Merge branch 'main' into rm-correct-prompts
palaster Oct 24, 2022
0079f73
Merge branch 'main' into rm-correct-prompts
palaster Oct 26, 2022
c4417bf
Merge branch 'main' into rm-correct-prompts
palaster Oct 27, 2022
c7af9a6
Bump libc version from "0.2.135" to "0.2.136"
palaster Oct 27, 2022
4b4222f
Bump libc version from "0.2.136" to "0.2.137"
palaster Oct 29, 2022
7049c64
Merge branch 'main' into rm-correct-prompts
palaster Oct 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/uu/rm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ walkdir = "2.2"
remove_dir_all = "0.7.0"
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["fs"] }

[target.'cfg(unix)'.dependencies]
libc = "0.2.137"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_Storage_FileSystem"] }

Expand Down
207 changes: 152 additions & 55 deletions src/uu/rm/src/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@
#[macro_use]
extern crate uucore;

use clap::{crate_version, Arg, ArgAction, Command};
use clap::{crate_version, parser::ValueSource, Arg, ArgAction, Command};
use remove_dir_all::remove_dir_all;
use std::collections::VecDeque;
use std::fs;
use std::fs::File;
use std::io::ErrorKind;
use std::io::{stderr, stdin, BufRead, Write};
use std::fs::{self, File, Metadata};
use std::io::{stderr, stdin, BufRead, ErrorKind, Write};
use std::ops::BitOr;
use std::path::{Path, PathBuf};
use uucore::display::Quotable;
Expand Down Expand Up @@ -87,17 +85,30 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();

let force = matches.get_flag(OPT_FORCE);
let force_flag = matches.get_flag(OPT_FORCE);

// If -f(--force) is before any -i (or variants) we want prompts else no prompts
let force_prompt_never: bool = force_flag && {
let force_index = matches.index_of(OPT_FORCE).unwrap_or(0);
![OPT_PROMPT, OPT_PROMPT_MORE, OPT_INTERACTIVE]
.iter()
.any(|flag| {
matches.value_source(flag) == Some(ValueSource::CommandLine)
&& matches.index_of(flag).unwrap_or(0) > force_index
})
};

if files.is_empty() && !force {
if files.is_empty() && !force_flag {
// Still check by hand and not use clap
// Because "rm -f" is a thing
return Err(UUsageError::new(1, "missing operand"));
} else {
let options = Options {
force,
force: force_flag,
interactive: {
if matches.get_flag(OPT_PROMPT) {
if force_prompt_never {
InteractiveMode::Never
} else if matches.get_flag(OPT_PROMPT) {
InteractiveMode::Always
} else if matches.get_flag(OPT_PROMPT_MORE) {
InteractiveMode::Once
Expand Down Expand Up @@ -159,20 +170,26 @@ pub fn uu_app() -> Command {
Arg::new(OPT_PROMPT)
.short('i')
.help("prompt before every removal")
.overrides_with_all(&[OPT_PROMPT_MORE, OPT_INTERACTIVE])
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_PROMPT_MORE)
.short('I')
.help("prompt once before removing more than three files, or when removing recursively. \
Less intrusive than -i, while still giving some protection against most mistakes")
.overrides_with_all(&[OPT_PROMPT, OPT_INTERACTIVE])
.action(ArgAction::SetTrue),
)
.arg(Arg::new(OPT_PROMPT_MORE).short('I').help(
"prompt once before removing more than three files, or when removing recursively. \
Less intrusive than -i, while still giving some protection against most mistakes",
).action(ArgAction::SetTrue))
.arg(
Arg::new(OPT_INTERACTIVE)
.long(OPT_INTERACTIVE)
.help(
"prompt according to WHEN: never, once (-I), or always (-i). Without WHEN, \
prompts always",
)
.value_name("WHEN"),
.value_name("WHEN")
.overrides_with_all(&[OPT_PROMPT, OPT_PROMPT_MORE]),
)
.arg(
Arg::new(OPT_ONE_FILE_SYSTEM)
Expand Down Expand Up @@ -361,12 +378,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
}

fn remove_dir(path: &Path, options: &Options) -> bool {
let response = if options.interactive == InteractiveMode::Always {
prompt_file(path, true)
} else {
true
};
if response {
if prompt_file(path, options, true) {
if let Ok(mut read_dir) = fs::read_dir(path) {
if options.dir || options.recursive {
if read_dir.next().is_none() {
Expand Down Expand Up @@ -411,12 +423,7 @@ fn remove_dir(path: &Path, options: &Options) -> bool {
}

fn remove_file(path: &Path, options: &Options) -> bool {
let response = if options.interactive == InteractiveMode::Always {
prompt_file(path, false)
} else {
true
};
if response && prompt_write_protected(path, false, options) {
if prompt_file(path, options, false) {
match fs::remove_file(path) {
Ok(_) => {
if options.verbose {
Expand All @@ -438,46 +445,138 @@ fn remove_file(path: &Path, options: &Options) -> bool {
false
}

fn prompt_write_protected(path: &Path, is_dir: bool, options: &Options) -> bool {
fn prompt_file(path: &Path, options: &Options, is_dir: bool) -> bool {
// If interactive is Never we never want to send prompts
if options.interactive == InteractiveMode::Never {
return true;
}
match File::open(path) {
Ok(_) => true,
Err(err) => {
if err.kind() == ErrorKind::PermissionDenied {
if is_dir {
prompt(&(format!("rm: remove write-protected directory {}? ", path.quote())))
// 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(&(format!("remove symbolic link {}? ", path.quote())));
tertsdiepraam marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
if is_dir {
// We can't use metadata.permissions.readonly for directories because it only works on files
// So we have to handle wether a directory is writable on not manually
if let Ok(metadata) = fs::metadata(path) {
handle_writable_directory(path, options, &metadata)
} else {
true
}
} else {
// File::open(path) doesn't open the file in write mode so we need to use file options to open it in also write mode to check if it can written too
match File::options().read(true).write(true).open(path) {
Ok(file) => {
if let Ok(metadata) = file.metadata() {
if metadata.permissions().readonly() {
if metadata.len() == 0 {
prompt(
&(format!(
"remove write-protected regular empty file {}? ",
path.quote()
)),
)
} else {
prompt(
&(format!(
"remove write-protected regular file {}? ",
path.quote()
)),
)
}
} else if options.interactive == InteractiveMode::Always {
if metadata.len() == 0 {
prompt(&(format!("remove regular empty file {}? ", path.quote())))
} else {
prompt(&(format!("remove file {}? ", path.quote())))
}
} else {
true
}
} else {
if fs::metadata(path).unwrap().len() == 0 {
return prompt(
&(format!(
"rm: remove write-protected regular empty file {}? ",
path.quote()
)),
);
true
}
}
Err(err) => {
if err.kind() == ErrorKind::PermissionDenied {
if let Ok(metadata) = fs::metadata(path) {
if metadata.len() == 0 {
prompt(
&(format!(
"remove write-protected regular empty file {}? ",
path.quote()
)),
)
} else {
prompt(
&(format!(
"remove write-protected regular file {}? ",
path.quote()
)),
)
}
} else {
prompt(&(format!("remove write-protected regular file {}? ", path.quote())))
}
prompt(&(format!("rm: remove write-protected regular file {}? ", path.quote())))
} else {
true
}
} else {
true
}
}
}
}

fn prompt_descend(path: &Path) -> bool {
prompt(&(format!("rm: descend into directory {}? ", 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
// Most cases are covered by keep eye out for edge cases
#[cfg(unix)]
fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
let mode = metadata.permissions().mode();
// Check if directory has user write permissions
// Why is S_IWUSR showing up as a u16 on macos?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected, libc defines the modes as the type mode_t, which is u32 on linux and u16 on mac. You can check it here: https://docs.rs/libc/latest/libc/type.mode_t.html (and by changing the platform in the top-left corner)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission check seems different from before now? I think we need something like this:

let perm = |metadata: Metadata, p: Permission| {
if geteuid() == metadata.uid() {
metadata.mode() & ((p as u32) << 6) != 0
} else if getegid() == metadata.gid() {
metadata.mode() & ((p as u32) << 3) != 0
} else {
metadata.mode() & (p as u32) != 0
}
};

Could that be correct? I have to admit I'm not an expert on this, so I could be wrong :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at there's it looks to be the same as what I was doing before with shifting of bits with that magic number

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since for rm we don't care about read or execute permission we don't need to use a Permission enum like they use and because I found the libc constant I don't need to use the bit shifting... I believe

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's quite the same. It feels like we should do something like what test is doing (though indeed only for writing). S_IWUSR is documented as "write permissions for the owner of the file" so that does not say much about the current user or access by group. Several sources also recommend using libc::access (or nix::access). @sylvestre do you know how this all works and what the right approach is?

Copy link
Contributor Author

@palaster palaster Oct 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gnu rm only checks for owner write permission you can test that by running:

touch u-w-empty.txt
touch g-w-empty.txt
touch o-w-empty.txt
chmod u-w u-w-empty.txt
chmod g-w g-w-empty.txt
chmod o-w o-w-empty.txt

Then use rm *
rm then only prompts for u-w-empty.txt
rm: remove write-protected regular empty file 'u-w-empty.txt'? y

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, you seem to be right. I think you've got a nice approximation at the moment. Looking at the GNU code this whole thing is quite complicated. Let's put a comment here that it's probably not 100% compatible and move on with our lives 😄

let user_writable = (mode & (libc::S_IWUSR as u32)) != 0;
if !user_writable {
prompt(&(format!("remove write-protected directory {}? ", path.quote())))
} else if options.interactive == InteractiveMode::Always {
prompt(&(format!("remove directory {}? ", path.quote())))
} else {
true
}
}

fn prompt_file(path: &Path, is_dir: bool) -> bool {
if is_dir {
prompt(&(format!("rm: remove directory {}? ", path.quote())))
// For windows we can use windows metadata trait and file attributes to see if a directory is readonly
#[cfg(windows)]
fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool {
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(&(format!("remove write-protected directory {}? ", path.quote())))
} else if options.interactive == InteractiveMode::Always {
prompt(&(format!("remove directory {}? ", path.quote())))
} else {
prompt(&(format!("rm: remove file {}? ", path.quote())))
true
}
}

// I have this here for completeness but it will always return "remove directory {}" because metadata.permissions().readonly() only works for file not directories
palaster marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(not(windows))]
#[cfg(not(unix))]
fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool {
if options.interactive == InteractiveMode::Always {
prompt(&(format!("remove directory {}? ", path.quote())))
} else {
true
}
}

fn prompt_descend(path: &Path) -> bool {
prompt(&(format!("descend into directory {}? ", path.quote())))
}

fn normalize(path: &Path) -> PathBuf {
// copied from https://github.com/rust-lang/cargo/blob/2e4cfc2b7d43328b207879228a2ca7d427d188bb/src/cargo/util/paths.rs#L65-L90
// both projects are MIT https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT
Expand All @@ -487,7 +586,7 @@ fn normalize(path: &Path) -> PathBuf {
}

fn prompt(msg: &str) -> bool {
let _ = stderr().write_all(msg.as_bytes());
let _ = stderr().write_all(format!("{}: {}", uucore::util_name(), msg).as_bytes());
let _ = stderr().flush();

let mut buf = Vec::new();
Expand All @@ -501,15 +600,13 @@ fn prompt(msg: &str) -> bool {
}

#[cfg(not(windows))]
fn is_symlink_dir(_metadata: &fs::Metadata) -> bool {
fn is_symlink_dir(_metadata: &Metadata) -> bool {
false
}

#[cfg(windows)]
use std::os::windows::prelude::MetadataExt;

#[cfg(windows)]
fn is_symlink_dir(metadata: &fs::Metadata) -> bool {
fn is_symlink_dir(metadata: &Metadata) -> bool {
use std::os::windows::prelude::MetadataExt;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY;

metadata.file_type().is_symlink()
Expand Down