Skip to content

Commit 45e6cbd

Browse files
sylvestrecakebaker
authored andcommitted
rm: remove the unsafe code and move the rm linux functions in a dedicated file
1 parent e773c95 commit 45e6cbd

File tree

4 files changed

+399
-170
lines changed

4 files changed

+399
-170
lines changed

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

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

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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::*;

0 commit comments

Comments
 (0)