1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//  * This file is part of the uutils coreutils package.
//  *
//  * (c) Alex Lyon <arcterus@mail.com>
//  *
//  * For the full copyright and license information, please view the LICENSE
//  * file that was distributed with this source code.

// spell-checker:ignore (ToDO) bitor ulong

#[macro_use]
extern crate uucore;

use clap::{App, Arg};
use remove_dir_all::remove_dir_all;
use std::collections::VecDeque;
use std::fs;
use std::io::{stderr, stdin, BufRead, Write};
use std::ops::BitOr;
use std::path::Path;
use walkdir::{DirEntry, WalkDir};

#[derive(Eq, PartialEq, Clone, Copy)]
enum InteractiveMode {
    None,
    Once,
    Always,
}

struct Options {
    force: bool,
    interactive: InteractiveMode,
    #[allow(dead_code)]
    one_fs: bool,
    preserve_root: bool,
    recursive: bool,
    dir: bool,
    verbose: bool,
}

static ABOUT: &str = "Remove (unlink) the FILE(s)";
static VERSION: &str = env!("CARGO_PKG_VERSION");
static OPT_DIR: &str = "dir";
static OPT_INTERACTIVE: &str = "interactive";
static OPT_FORCE: &str = "force";
static OPT_NO_PRESERVE_ROOT: &str = "no-preserve-root";
static OPT_ONE_FILE_SYSTEM: &str = "one-file-system";
static OPT_PRESERVE_ROOT: &str = "preserve-root";
static OPT_PROMPT: &str = "prompt";
static OPT_PROMPT_MORE: &str = "prompt-more";
static OPT_RECURSIVE: &str = "recursive";
static OPT_RECURSIVE_R: &str = "recursive_R";
static OPT_VERBOSE: &str = "verbose";

static ARG_FILES: &str = "files";

fn get_usage() -> String {
    format!("{0} [OPTION]... FILE...", executable!())
}

fn get_long_usage() -> String {
    String::from(
        "By default, rm does not remove directories.  Use the --recursive (-r or -R)
        option to remove each listed directory, too, along with all of its contents

        To remove a file whose name starts with a '-', for example '-foo',
        use one of these commands:
        rm -- -foo

        rm ./-foo

        Note that if you use rm to remove a file, it might be possible to recover
        some of its contents, given sufficient expertise and/or time.  For greater
        assurance that the contents are truly unrecoverable, consider using shred.",
    )
}

