Skip to content

Commit a16630f

Browse files
AnirbanHalder654322just-an-engineersylvestre
authored
rm: fix the usage of '/..' '/.' with -rf options
fix the test tests/rm/r-4 --------- Co-authored-by: Julian Beltz <[email protected]> Co-authored-by: Sylvestre Ledru <[email protected]>
1 parent 8a481cc commit a16630f

File tree

3 files changed

+148
-4
lines changed

3 files changed

+148
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/rm/src/rm.rs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (path) eacces inacc
6+
// spell-checker:ignore (path) eacces inacc rm-r4
77

88
use clap::{builder::ValueParser, crate_version, parser::ValueSource, Arg, ArgAction, Command};
99
use std::collections::VecDeque;
1010
use std::ffi::{OsStr, OsString};
1111
use std::fs::{self, File, Metadata};
1212
use std::io::ErrorKind;
1313
use std::ops::BitOr;
14+
#[cfg(not(windows))]
15+
use std::os::unix::ffi::OsStrExt;
16+
use std::path::MAIN_SEPARATOR;
1417
use std::path::{Path, PathBuf};
1518
use uucore::display::Quotable;
1619
use uucore::error::{UResult, USimpleError, UUsageError};
17-
use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show_error};
20+
use uucore::{
21+
format_usage, help_about, help_section, help_usage, os_str_as_bytes, prompt_yes, show_error,
22+
};
1823
use walkdir::{DirEntry, WalkDir};
1924

2025
#[derive(Eq, PartialEq, Clone, Copy)]
@@ -290,6 +295,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
290295

