# Filesystem Utils Module
**Purpose**: Security-hardened file operations with path validation and safe I/O.
## File Responsibilities
### path_ops.rs
- **open_file_safe()**: Secure file opening with O_NOFOLLOW and O_NONBLOCK
- **read_file_safe()**: Size-limited file reading with timeout protection
- **MAX_FILE_SIZE**: Configurable upper bound for file reads (100 MB)
### normalization.rs
- **normalize_path()**: Pure path normalization without filesystem access
- Returns `Cow::Borrowed` when no normalization needed (zero allocation)
- Idempotent, preserves absolute/relative distinction
### validation.rs
- **validate_path()**: Security checks (null bytes, control chars, length, UTF-8)
- **validate_path_with_env()**: Testable variant accepting `EnvProvider`
- Delegates reserved name checks to `platform.rs`
### platform.rs
- **canonicalize_safe()**: Symlink resolution with validation
- **is_reserved_name()**: Single source of truth for platform-reserved name detection
### security.rs
- **is_within_root()**: Path containment checking for traversal prevention (syntactic)
- **is_canonical_within_root()**: Optimized variant for pre-canonicalized paths
- **FileIdentity**: Unix (dev, ino) pair for file identity comparison
- **get_stdout_identity()**: Detects stdout redirection target via fstat
- **is_output_file()**: Two-tier detection (inode + time heuristic) for output files
- **output_protection_threshold()**: Configurable threshold for redirect detection
### glob.rs
- **GlobMatcher**: Thread-safe wrapper around `globset::GlobSet`
- **Compilation**: Compiles list of glob strings into efficient automata
- **Sharing**: Uses `Arc` internally to allow cheap cloning and sharing across threads
### timeout.rs
- **with_timeout()**: Executes a closure in a thread pool with configurable timeout
- **ThreadPool**: Static, bounded MPMC pool (crossbeam-channel) sized to 4× CPU count
- **get_timeout_duration()**: Reads `LUFF_READ_TIMEOUT_MS` env var (default 5000ms)
- **Scoped to fs_utils**: Exists to support `read_file_safe`, not a general utility
### mod.rs
- **Re-exports**: Public API surface
- **Module integration**: Ties together all submodules
## Critical Functions & Types
```rust
open_file_safe(path) -> Result<File>
// Checks: O_NOFOLLOW, O_NONBLOCK, is_file() (TOCTOU), size limit
read_file_safe(path) -> Result<String>
// Wraps open_file_safe in a thread pool with timeout
// Enforces MAX_FILE_SIZE and UTF-8 validity
normalize_path(path) -> Cow<'_, Path>
// Pure function, no I/O, collapses . and ..
// Returns Borrowed when already normalized
canonicalize_safe(path) -> io::Result<PathBuf>
// Requires existence, resolves symlinks, returns absolute path
validate_path(path) -> bool
// Fast checks: no null bytes, length < 4096, valid UTF-8
is_within_root(path, root) -> bool
// Prevents path traversal attacks via prefix checking (syntactic)
is_canonical_within_root(path, root) -> bool
// Prefix checking for pre-canonicalized paths (no normalization overhead)
with_timeout(f) -> io::Result<T>
// Executes closure in thread pool, returns timeout error if exceeded
// Closure returns T directly; only timeout injects io::Error
struct GlobMatcher
// Thread-safe matcher for .gitignore-style patterns
struct FileIdentity { dev, ino }
// Unix file identity for inode-based comparison
is_output_file(path, stdout_identity) -> bool
// Two-tier: inode comparison then time-based heuristic
```
## Security Model
### Protection Against
| Path traversal | Prefix checking after normalization | is_within_root() |
| Symlink attacks | O_NOFOLLOW + Validation after resolve | open_file_safe() |
| TOCTOU races | Re-check metadata.is_file() on fd | open_file_safe() |
| FIFO blocking | O_NONBLOCK on open + Thread pool timeout | read_file_safe() |
| Memory exhaustion | 100MB size limit (checked before read) | read_file_safe() |
| Injection | Null byte detection | validate_path() |
| DoS | Path length limit | validate_path() |
| Output loops | Inode match + time heuristic | is_output_file() |
| Control chars | Byte-level component scanning | validate_path() |
| Reserved names | Cross-platform reserved name rejection | is_reserved_name() |
### Constants
```rust
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100 MB
const MAX_PATH_LENGTH: usize = 4096; // characters
const MAX_COMPONENT_LENGTH: usize = 255; // per-component filesystem limit
const DEFAULT_TIMEOUT_MS: u64 = 5000; // thread pool timeout
```
## Key Invariants
- **read_file_safe()**: Executes in thread pool to prevent blocking, enforces size limits
- **open_file_safe()**: Validates file type and size using file descriptor metadata
- **normalize_path()**: Idempotent, no filesystem I/O, preserves absolute/relative distinction
- **canonicalize_safe()**: Result always exists, absolute, and symlinks resolved
- **validate_path()**: Never panics, returns false for suspicious input
- **is_within_root()**: Both paths normalized before comparison (syntactic only)
- **is_canonical_within_root()**: Debug-asserts both paths are absolute and canonical
- **is_reserved_name()**: Single definition in platform.rs, used by validation.rs
- **GlobMatcher**: Always thread-safe (Send + Sync), cheap to clone
- **FileIdentity**: Equality implies same physical file (within a single system)
## Performance Notes
- validate_path() is O(n) in path length, no I/O
- normalize_path() is O(components), zero allocation when already normalized
- is_reserved_name() uses ASCII case-insensitive comparison, no heap allocation
- read_file_safe() incurs thread switching overhead for safety
- canonicalize_safe() requires filesystem round-trip (slowest operation)
- GlobMatcher uses compiled DFAs for fast matching
- is_canonical_within_root() skips normalization for pre-canonicalized paths
## Extension Points
- Configurable MAX_FILE_SIZE via Config struct
- Configurable path/component limits via LUFF_MAX_PATH_LENGTH / LUFF_MAX_COMPONENT_LENGTH env vars
- Configurable output protection via LUFF_OUTPUT_PROTECTION_MS env var
- Async variants with tokio::fs for async runtime
- Platform-specific validation (is_reserved_name already cross-platform)
- Custom validation predicates via EnvProvider trait