pub fn uumain(args: impl uucore::Args) -> i32 {
    let usage = get_usage();
    let long_usage = get_long_usage();

    let matches = App::new(executable!())
        .version(VERSION)
        .about(ABOUT)
        .usage(&usage[..])
        .after_help(&long_usage[..])

        .arg(
            Arg::with_name(OPT_FORCE)
            .short("f")
            .long(OPT_FORCE)
            .multiple(true)
            .help("ignore nonexistent files and arguments, never prompt")
        )
        .arg(
            Arg::with_name(OPT_PROMPT)
            .short("i")
            .long("prompt before every removal")
        )
        .arg(
            Arg::with_name(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")
        )
        .arg(
            Arg::with_name(OPT_INTERACTIVE)
            .long(OPT_INTERACTIVE)
            .help("prompt according to WHEN: never, once (-I), or always (-i). Without WHEN, prompts always")
            .value_name("WHEN")
            .takes_value(true)
        )
        .arg(
            Arg::with_name(OPT_ONE_FILE_SYSTEM)
            .long(OPT_ONE_FILE_SYSTEM)
            .help("when removing a hierarchy recursively, skip any directory that is on a file system different from that of the corresponding command line argument (NOT IMPLEMENTED)")
        )
        .arg(
            Arg::with_name(OPT_NO_PRESERVE_ROOT)
            .long(OPT_NO_PRESERVE_ROOT)
            .help("do not treat '/' specially")
        )
        .arg(
            Arg::with_name(OPT_PRESERVE_ROOT)
            .long(OPT_PRESERVE_ROOT)
            .help("do not remove '/' (default)")
        )
        .arg(
            Arg::with_name(OPT_RECURSIVE).short("r")
            .long(OPT_RECURSIVE)
            .help("remove directories and their contents recursively")
        )
        .arg(
            // To mimic GNU's behavior we also want the '-R' flag. However, using clap's
            // alias method 'visible_alias("R")' would result in a long '--R' flag.
            Arg::with_name(OPT_RECURSIVE_R).short("R")
            .help("Equivalent to -r")
        )
        .arg(
            Arg::with_name(OPT_DIR)
            .short("d")
            .long(OPT_DIR)
            .help("remove empty directories")
        )
        .arg(
            Arg::with_name(OPT_VERBOSE)
            .short("v")
            .long(OPT_VERBOSE)
            .help("explain what is being done")
        )
        .arg(
            Arg::with_name(ARG_FILES)
            .multiple(true)
            .takes_value(true)
            .min_values(1)
        )
        .get_matches_from(args);

    let files: Vec<String> = matches
        .values_of(ARG_FILES)
        .map(|v| v.map(ToString::to_string).collect())
        .unwrap_or_default();

    let force = matches.is_present(OPT_FORCE);

    if files.is_empty() && !force {
        // Still check by hand and not use clap
        // Because "rm -f" is a thing
        show_error!("missing an argument");
        show_error!("for help, try '{0} --help'", executable!());
        return 1;
    } else {
        let options = Options {
            force,
            interactive: {
                if matches.is_present(OPT_PROMPT) {
                    InteractiveMode::Always
                } else if matches.is_present(OPT_PROMPT_MORE) {
                    InteractiveMode::Once
                } else if matches.is_present(OPT_INTERACTIVE) {
                    match &matches.value_of(OPT_INTERACTIVE).unwrap()[..] {
                        "none" => InteractiveMode::None,
                        "once" => InteractiveMode::Once,
                        "always" => InteractiveMode::Always,
                        val => crash!(1, "Invalid argument to interactive ({})", val),
                    }
                } else {
                    InteractiveMode::None
                }
            },
            one_fs: matches.is_present(OPT_ONE_FILE_SYSTEM),
            preserve_root: !matches.is_present(OPT_NO_PRESERVE_ROOT),
            recursive: matches.is_present(OPT_RECURSIVE) || matches.is_present(OPT_RECURSIVE_R),
            dir: matches.is_present(OPT_DIR),
            verbose: matches.is_present(OPT_VERBOSE),
        };
        if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) {
            let msg = if options.recursive {
                "Remove all arguments recursively? "
            } else {
                "Remove all arguments? "
            };
            if !prompt(msg) {
                return 0;
            }
        }

        if remove(files, options) {
            return 1;
        }
    }

    0
}

// TODO: implement one-file-system (this may get partially implemented in walkdir)
fn remove(files: Vec<String>, options: Options) -> bool {
    let mut had_err = false;

    for filename in &files {
        let file = Path::new(filename);
        had_err = match file.symlink_metadata() {
            Ok(metadata) => {
                if metadata.is_dir() {
                    handle_dir(file, &options)
                } else if is_symlink_dir(&metadata) {
                    remove_dir(file, &options)
                } else {
                    remove_file(file, &options)
                }
            }
            Err(_e) => {
                // TODO: actually print out the specific error
                // TODO: When the error is not about missing files
                // (e.g., permission), even rm -f should fail with
                // outputting the error, but there's no easy eay.
                if !options.force {
                    show_error!("cannot remove '{}': No such file or directory", filename);
                    true
                } else {
                    false
                }
            }
        }
        .bitor(had_err);
    }

    had_err
}

