walkdir_minimal 1.3.0

Fast, minimalist, and POSIX-compliant recursive directory traverser with zero external dependencies.
Documentation
use super::*;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::PathBuf;

fn create_temp_dir(name: &str) -> PathBuf {
    let tmp = env::temp_dir().join(name);
    let _ = fs::remove_dir_all(&tmp);
    fs::create_dir_all(&tmp).unwrap();
    tmp
}

struct PermGuard {
    locked_path: PathBuf,
    original_mode: u32,
    root: PathBuf,
}

impl PermGuard {
    fn new(locked_path: PathBuf, restricted_mode: u32, root: PathBuf) -> Self {
        let original_mode = fs::metadata(&locked_path)
            .expect("failed to read metadata before restricting permissions")
            .permissions()
            .mode();

        fs::set_permissions(&locked_path, fs::Permissions::from_mode(restricted_mode))
            .expect("failed to set restricted permissions");

        Self {
            locked_path,
            original_mode,
            root,
        }
    }
}

impl Drop for PermGuard {
    fn drop(&mut self) {
        let _ = fs::set_permissions(
            &self.locked_path,
            fs::Permissions::from_mode(self.original_mode),
        );
        let _ = fs::remove_dir_all(&self.root);
    }
}

#[test]
fn test_ignore_permission_denied_on_locked_subdir() {
    let root = create_temp_dir("walkdir_test_permission_denied");

    fs::write(root.join("accessible.txt"), b"ok").expect("failed to create accessible.txt");

    let locked = root.join("locked");
    fs::create_dir(&locked).expect("failed to create locked dir");
    fs::write(locked.join("secret.txt"), b"secret").expect("failed to create secret.txt");

    let _guard = PermGuard::new(locked.clone(), 0o000, root.clone());

    {
        let results: Vec<_> = WalkDir::new(&root)
            .expect("failed to create walker")
            .ignore_permission_denied(false)
            .collect();

        let errors: Vec<_> = results.iter().filter(|r| r.is_err()).collect();
        let entries: Vec<_> = results.iter().filter_map(|r| r.as_ref().ok()).collect();

        println!("ignore_permission_denied false");
        println!("{:#?}", errors);

        assert_eq!(
            errors.len(),
            1,
            "expected exactly one error, got: {errors:?}"
        );
        match errors[0].as_ref().unwrap_err() {
            WalkError::Io(io_err, path) => {
                assert_eq!(
                    io_err.kind(),
                    std::io::ErrorKind::PermissionDenied,
                    "expected PermissionDenied, got {io_err}"
                );
                assert_eq!(
                    path, &locked,
                    "error path should point to the locked directory"
                );
            }
            other => panic!("unexpected error variant: {other:?}"),
        }

        let names: Vec<_> = entries
            .iter()
            .map(|e| e.path().file_name().unwrap().to_string_lossy().into_owned())
            .collect();
        assert!(names.contains(&"accessible.txt".to_string()));
        assert!(names.contains(&"locked".to_string()));
        assert!(
            !names.contains(&"secret.txt".to_string()),
            "secret.txt should not be reachable"
        );
    }

    {
        let results: Vec<_> = WalkDir::new(&root)
            .expect("failed to create walker")
            .ignore_permission_denied(true)
            .collect();

        let errors: Vec<_> = results.iter().filter(|r| r.is_err()).collect();
        assert!(errors.is_empty(), "expected no errors, got: {errors:?}");

        println!("ignore_permission_denied true");
        println!("{:#?}", errors);

        let names: Vec<_> = results
            .iter()
            .filter_map(|r| r.as_ref().ok())
            .map(|e| e.path().file_name().unwrap().to_string_lossy().into_owned())
            .collect();

        assert!(names.contains(&"accessible.txt".to_string()));
        assert!(names.contains(&"locked".to_string()));
        assert!(
            !names.contains(&"secret.txt".to_string()),
            "secret.txt should not be reachable"
        );
    }
}

#[test]
fn walkdir_filter_works() {
    println!("\nFilter Works:");

    let tmp = create_temp_dir("walkdir_minimal_filter");
    fs::create_dir_all(tmp.join("a")).unwrap();
    fs::create_dir_all(tmp.join("b_ignore")).unwrap();
    File::create(tmp.join("a/file1.txt")).unwrap();
    File::create(tmp.join("b_ignore/file2.txt")).unwrap();

    let walker = WalkDir::new(&tmp)
        .unwrap()
        .filter_entry(|e| !e.path().to_string_lossy().contains("ignore"));

    let mut files = Vec::new();
    for entry in walker {
        let e = entry.unwrap();
        println!("{}", e.path().display());
        files.push(e.path().to_path_buf());
    }

    assert!(files.iter().any(|p| p.ends_with("file1.txt")));
    assert!(!files.iter().any(|p| p.ends_with("file2.txt")));
}

