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
- Use
entry.file_type()instead ofentry.path().is_dir(): On most operating systems (readdird_typeon Linux/macOS,WIN32_FIND_DATAon Windows), the directory entry returned byread_diralready contains the file type in memory! Callingentry.file_type()?requires zero extra system calls, whereas callingentry.path().is_dir()issues an extrastat()syscall for every single file! - Do not follow symbolic links into directories by default: Calling
entry.file_type()?.is_dir()returnsfalsefor 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!) - 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(¤t_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"