1use std::io;
2use std::path::Path;
3
4#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum InteractiveMode {
10 Never,
12 Once,
14 Always,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PreserveRoot {
21 Yes,
23 All,
25 No,
27}
28
29#[derive(Debug)]
31pub struct RmConfig {
32 pub force: bool,
34 pub interactive: InteractiveMode,
36 pub recursive: bool,
38 pub dir: bool,
40 pub verbose: bool,
42 pub preserve_root: PreserveRoot,
44 pub one_file_system: bool,
46}
47
48impl Default for RmConfig {
49 fn default() -> Self {
50 Self {
51 force: false,
52 interactive: InteractiveMode::Never,
53 recursive: false,
54 dir: false,
55 verbose: false,
56 preserve_root: PreserveRoot::Yes,
57 one_file_system: false,
58 }
59 }
60}
61
62fn prompt_yes(msg: &str) -> bool {
64 eprint!("{}", msg);
65 let mut answer = String::new();
66 if io::stdin().read_line(&mut answer).is_err() {
67 return false;
68 }
69 let trimmed = answer.trim();
70 trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes")
71}
72
73pub fn rm_path(path: &Path, config: &RmConfig) -> Result<bool, io::Error> {
79 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
81 if canonical == Path::new("/") {
82 if matches!(config.preserve_root, PreserveRoot::Yes | PreserveRoot::All) {
83 eprintln!("rm: it is dangerous to operate recursively on '/'");
84 eprintln!("rm: use --no-preserve-root to override this failsafe");
85 return Ok(false);
86 }
87 }
88
89 let meta = match std::fs::symlink_metadata(path) {
90 Ok(m) => m,
91 Err(e) => {
92 if config.force && e.kind() == io::ErrorKind::NotFound {
93 return Ok(true);
94 }
95 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
96 return Ok(false);
97 }
98 };
99
100 if meta.is_dir() {
101 if config.recursive {
102 if config.interactive == InteractiveMode::Always
103 && !prompt_yes(&format!(
104 "rm: descend into directory '{}'? ",
105 path.display()
106 ))
107 {
108 return Ok(false);
109 }
110 #[cfg(unix)]
111 let root_dev = meta.dev();
112 #[cfg(not(unix))]
113 let root_dev = 0u64;
114 let ok = rm_recursive(path, config, root_dev)?;
115 Ok(ok)
116 } else if config.dir {
117 if config.interactive == InteractiveMode::Always
118 && !prompt_yes(&format!("rm: remove directory '{}'? ", path.display()))
119 {
120 return Ok(false);
121 }
122 match std::fs::remove_dir(path) {
123 Ok(()) => {
124 if config.verbose {
125 eprintln!("removed directory '{}'", path.display());
126 }
127 Ok(true)
128 }
129 Err(e) => {
130 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
131 Ok(false)
132 }
133 }
134 } else {
135 eprintln!("rm: cannot remove '{}': Is a directory", path.display());
136 Ok(false)
137 }
138 } else {
139 if config.interactive == InteractiveMode::Always
140 && !prompt_yes(&format!("rm: remove file '{}'? ", path.display()))
141 {
142 return Ok(false);
143 }
144 match std::fs::remove_file(path) {
145 Ok(()) => {
146 if config.verbose {
147 eprintln!("removed '{}'", path.display());
148 }
149 Ok(true)
150 }
151 Err(e) => {
152 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
153 Ok(false)
154 }
155 }
156 }
157}
158
159fn rm_recursive(path: &Path, config: &RmConfig, root_dev: u64) -> Result<bool, io::Error> {
162 if config.interactive == InteractiveMode::Never && !config.verbose {
164 let success = std::sync::atomic::AtomicBool::new(true);
165 rm_recursive_parallel(path, config, root_dev, &success);
166 if let Err(e) = std::fs::remove_dir(path) {
168 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
169 return Ok(false);
170 }
171 return Ok(success.load(std::sync::atomic::Ordering::Relaxed));
172 }
173
174 let mut success = true;
175
176 let entries = match std::fs::read_dir(path) {
177 Ok(rd) => rd,
178 Err(e) => {
179 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
180 return Ok(false);
181 }
182 };
183
184 for entry in entries {
185 let entry = entry?;
186 let child_path = entry.path();
187 let child_meta = match std::fs::symlink_metadata(&child_path) {
188 Ok(m) => m,
189 Err(e) => {
190 eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
191 success = false;
192 continue;
193 }
194 };
195
196 #[cfg(unix)]
197 let skip_fs = config.one_file_system && child_meta.dev() != root_dev;
198 #[cfg(not(unix))]
199 let skip_fs = false;
200
201 if skip_fs {
202 continue;
203 }
204
205 if child_meta.is_dir() {
206 if config.interactive == InteractiveMode::Always
207 && !prompt_yes(&format!(
208 "rm: descend into directory '{}'? ",
209 child_path.display()
210 ))
211 {
212 success = false;
213 continue;
214 }
215 if !rm_recursive(&child_path, config, root_dev)? {
216 success = false;
217 }
218 } else {
219 if config.interactive == InteractiveMode::Always
220 && !prompt_yes(&format!("rm: remove file '{}'? ", child_path.display()))
221 {
222 success = false;
223 continue;
224 }
225 match std::fs::remove_file(&child_path) {
226 Ok(()) => {
227 if config.verbose {
228 eprintln!("removed '{}'", child_path.display());
229 }
230 }
231 Err(e) => {
232 eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
233 success = false;
234 }
235 }
236 }
237 }
238
239 if config.interactive == InteractiveMode::Always
241 && !prompt_yes(&format!("rm: remove directory '{}'? ", path.display()))
242 {
243 return Ok(false);
244 }
245
246 match std::fs::remove_dir(path) {
247 Ok(()) => {
248 if config.verbose {
249 eprintln!("removed directory '{}'", path.display());
250 }
251 }
252 Err(e) => {
253 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
254 success = false;
255 }
256 }
257
258 Ok(success)
259}
260
261fn rm_recursive_parallel(
263 path: &Path,
264 config: &RmConfig,
265 root_dev: u64,
266 success: &std::sync::atomic::AtomicBool,
267) {
268 let entries = match std::fs::read_dir(path) {
269 Ok(rd) => rd,
270 Err(e) => {
271 if !config.force {
272 eprintln!("rm: cannot remove '{}': {}", path.display(), e);
273 }
274 success.store(false, std::sync::atomic::Ordering::Relaxed);
275 return;
276 }
277 };
278
279 let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
280
281 use rayon::prelude::*;
282 entries.par_iter().for_each(|entry| {
283 let child_path = entry.path();
284 let child_meta = match std::fs::symlink_metadata(&child_path) {
285 Ok(m) => m,
286 Err(e) => {
287 if !config.force {
288 eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
289 }
290 success.store(false, std::sync::atomic::Ordering::Relaxed);
291 return;
292 }
293 };
294
295 #[cfg(unix)]
296 let skip_fs = config.one_file_system && child_meta.dev() != root_dev;
297 #[cfg(not(unix))]
298 let skip_fs = false;
299
300 if skip_fs {
301 return;
302 }
303
304 if child_meta.is_dir() {
305 rm_recursive_parallel(&child_path, config, root_dev, success);
306 if let Err(e) = std::fs::remove_dir(&child_path) {
307 if !config.force {
308 eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
309 }
310 success.store(false, std::sync::atomic::Ordering::Relaxed);
311 }
312 } else if let Err(e) = std::fs::remove_file(&child_path) {
313 if !config.force {
314 eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
315 }
316 success.store(false, std::sync::atomic::Ordering::Relaxed);
317 }
318 });
319}