#[test]
fn walkdir_follow_symlinks() {
    println!("\nFollow symlinks:");

    let tmp = create_temp_dir("walkdir_minimal_symlinks");
    fs::create_dir_all(tmp.join("target")).unwrap();
    File::create(tmp.join("target/file.txt")).unwrap();

    let link_path = tmp.join("link_to_target");
    symlink(tmp.join("target"), &link_path).unwrap();

    let walker = WalkDir::new(&tmp)
        .unwrap()
        .follow_links(true)
        .detect_loops(false);

    let mut paths = Vec::new();
    for entry in walker {
        let e = entry.unwrap();
        println!("{}", e.path().display());
        paths.push(e.path().to_path_buf());
    }

    assert!(paths.iter().any(|p| p.ends_with("file.txt")));
}

#[test]
fn walkdir_loop_detection() {
    println!("\nLoop detection:");

    let tmp = create_temp_dir("walkdir_minimal_loops");
    fs::create_dir_all(tmp.join("a")).unwrap();
    fs::create_dir_all(tmp.join("a/b")).unwrap();

    symlink(tmp.join("a"), tmp.join("a/b/link_back")).unwrap();

    let walker = WalkDir::new(&tmp)
        .unwrap()
        .follow_links(true)
        .detect_loops(true);

    let mut visited = 0;
    let mut loop_detected = false;
    for entry in walker {
        if let Err(WalkError::LoopDetected(_)) = entry {
            println!("Loop detected");
            loop_detected = true;
            break;
        }
        println!("visited {}", entry.unwrap().path().display());
        visited += 1;
    }

    assert!(visited < 10);
    assert!(
        loop_detected,
        "Deveria ter detectado um loop infinito de links simbólicos"
    );
}

#[test]
fn walkdir_handles_large_dir() {
    println!("\nHandle Large Dir:");

    let tmp = create_temp_dir("walkdir_minimal_large");
    for i in 0..50 {
        let mut f = File::create(tmp.join(format!("file_{i}.txt"))).unwrap();
        writeln!(f, "conteúdo {i}").unwrap();
    }

    let count = WalkDir::new(&tmp).unwrap().count();
    println!("Found {} files", count);
    assert_eq!(count, 50);
}

#[test]
fn walkdir_ignores_broken_symlinks() {
    println!("\nBroken and valid symlinks test:");

    let tmp = create_temp_dir("walkdir_minimal_broken_link");
    let real_file = tmp.join("file.txt");
    let real_dir = tmp.join("dir");
    fs::write(&real_file, "hello").unwrap();
    fs::create_dir(&real_dir).unwrap();
    let valid_file_link = tmp.join("link_to_file");
    let _ = symlink(&real_file, &valid_file_link);
    let valid_dir_link = tmp.join("link_to_dir");
    let _ = symlink(&real_dir, &valid_dir_link);
    let broken_link = tmp.join("broken_link");
    let _ = symlink("/nonexistent/path", &broken_link);

    let walker = WalkDir::new(&tmp).unwrap().follow_links(true);

    let mut found_valid_links = 0;
    let mut found_broken_links = 0;

    for item in walker {
        match item {
            Ok(e) => {
                let path = e.path();
                println!("Found: {}", path.display());
                assert_ne!(path, &broken_link);

                if path == valid_file_link || path == valid_dir_link {
                    found_valid_links += 1;
                }
            }
            Err(err) => match err {
                WalkError::Io(io_err, ..) => {
                    println!("IO error: {:?}", io_err);
                    found_broken_links += 1;
                }
                _ => {}
            },
        }
    }

    println!("Valid links: {found_valid_links}, Broken links: {found_broken_links}");
}

#[test]
fn walkdir_follow_symlinks_no_loop_detection() {
    println!("\nFollow symlinks no loop detection:");

    let tmp = env::temp_dir().join("walkdir_minimal_symlink_noloop_test");
    let _ = fs::remove_dir_all(&tmp);
    fs::create_dir_all(&tmp).unwrap();

    let dir_a = tmp.join("a");
    let dir_b = tmp.join("b");
    fs::create_dir(&dir_a).unwrap();
    fs::create_dir(&dir_b).unwrap();
    File::create(dir_a.join("file_a.txt")).unwrap();

    symlink(&dir_a, dir_b.join("link_to_a")).unwrap();
    symlink(&dir_b, dir_a.join("link_to_b")).unwrap();

    let walker = WalkDir::new(&tmp)
        .unwrap()
        .follow_links(true)
        .detect_loops(false)
        .max_depth(5);

    let mut count = 0;
    for entry in walker {
        let e = entry.unwrap();
        println!("Visited: {}", e.path().display());
        count += 1;
    }

    assert!(
        count > 2,
        "Expected to visit multiple paths when following symbolic links"
    );
}