obliterate 1.2.0

Force-remove Files and Directories on Linux Including Paths with 000 Permissions.
Documentation
use crate::error::Error;
use crate::{ensure_removed, remove};
use std::fs;
use std::os::unix::fs as unix_fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;

fn tmp() -> TempDir {
    tempfile::tempdir().expect("failed to create temp dir")
}

#[test]
fn remove_a_regular_file() {
    let dir = tmp();
    let file = dir.path().join("file.txt");
    fs::write(&file, b"hello").unwrap();

    remove(&file).expect("should remove file");
    assert!(!file.exists());
}

#[test]
fn remove_an_empty_directory() {
    let dir = tmp();
    let sub = dir.path().join("empty");
    fs::create_dir(&sub).unwrap();

    remove(&sub).expect("should remove empty dir");
    assert!(!sub.exists());
}

#[test]
fn remove_a_nested_directory_tree() {
    let dir = tmp();
    let root = dir.path().join("root");
    fs::create_dir_all(root.join("a/b/c")).unwrap();
    fs::write(root.join("a/b/c/file.txt"), b"deep").unwrap();
    fs::write(root.join("a/file.txt"), b"mid").unwrap();

    remove(&root).expect("should remove nested tree");
    assert!(!root.exists());
}

#[test]
fn remove_a_symlink_without_following_it() {
    let dir = tmp();
    let target = dir.path().join("target");
    let link = dir.path().join("link");
    fs::create_dir(&target).unwrap();
    unix_fs::symlink(&target, &link).unwrap();

    remove(&link).expect("should remove symlink");
    assert!(!link.exists(), "symlink should be gone");
    assert!(target.exists(), "target must survive");
}

#[test]
fn remove_returns_not_found_for_missing_path() {
    let dir = tmp();
    let missing = dir.path().join("ghost");

    let err = remove(&missing).expect_err("should fail");
    assert!(matches!(err, Error::Io(e) if e.kind() == std::io::ErrorKind::NotFound));
}

#[test]
fn remove_rejects_dotdot_as_last_segment() {
    let dir = tmp();
    let sub = dir.path().join("sub");
    fs::create_dir(&sub).unwrap();
    let dotdot = sub.join("..");

    let err = remove(&dotdot).expect_err("should reject '..'");
    assert!(matches!(
        err,
        Error::Io(ref e) if e.kind() == std::io::ErrorKind::InvalidInput
    ));
}

#[test]
fn remove_a_readonly_file() {
    let dir = tmp();
    let file = dir.path().join("ro.txt");
    fs::write(&file, b"locked").unwrap();

    let mut perms = fs::metadata(&file).unwrap().permissions();
    perms.set_mode(0o444);
    fs::set_permissions(&file, perms).unwrap();

    remove(&file).expect("should remove read-only file");
    assert!(!file.exists());
}

#[test]
fn remove_a_directory_without_read_permission() {
    if unsafe { libc::getuid() } == 0 {
        return;
    }

    let dir = tmp();
    let locked = dir.path().join("locked");
    fs::create_dir(&locked).unwrap();
    fs::write(locked.join("file.txt"), b"hi").unwrap();

    use std::os::unix::fs::PermissionsExt;
    let mut perms = fs::metadata(&locked).unwrap().permissions();
    perms.set_mode(0o000);
    fs::set_permissions(&locked, perms).unwrap();

    let result = remove(&locked);

    if locked.exists() {
        if let Ok(meta) = fs::symlink_metadata(&locked) {
            let mut perms = meta.permissions();
            perms.set_mode(0o755);
            let _ = fs::set_permissions(&locked, perms);
        }
    }

    result.expect("should remove directory even without read permission");
    assert!(!locked.exists());
}

#[test]
fn ensure_removed_succeeds_when_path_does_not_exist() {
    let dir = tmp();
    let missing = dir.path().join("never_existed");

    ensure_removed(&missing).expect("should succeed silently");
}

#[test]
fn ensure_removed_removes_existing_file() {
    let dir = tmp();
    let file = dir.path().join("file.txt");
    fs::write(&file, b"data").unwrap();

    ensure_removed(&file).expect("should remove existing file");
    assert!(!file.exists());
}

#[test]
fn ensure_removed_removes_existing_tree() {
    let dir = tmp();
    let root = dir.path().join("tree");
    fs::create_dir_all(root.join("x/y")).unwrap();
    fs::write(root.join("x/y/z.txt"), b"leaf").unwrap();

    ensure_removed(&root).expect("should remove existing tree");
    assert!(!root.exists());
}

#[test]
fn ensure_removed_is_idempotent() {
    let dir = tmp();
    let file = dir.path().join("once.txt");
    fs::write(&file, b"x").unwrap();

    ensure_removed(&file).expect("first call");
    ensure_removed(&file).expect("second call on already-removed path");
}