File System Operations

Directory Traversal

Directory traversal walks a directory—either shallowly (listing immediate children via std::fs::read_dir) or recursively across nested subdirectories—to discover, filter, or aggregate files on disk.

Key Performance and Safety Rules for Recursive Directory Traversal

  1. Use entry.file_type() instead of entry.path().is_dir(): On most operating systems (readdir d_type on Linux/macOS, WIN32_FIND_DATA on Windows), the directory entry returned by read_dir already contains the file type in memory! Calling entry.file_type()? requires zero extra system calls, whereas calling entry.path().is_dir() issues an extra stat() syscall for every single file!
  2. Do not follow symbolic links into directories by default: Calling entry.file_type()?.is_dir() returns false for symbolic links (even symlinks pointing to directories!), which naturally prevents infinite recursion loops caused by symlink cycles (ln -s .. loop)! (In contrast, entry.path().is_dir() follows symlinks and will loop forever on a cyclic symlink!)
  3. Sort entries explicitly if deterministic order is required: Operating-system filesystems (APFS, ext4, NTFS) return directory entries in internal hash/btree order, not alphabetical order.
Why `entry.file_type()?.is_dir()` Beats `entry.path().is_dir()`:

  `entry.file_type()?.is_dir()`
    - Uses OS `readdir` `d_type` already in RAM (`0` extra `stat` syscalls!)
    - Returns `false` on symlinks -> Immune to infinite symlink loops!

  `entry.path().is_dir()`
    - Issues an extra OS `stat()` syscall on every entry!
    - Follows symlinks -> Vulnerable to infinite directory symlink cycles!

Implementing a Safe, Symlink-Cycle-Proof Recursive Directory Walker

use std::fs;
use std::io;
use std::path::{Path, PathBuf};

pub fn collect_files_recursively(root: &Path, extension_filter: &str) -> io::Result<Vec<PathBuf>> {
    let mut matched = Vec::new();
    let mut stack = vec![root.to_path_buf()];

    // Iterative stack-based DFS avoids call-stack overflow on deeply nested trees:
    while let Some(current_dir) = stack.pop() {
        for entry_res in fs::read_dir(&current_dir)? {
            let entry = entry_res?;
            // Zero-syscall file type check (does NOT follow symlinks):
            let file_type = entry.file_type()?;
            let path = entry.path();

            if file_type.is_dir() {
                stack.push(path);
            } else if file_type.is_file() {
                if path.extension().and_then(|e| e.to_str()) == Some(extension_filter) {
                    matched.push(path);
                }
            }
        }
    }

    // Sort paths so output order is 100% deterministic across filesystems:
    matched.sort();
    Ok(matched)
}

fn main() -> io::Result<()> {
    let base = std::env::temp_dir().join("rust_ch50_walk_demo");
    let sub = base.join("nested");
    fs::create_dir_all(&sub)?;

    fs::write(base.join("a.rs"), "// a")?;
    fs::write(base.join("notes.txt"), "ignore")?;
    fs::write(sub.join("b.rs"), "// b")?;

    let rs_files = collect_files_recursively(&base, "rs")?;
    for file in &rs_files {
        println!(
            "discovered: {:?}",
            file.strip_prefix(&base).unwrap()
        );
    }

    fs::remove_dir_all(&base)?;
    Ok(())
}
discovered: "a.rs"
discovered: "nested/b.rs"