fn handle_dir(path: &Path, options: &Options) -> bool {
    let mut had_err = false;

    let is_root = path.has_root() && path.parent().is_none();
    if options.recursive && (!is_root || !options.preserve_root) {
        if options.interactive != InteractiveMode::Always {
            // we need the extra crate because apparently fs::remove_dir_all() does not function
            // correctly on Windows
            if let Err(e) = remove_dir_all(path) {
                had_err = true;
                show_error!("could not remove '{}': {}", path.display(), e);
            }
        } else {
            let mut dirs: VecDeque<DirEntry> = VecDeque::new();

            for entry in WalkDir::new(path) {
                match entry {
                    Ok(entry) => {
                        let file_type = entry.file_type();
                        if file_type.is_dir() {
                            dirs.push_back(entry);
                        } else {
                            had_err = remove_file(entry.path(), options).bitor(had_err);
                        }
                    }
                    Err(e) => {
                        had_err = true;
                        show_error!("recursing in '{}': {}", path.display(), e);
                    }
                }
            }

            for dir in dirs.iter().rev() {
                had_err = remove_dir(dir.path(), options).bitor(had_err);
            }
        }
    } else if options.dir && (!is_root || !options.preserve_root) {
        had_err = remove_dir(path, options).bitor(had_err);
    } else if options.recursive {
        show_error!("could not remove directory '{}'", path.display());
        had_err = true;
    } else {
        show_error!(
            "cannot remove '{}': Is a directory", // GNU's rm error message does not include help
            path.display()
        );
        had_err = true;
    }

    had_err
}

fn remove_dir(path: &Path, options: &Options) -> bool {
    let response = if options.interactive == InteractiveMode::Always {
        prompt_file(path, true)
    } else {
        true
    };
    if response {
        if let Ok(mut read_dir) = fs::read_dir(path) {
            if options.dir || options.recursive {
                if read_dir.next().is_none() {
                    match fs::remove_dir(path) {
                        Ok(_) => {
                            if options.verbose {
                                println!("removed directory '{}'", path.display());
                            }
                        }
                        Err(e) => {
                            show_error!("cannot remove '{}': {}", path.display(), e);
                            return true;
                        }
                    }
                } else {
                    // directory can be read but is not empty
                    show_error!("cannot remove '{}': Directory not empty", path.display());
                    return true;
                }
            } else {
                // called to remove a symlink_dir (windows) without "-r"/"-R" or "-d"
                show_error!("cannot remove '{}': Is a directory", path.display());
                return true;
            }
        } else {
            // GNU's rm shows this message if directory is empty but not readable
            show_error!("cannot remove '{}': Directory not empty", path.display());
            return true;
        }
    }

    false
}

fn remove_file(path: &Path, options: &Options) -> bool {
    let response = if options.interactive == InteractiveMode::Always {
        prompt_file(path, false)
    } else {
        true
    };
    if response {
        match fs::remove_file(path) {
            Ok(_) => {
                if options.verbose {
                    println!("removed '{}'", path.display());
                }
            }
            Err(e) => {
                show_error!("removing '{}': {}", path.display(), e);
                return true;
            }
        }
    }

    false
}

fn prompt_file(path: &Path, is_dir: bool) -> bool {
    if is_dir {
        prompt(&(format!("rm: remove directory '{}'? ", path.display())))
    } else {
        prompt(&(format!("rm: remove file '{}'? ", path.display())))
    }
}

fn prompt(msg: &str) -> bool {
    let _ = stderr().write_all(msg.as_bytes());
    let _ = stderr().flush();

    let mut buf = Vec::new();
    let stdin = stdin();
    let mut stdin = stdin.lock();

    #[allow(clippy::match_like_matches_macro)]
    // `matches!(...)` macro not stabilized until rust v1.42
    match stdin.read_until(b'\n', &mut buf) {
        Ok(x) if x > 0 => match buf[0] {
            b'y' | b'Y' => true,
            _ => false,
        },
        _ => false,
    }
}

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

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

#[cfg(windows)]
fn is_symlink_dir(metadata: &fs::Metadata) -> bool {
    use std::os::raw::c_ulong;
    pub type DWORD = c_ulong;
    pub const FILE_ATTRIBUTE_DIRECTORY: DWORD = 0x10;

    metadata.file_type().is_symlink()
        && ((metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY) != 0)
}