mod error;
pub use crate::error::Error;
pub use crate::error::Result;
use std::fs;
use std::io;
use std::io::ErrorKind;
use std::path::Path;
pub fn remove<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
let parent: &Path = path
.parent()
.ok_or_else(|| Error::InvalidTarget("Invalid path, cannot get parent".to_string()))?;
let last_segment = path.components().last().ok_or_else(|| {
Error::InvalidTarget("Invalid path, cannot get last file path component".to_string())
})?;
let last_segment_str = last_segment.as_os_str().to_str().ok_or_else(|| {
Error::InvalidTarget("Invalid path, cannot convert last segment to string".to_string())
})?;
if cfg!(not(target_os = "windows")) && (last_segment_str == "." || last_segment_str == "..") {
return Err(Error::InvalidTarget(
"Invalid path, last path segment cannot be \".\" or \"..\"".to_string(),
));
}
let path = parent.join(last_segment);
match path.symlink_metadata() {
Ok(_) => recursive_remove(&path).map_err(Error::IoError),
Err(err) => match err.kind() {
ErrorKind::NotFound => Err(Error::NotFound),
_ => Err(Error::IoError(err)),
},
}
}
pub fn ensure_removed<P: AsRef<Path>>(path: P) -> Result<()> {
if let Err(err) = path.as_ref().symlink_metadata() {
if err.kind() == ErrorKind::NotFound {
return Ok(());
}
};
remove(path)
}
fn recursive_remove(path: &Path) -> io::Result<()> {
fix_permissions(path)?;
let metadata = path.symlink_metadata()?;
if !metadata.is_dir() {
fs::remove_file(path)
} else if fs::remove_dir(path).is_ok() {
Ok(())
} else {
for child in fs::read_dir(&path)? {
let child = child?;
let path = child.path();
stacker::maybe_grow(4 * 1024, 16 * 1024, ||
recursive_remove(&path))?;
}
fs::remove_dir(path)
}
}
#[cfg(target_os = "windows")]
fn fix_permissions(path: &Path) -> io::Result<()> {
let mut permissions = fs::symlink_metadata(&path)?.permissions();
permissions.set_readonly(false);
fs::set_permissions(&path, permissions)
}
#[cfg(not(target_os = "windows"))]
fn fix_permissions(_: &Path) -> io::Result<()> {
Ok(())
}
#[cfg(test)]
#[cfg(not(target_os = "windows"))] mod tests {
use crate::ensure_removed;
use crate::error::Error;
use crate::remove;
use std::ops::Not;
use std::process::{Command, ExitStatus};
use std::sync::Once;
static INITIALIZATION: Once = Once::new();
fn initialize() {
INITIALIZATION.call_once(|| {
sh_exec("mkdir -p target/testdir");
std::env::set_current_dir("target/testdir").unwrap();
});
}
#[test]
fn remove_parent_directory_test() {
initialize();
sh_exec("mkdir -p parentdirtest");
assert_invalid_target(remove("parentdirtest/.."));
assert_invalid_target(remove(".."));
sh_exec("rmdir parentdirtest");
}
#[test]
fn remove_current_directory_test() {
initialize();
sh_exec("mkdir -p dotdir");
let remove_result = remove("dotdir/.");
assert!(
remove_result.is_ok(),
"in contrast to `rm -rf`, removing a path with last component being . is allowed.\
The parent should still be computable though."
);
assert_invalid_target(remove("."));
sh_exec("rm -rf dotdir");
}
fn assert_invalid_target(remove_result: Result<(), Error>) {
match remove_result {
Err(Error::InvalidTarget(_)) => (),
_ => panic!(
"removal target '..' should be considered invalid, actual result is: {:?}",
remove_result
),
};
}
#[test]
fn remove_inner_symlink_test() {
initialize();
sh_exec("mkdir inner");
sh_exec("touch inner/real_file");
sh_exec("ln -s inner/real_file inner/symlink");
assert!(remove("inner/symlink").is_ok());
sh_exec("test -f inner/real_file");
sh_exec("! test -e inner/symlink");
sh_exec("rm -rf inner");
}
#[test]
fn remove_outer_symlink_test() {
initialize();
sh_exec("mkdir -p dir1/dir2");
sh_exec("ln -s dir1/dir2 symlink");
assert!(remove("symlink").is_ok());
sh_exec("! test -e symlink");
sh_exec("test -e dir1");
sh_exec("rm -rf dir1");
}
#[test]
fn behavior_test() {
initialize();
test_eq_behavior("");
test_eq_behavior("touch target");
test_eq_behavior("touch target; chmod 000 target");
test_eq_behavior("touch target; chmod 777 target");
test_eq_behavior("mkdir target");
test_eq_behavior("mkdir target; ln -s unexistent target/link");
test_eq_behavior("mkdir target; chmod 000 target");
test_eq_behavior("mkdir target; chmod 777 target");
test_eq_behavior("mkdir -p target/subdir; chmod 000 target");
test_eq_behavior("mkdir -p target/subdir; chmod 777 target");
test_eq_behavior("mkdir -p target/subdir; chmod 444 target");
test_eq_behavior("mkdir -p target/subdir; chmod 222 target");
test_eq_behavior("mkdir -p target/subdir; chmod 111 target");
test_eq_behavior("ln -s unexistent target");
test_eq_behavior("ln -s /abc/def target");
test_eq_behavior("ln -s / target");
}
fn test_eq_behavior(up: &str) {
test_eq_behavior_fn(
&|| {
assert!(sh_exec_status(up).success());
},
up,
);
}
fn sh_exec(script: &str) {
if sh_exec_status(script).success().not() {
panic!("Non-zero exit status of `{}`", script)
}
}
fn sh_exec_status(script: &str) -> ExitStatus {
Command::new("sh")
.arg("-c")
.arg(script)
.status()
.expect("failed to execute `sh`")
}
fn test_eq_behavior_fn<F>(up: &F, test_name: &str)
where
F: Fn(),
{
clean();
up();
let rm_success = rm_rf_success();
clean();
up();
let rust_success = rust_remove_success();
clean();
assert_eq!(
rm_success, rust_success,
"`rm -rf` and `force_remove` behaved differently for test: {}",
test_name
);
}
fn clean() {
Command::new("sh")
.arg("-c")
.arg("chmod 777 target; rm -rf target")
.output()
.unwrap();
}
fn rm_rf_success() -> bool {
Command::new("rm")
.arg("-rf")
.arg("target")
.output()
.unwrap()
.status
.success()
}
fn rust_remove_success() -> bool {
ensure_removed("target").is_ok()
}
}