path-rs
Cross-platform path expansion, normalization, resolution, traversal, searching, and caching for Rust.
path-rs complements std::path::{Path, PathBuf}. It does not replace them.
use ;
use ;
let expanded = expand_input?;
let clean = normalize?;
assert_eq!;
let _ = expanded;
Why this crate exists
CLI tools, build systems, deployers, and workspace managers repeatedly reimplement:
~and environment variable expansion- platform config/data/cache locations
- Windows drive / UNC / verbatim path quirks
- lexical normalization vs filesystem canonicalization
- safe-ish joining under a root
- directory listing and glob search
- optional discovery caching
path-rs provides one coherent, well-tested layer for those jobs while leaving filesystem identity to Path / PathBuf.
Path versus PathBuf
| Type | Role |
|---|---|
std::path::Path / PathBuf |
Native path representation and OS APIs |
path-rs |
Expansion, lexical ops, platform dirs, listing, search, cache |
Internal representation is always Path / PathBuf / OsStr — never String as a filesystem path.
Installation
[]
= "0.1"
Default features: listing, search.
Operation matrix
| Operation | Filesystem access | Requires existence | Resolves symlinks |
|---|---|---|---|
expand_input |
No* | No | No |
normalize |
No | No | No |
resolve_against |
No | No | No |
join_relative |
No | No | No |
resolve_inside |
No | No | No |
canonicalize_existing |
Yes | Yes | Yes |
list |
Yes | Root yes | Configurable |
search |
Yes | Root yes | Configurable |
* Home / platform directory resolution may consult environment variables and platform APIs.
Basic usage
use ;
let input = expand_input?;
let clean = normalize?;
// Lexical containment — not symlink-safe:
let inside = resolve_inside;
let _ = ;
Expansion
use ;
let opts = ExpandOptions ;
let path = expand_input?;
Supported forms:
~,~/path,~\path(leading component only; no~user)%VAR%(including%%escape)$VAR,${VAR}- Optional WSL:
/mnt/c/...→C:\...whentranslate_wsl_pathsis enabled
Never expands $(command), backticks, or shell syntax.
Undefined variables: strict mode errors; permissive mode leaves the token unchanged.
Normalization
use ;
// Lexical only — no I/O:
let n = normalize?;
// Filesystem — must exist, resolves symlinks:
let c = canonicalize_existing?;
Do not call canonicalize merely to “clean” a path.
Relative resolution and root containment
use ;
let a = resolve_against?;
let b = join_relative?; // rejects absolute children
let c = resolve_inside; // Err(RootEscape)
Security: lexical containment is not a symlink-safe boundary. See SECURITY.md.
Windows paths
Handles drive-absolute (C:\, C:/), rejects drive-relative (C:foo, C:) by default, preserves UNC and verbatim prefixes, and classifies device namespaces (\\.\).
use ;
use Path;
Windows reserved device names (CON, NUL, COM1, …) can be detected for app-name validation and policy checks.
Long path behavior follows the OS and process configuration; this crate does not invent MAX_PATH guarantees.
WSL paths
Translation is opt-in (ExpandOptions::translate_wsl_paths or translate_wsl_path).
Only /mnt/<single-letter-drive>/... is recognized. /mnt/data is not treated as a drive.
UTF-8 behavior
use ;
let s = path_to_utf8?;
let log = path_to_string_lossy; // logs/UI only
Never round-trip lossy UTF-8 back into filesystem operations.
With feature unicode, logical_path_key provides NFC comparison keys (not filesystem paths).
Listing
use ;
let entries = list?;
Defaults: no symlink following, no cache, hidden files excluded, fail-fast errors, deterministic path sort when enabled.
Glob searching
use ;
let hits = search?;
Patterns match paths relative to the search root. Use search_with for predicate-based filters.
Application roots
use ;
let paths = app_paths_with_options?;
// Product-specific subdirs (e.g. "mutations") stay in the application layer:
let _custom = paths.root_dir.join;
Never conflate the global application root with a repository root or the process CWD.
Path identity keys
use ;
let key = path_identity_key?;
// Comparison only — not a filesystem path.
Directory discovery
use ;
// Application supplies the domain predicate (e.g. "has a marker file"):
let roots = discover_where?;
Skip lists (node_modules, .git, …) are caller-configured, not hardcoded.
Caching
Caching is opt-in (CachePolicy::Bypass by default).
use ;
use Arc;
use Duration;
let cache = new;
let mut req = new;
req.cache = ReadThrough;
let hits = search_with_cache?;
Cache keys include root, patterns, and traversal options — not the root alone.
Feature persistent-cache enables a versioned on-disk cache under the platform cache directory.
Never use cache hits as a security boundary.
Security limitations
- Lexical normalization ≠ canonicalization
- Lexical root containment ≠ symlink-safe containment
- Cached results are not authoritative
- Environment expansion is untrusted input
- Glob/search can produce large result sets — set limits
See SECURITY.md.
Platform support
| Platform | Status |
|---|---|
| Windows | Supported |
| macOS | Supported |
| Linux | Supported |
| Other Unix-like | Best-effort via dirs / std |
CI runs on Ubuntu, macOS, and Windows.
Feature flags
| Feature | Default | Description |
|---|---|---|
listing |
yes | Directory listing (walkdir) |
search |
yes | Glob/predicate search (globset, implies listing) |
persistent-cache |
no | On-disk discovery cache |
unicode |
no | NFC logical path keys |
async |
no | spawn_blocking list/search wrappers |
MSRV
Rust 1.85 (edition 2024).
Examples
Contributing
See CONTRIBUTING.md.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.