luff 0.2.1

Print files with formatting
Documentation
# 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

| Attack            | Mitigation                                   | Function                  |
| ----------------- | -------------------------------------------- | ------------------------- |
| 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