Skip to content

Commit 7bbd68d

Browse files
committed
rm: remove the unsafe code and move the rm linux functions in a dedicated file
1 parent 56584a5 commit 7bbd68d

File tree

4 files changed

+406
-169
lines changed

4 files changed

+406
-169
lines changed

src/uu/rm/src/platform/linux.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
// Linux-specific implementations for the rm utility
7+
8+
// spell-checker:ignore fstatat unlinkat
9+
10+
use std::fs;
11+
use std::path::Path;
12+
use uucore::display::Quotable;
13+
use uucore::error::FromIo;
14+
use uucore::safe_traversal::DirFd;
15+
use uucore::show_error;
16+
use uucore::translate;
17+
18+
use super::super::{
19+
InteractiveMode, Options, is_dir_empty, is_readable_metadata, prompt_descend, prompt_dir,
20+
prompt_file, remove_file, show_permission_denied_error, show_removal_error,
21+
verbose_removed_directory, verbose_removed_file,
22+
};
23+
24+
/// Whether the given file or directory is readable.
25+
pub fn is_readable(path: &Path) -> bool {
26+
match fs::metadata(path) {
27+
Err(_) => false,
28+
Ok(metadata) => is_readable_metadata(&metadata),
29+
}
30+
}
31+
32+
/// Helper function to remove directory handling special cases
33+
pub fn remove_dir_with_special_cases(path: &Path, options: &Options, error_occurred: bool) -> bool {
34+
match fs::remove_dir(path) {
35+
Err(_) if !error_occurred && !is_readable(path) => {
36+
// For compatibility with GNU test case
37+
// `tests/rm/unread2.sh`, show "Permission denied" in this
38+
// case instead of "Directory not empty".
39+
show_permission_denied_error(path);
40+
true
41+
}
42+
Err(_) if !error_occurred && path.read_dir().is_err() => {
43+
// For compatibility with GNU test case on Linux
44+
// Check if directory is readable by attempting to read it
45+
show_permission_denied_error(path);
46+
true
47+
}
48+
Err(e) if !error_occurred => {
49+
// Check if directory is readable - if not, show permission denied
50+
// for compatibility with GNU rm behavior
51+
if is_readable(path) {
52+
show_removal_error(e, path)
53+
} else {
54+
show_permission_denied_error(path);
55+
true
56+
}
57+
}
58+
Err(_) => {
59+
// If we already had errors while
60+
// trying to remove the children, then there is no need to
61+
// show another error message as we return from each level
62+
// of the recursion.
63+
error_occurred
64+
}
65+
Ok(_) => {
66+
verbose_removed_directory(path, options);
67+
false
68+
}
69+
}
70+
}
71+
72+
pub fn safe_remove_dir_recursive(path: &Path, options: &Options) -> bool {
73+
// Base case 1: this is a file or a symbolic link.
74+
if !path.is_dir() || path.is_symlink() {
75+
return remove_file(path, options);
76+
}
77+
78+
// Try to open the directory using DirFd for secure traversal
79+
let dir_fd = match DirFd::open(path) {
80+
Ok(fd) => fd,
81+
Err(e) => {
82+
// If we can't open the directory for safe traversal,
83+
// handle the error appropriately and try to remove if possible
84+
if e.kind() == std::io::ErrorKind::PermissionDenied {
85+
// Try to remove the directory directly if it's empty
86+
match fs::remove_dir(path) {
87+
Ok(_) => {
88+
verbose_removed_directory(path, options);
89+
return false;
90+
}
91+
Err(_remove_err) => {
92+
// If we can't read the directory AND can't remove it,
93+
// show permission denied error for GNU compatibility
94+
return show_permission_denied_error(path);
95+
}
96+
}
97+
}
98+
return show_removal_error(e, path);
99+
}
100+
};
101+
102+
let error = safe_remove_dir_recursive_impl(path, &dir_fd, options);
103+
104+
// After processing all children, remove the directory itself
105+
if error {
106+
error
107+
} else {
108+
// Ask user permission if needed
109+
if options.interactive == InteractiveMode::Always && !prompt_dir(path, options) {
110+
return false;
111+
}
112+
113+
// Before trying to remove the directory, check if it's actually empty
114+
// This handles the case where some children weren't removed due to user "no" responses
115+
if !is_dir_empty(path) {
116+
// Directory is not empty, so we can't/shouldn't remove it
117+
// In interactive mode, this might be expected if user said "no" to some children
118+
// In non-interactive mode, this indicates an error (some children couldn't be removed)
119+
if options.interactive == InteractiveMode::Always {
120+
return false;
121+
}
122+
// Try to remove the directory anyway and let the system tell us why it failed
123+
// Use false for error_occurred since this is the main error we want to report
124+
return remove_dir_with_special_cases(path, options, false);
125+
}
126+
127+
// Directory is empty and user approved removal
128+
remove_dir_with_special_cases(path, options, error)
129+
}
130+
}
131+
132+
pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options) -> bool {
133+
// Read directory entries using safe traversal
134+
let entries = match dir_fd.read_dir() {
135+
Ok(entries) => entries,
136+
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
137+
// This is not considered an error - just like the original
138+
return false;
139+
}
140+
Err(e) => {
141+
if !options.force {
142+
show_error!(
143+
"{}",
144+
e.map_err_context(
145+
|| translate!("rm-error-cannot-remove", "file" => path.quote())
146+
)
147+
);
148+
}
149+
return !options.force;
150+
}
151+
};
152+
153+
let mut error = false;
154+
155+
// Process each entry
156+
for entry_name in entries {
157+
let entry_path = path.join(&entry_name);
158+
159+
// Get metadata for the entry using fstatat
160+
let entry_stat = match dir_fd.stat_at(&entry_name, false) {
161+
Ok(stat) => stat,
162+
Err(e) => {
163+
if !options.force {
164+
let e = e.map_err_context(
165+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
166+
);
167+
show_error!("{e}");
168+
}
169+
error = !options.force;
170+
continue;
171+
}
172+
};
173+
174+
// Check if it's a directory
175+
let is_dir = (entry_stat.st_mode & libc::S_IFMT) == libc::S_IFDIR;
176+
177+
if is_dir {
178+
// Ask user if they want to descend into this directory
179+
if options.interactive == InteractiveMode::Always
180+
&& !is_dir_empty(&entry_path)
181+
&& !prompt_descend(&entry_path)
182+
{
183+
continue;
184+
}
185+
186+
// Recursively remove subdirectory using safe traversal
187+
let child_dir_fd = match dir_fd.open_subdir(&entry_name) {
188+
Ok(fd) => fd,
189+
Err(e) => {
190+
// If we can't open the subdirectory for safe traversal,
191+
// try to handle it as best we can with safe operations
192+
if e.kind() == std::io::ErrorKind::PermissionDenied {
193+
// For permission denied, try to remove the directory directly
194+
// if it's empty, or show appropriate error
195+
if let Err(remove_err) = dir_fd.unlink_at(&entry_name, true) {
196+
if !options.force {
197+
let remove_err = remove_err.map_err_context(
198+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
199+
);
200+
show_error!("{remove_err}");
201+
}
202+
error = !options.force;
203+
} else {
204+
verbose_removed_directory(&entry_path, options);
205+
}
206+
} else {
207+
// For other errors, report and continue
208+
if !options.force {
209+
let e = e.map_err_context(
210+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
211+
);
212+
show_error!("{e}");
213+
}
214+
error = !options.force;
215+
}
216+
continue;
217+
}
218+
};
219+
220+
let child_error = safe_remove_dir_recursive_impl(&entry_path, &child_dir_fd, options);
221+
error = error || child_error;
222+
223+
// Ask user permission if needed for this subdirectory
224+
if !child_error
225+
&& options.interactive == InteractiveMode::Always
226+
&& !prompt_dir(&entry_path, options)
227+
{
228+
continue;
229+
}
230+
231+
// Remove the now-empty subdirectory using safe unlinkat
232+
if !child_error {
233+
if let Err(e) = dir_fd.unlink_at(&entry_name, true) {
234+
let e = e.map_err_context(
235+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
236+
);
237+
show_error!("{e}");
238+
error = true;
239+
} else {
240+
verbose_removed_directory(&entry_path, options);
241+
}
242+
}
243+
} else {
244+
// Remove file - check if user wants to remove it first
245+
if prompt_file(&entry_path, options) {
246+
if let Err(e) = dir_fd.unlink_at(&entry_name, false) {
247+
let e = e.map_err_context(
248+
|| translate!("rm-error-cannot-remove", "file" => entry_path.quote()),
249+
);
250+
show_error!("{e}");
251+
error = true;
252+
} else {
253+
verbose_removed_file(&entry_path, options);
254+
}
255+
}
256+
}
257+
}
258+
259+
error
260+
}

src/uu/rm/src/platform/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
// Platform-specific implementations for the rm utility
7+
8+
#[cfg(target_os = "linux")]
9+
pub mod linux;
10+
11+
#[cfg(target_os = "linux")]
12+
pub use linux::*;
13+
14+
#[cfg(target_os = "linux")]
15+
pub use uucore::safe_traversal::DirFd;

0 commit comments

Comments
 (0)