Skip to content

Commit e9aa7c5

Browse files
committed
rm: remove the unsafe code and move the rm linux functions in a dedicated file
1 parent e755c86 commit e9aa7c5

File tree

4 files changed

+421
-170
lines changed

4 files changed

+421
-170
lines changed

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

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

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)