291296
for filename in files {
292297
let file = Path::new(filename);
298+
293299
had_err = match file.symlink_metadata() {
294300
Ok(metadata) => {
295301
if metadata.is_dir() {
@@ -300,6 +306,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
300306
remove_file(file, options)
301307
}
302308
}
309+
303310
Err(_e) => {
304311
// TODO: actually print out the specific error
305312
// TODO: When the error is not about missing files
@@ -326,6 +333,15 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
326333
fn handle_dir(path: &Path, options: &Options) -> bool {
327334
let mut had_err = false;
328335

336+
let path = clean_trailing_slashes(path);
337+
if path_is_current_or_parent_directory(path) {
338+
show_error!(
339+
"refusing to remove '.' or '..' directory: skipping '{}'",
340+
path.display()
341+
);
342+
return true;
343+
}
344+
329345
let is_root = path.has_root() && path.parent().is_none();
330346
if options.recursive && (!is_root || !options.preserve_root) {
331347
if options.interactive != InteractiveMode::Always && !options.verbose {
@@ -396,7 +412,11 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
396412
} else if options.dir && (!is_root || !options.preserve_root) {
397413
had_err = remove_dir(path, options).bitor(had_err);
398414
} else if options.recursive {
399-
show_error!("could not remove directory {}", path.quote());
415+
show_error!(
416+
"it is dangerous to operate recursively on '{}'",
417+
MAIN_SEPARATOR
418+
);
419+
show_error!("use --no-preserve-root to override this failsafe");
400420
had_err = true;
401421
} else {
402422
show_error!(
@@ -559,6 +579,20 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata
559579
true
560580
}
561581
}
582+
/// Checks if the path is referring to current or parent directory , if it is referring to current or any parent directory in the file tree e.g '/../..' , '../..'
583+
fn path_is_current_or_parent_directory(path: &Path) -> bool {
584+
let path_str = os_str_as_bytes(path.as_os_str());
585+
let dir_separator = MAIN_SEPARATOR as u8;
586+
if let Ok(path_bytes) = path_str {
587+
return path_bytes == ([b'.'])
588+
|| path_bytes == ([b'.', b'.'])
589+
|| path_bytes.ends_with(&[dir_separator, b'.'])
590+
|| path_bytes.ends_with(&[dir_separator, b'.', b'.'])
591+
|| path_bytes.ends_with(&[dir_separator, b'.', dir_separator])
592+
|| path_bytes.ends_with(&[dir_separator, b'.', b'.', dir_separator]);
593+
}
594+
false
595+
}
562596

563597
// For windows we can use windows metadata trait and file attributes to see if a directory is readonly
564598
#[cfg(windows)]
@@ -586,6 +620,40 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata
586620
}
587621
}
588622

623+
/// Removes trailing slashes, for example 'd/../////' yield 'd/../' required to fix rm-r4 GNU test
624+
fn clean_trailing_slashes(path: &Path) -> &Path {
625+
let path_str = os_str_as_bytes(path.as_os_str());
626+
let dir_separator = MAIN_SEPARATOR as u8;
627+
628+
if let Ok(path_bytes) = path_str {
629+
let mut idx = if path_bytes.len() > 1 {
630+
path_bytes.len() - 1
631+
} else {
632+
return path;
633+
};
634+
// Checks if element at the end is a '/'
635+
if path_bytes[idx] == dir_separator {
636+
for i in (1..path_bytes.len()).rev() {
637+
// Will break at the start of the continuous sequence of '/', eg: "abc//////" , will break at
638+
// "abc/", this will clean ////// to the root '/', so we have to be careful to not
639+
// delete the root.
640+
if path_bytes[i - 1] != dir_separator {
641+
idx = i;
642+
break;
643+
}
644+
}
645+
#[cfg(unix)]
646+
return Path::new(OsStr::from_bytes(&path_bytes[0..=idx]));
647+
648+
#[cfg(not(unix))]
649+
// Unwrapping is fine here as os_str_as_bytes() would return an error on non unix
650+
// systems with non utf-8 characters and thus bypass the if let Ok branch
651+
return Path::new(std::str::from_utf8(&path_bytes[0..=idx]).unwrap());
652+
}
653+
}
654+
path
655+
}
656+
589657
fn prompt_descend(path: &Path) -> bool {
590658
prompt_yes!("descend into directory {}?", path.quote())
591659
}
@@ -611,3 +679,17 @@ fn is_symlink_dir(metadata: &Metadata) -> bool {
611679
metadata.file_type().is_symlink()
612680
&& ((metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY) != 0)
613681
}
682+
683+
mod tests {
684+
685+
#[test]
686+
// Testing whether path the `/////` collapses to `/`
687+
fn test_collapsible_slash_path() {
688+
use std::path::Path;
689+
690+
use crate::clean_trailing_slashes;
691+
let path = Path::new("/////");
692+
693+
assert_eq!(Path::new("/"), clean_trailing_slashes(path));
694+
}
695+
}

tests/by-util/test_rm.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,68 @@ fn test_remove_inaccessible_dir() {
677677
assert!(!at.dir_exists(dir_1));
678678
}
679679

680+
#[test]
681+
#[cfg(not(windows))]
682+
fn test_rm_current_or_parent_dir_rm4() {
683+
let ts = TestScenario::new(util_name!());
684+
let at = &ts.fixtures;
685+
686+
at.mkdir("d");
687+
688+
let answers = [
689+
"rm: refusing to remove '.' or '..' directory: skipping 'd/.'",
690+
"rm: refusing to remove '.' or '..' directory: skipping 'd/./'",
691+
"rm: refusing to remove '.' or '..' directory: skipping 'd/./'",
692+
"rm: refusing to remove '.' or '..' directory: skipping 'd/..'",
693+
"rm: refusing to remove '.' or '..' directory: skipping 'd/../'",
694+
];
695+
let std_err_str = ts
696+
.ucmd()
697+
.arg("-rf")
698+
.arg("d/.")
699+
.arg("d/./")
700+
.arg("d/.////")
701+
.arg("d/..")
702+
.arg("d/../")
703+
.fails()
704+
.stderr_move_str();
705+
706+
for (idx, line) in std_err_str.lines().enumerate() {
707+
assert_eq!(line, answers[idx]);
708+
}
709+
}
710+
711+
#[test]
712+
#[cfg(windows)]
713+
fn test_rm_current_or_parent_dir_rm4_windows() {
714+
let ts = TestScenario::new(util_name!());
715+
let at = &ts.fixtures;
716+
717+
at.mkdir("d");
718+
719+
let answers = [
720+
"rm: refusing to remove '.' or '..' directory: skipping 'd\\.'",
721+
"rm: refusing to remove '.' or '..' directory: skipping 'd\\.\\'",
722+
"rm: refusing to remove '.' or '..' directory: skipping 'd\\.\\'",
723+
"rm: refusing to remove '.' or '..' directory: skipping 'd\\..'",
724+
"rm: refusing to remove '.' or '..' directory: skipping 'd\\..\\'",
725+
];
726+
let std_err_str = ts
727+
.ucmd()
728+
.arg("-rf")
729+
.arg("d\\.")
730+
.arg("d\\.\\")
731+
.arg("d\\.\\\\\\\\")
732+
.arg("d\\..")
733+
.arg("d\\..\\")
734+
.fails()
735+
.stderr_move_str();
736+
737+
for (idx, line) in std_err_str.lines().enumerate() {
738+
assert_eq!(line, answers[idx]);
739+
}
740+
}
741+
680742
#[test]
681743
#[cfg(not(windows))]
682744
fn test_fifo_removal() {

0 commit comments

Comments
 (0)