strict_path/lib.rs
1//! # strict-path
2//!
3//! Prevent directory traversal with cross-platform, CVE-hardened path restriction and safe symlinks.
4//!
5//! This crate is not a thin wrapper over `std::path` or a naive path comparison.
6//! It performs full normalization/canonicalization and boundary enforcement with:
7//! - Safe symlink/junction handling (including cycle detection)
8//! - Windows-specific quirks (8.3 short names, UNC and verbatim prefixes, ADS)
9//! - Robust Unicode normalization and mixed-separator handling across platforms
10//! - Canonicalized path proofs encoded in the type system
11//!
12//! If a `StrictPath<Marker>` value exists, it is already proven to be inside its
13//! designated boundary by construction — not by best-effort string checks.
14//!
15//! 📚 **[Complete Guide & Examples](https://dk26.github.io/strict-path-rs/)** | 📖 **[API Reference](https://docs.rs/strict-path)**
16//!
17//! ## Quick start: one‑liners
18//!
19//! Most apps can start with these constructors and chain joins:
20//!
21//! ```rust
22//! # use strict_path::{StrictPath, VirtualPath};
23//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! // Use temporary directories in doctests so paths exist
25//! let d1 = tempfile::tempdir()?;
26//! let sp: StrictPath = StrictPath::with_boundary(d1.path())? // validated strict root
27//! .strict_join("users/alice.txt")?; // stays inside root
28//!
29//! let d2 = tempfile::tempdir()?;
30//! let vp: VirtualPath = VirtualPath::with_root(d2.path())? // virtual root "/"
31//! .virtual_join("assets/logo.png")?; // clamped to root
32//! // Create the file before inspecting/removing it in the example
33//! sp.create_parent_dir_all()?;
34//! sp.write("hello")?;
35//! sp.metadata()?; // inspect filesystem metadata safely
36//! sp.remove_file()?; // remove files through the wrapper
37//! # Ok(()) }
38//! ```
39//!
40//! For reusable policy and advanced flows (OS dirs, serde with context),
41//! use `PathBoundary`/`VirtualRoot` directly.
42//!
43//! ## Core Security Foundation: `StrictPath`
44//!
45//! **`StrictPath` is the fundamental security primitive** that provides our core guarantee: every
46//! `StrictPath` is mathematically proven to be within its designated boundary. This is not just
47//! validation — it's a type-level security contract that makes path traversal attacks impossible,
48//! including attacks relying on symlink aliasing, Windows path forms, or encoding tricks.
49//!
50//! Everything else in this crate builds upon `StrictPath`:
51//! - `PathBoundary` creates and validates `StrictPath` instances from external input
52//! - `VirtualPath` extends `StrictPath` with user-friendly virtual root semantics
53//! - `VirtualRoot` provides a root context for creating `VirtualPath` instances
54//!
55//! **The security model:** If you have a `StrictPath<Marker>` in your code, it cannot reference
56//! anything outside its boundary - this is enforced by the type system and cryptographic-grade
57//! path canonicalization.
58//!
59//! ## Why naive approaches fail (and CVEs they miss)
60//!
61//! String checks and one-off normalizers don’t compose into a secure system. Common pitfalls:
62//!
63//! - Checking for "../" misses double-encodings, mixed separators, and absolute replacements.
64//! - Blind `canonicalize()` checks fail on non-existent files and enable TOCTOU races (e.g., symlink swaps) between resolution and use.
65//! - Lexical normalization ignores platform aliasing (Windows 8.3 short names), ADS streams, and UNC/verbatim quirks.
66//!
67//! Illustrative (simplified) examples — these compile to keep doctests honest, but the logic
68//! remains insecure on purpose:
69//!
70//! ```rust
71//! # fn naive_filter(candidate: &str) -> Result<(), &'static str> {
72//! if candidate.contains("../") {
73//! return Err("nope");
74//! }
75//! Ok(())
76//! # }
77//! // ❌ Rejecting only "../" is bypassable via encoding ("..%2F" sneaks by).
78//! assert!(naive_filter("..%2Fconfig").is_ok());
79//! ```
80//!
81//! ```rust
82//! # use std::path::Path;
83//! # use std::{fs, io};
84//! # fn canonicalize_then_check(candidate: &Path, root: &Path) -> io::Result<()> {
85//! let real = fs::canonicalize(candidate)?;
86//! if !real.starts_with(root) {
87//! return Err(io::Error::new(io::ErrorKind::Other, "escape"));
88//! }
89//! fs::read(real)?;
90//! Ok(())
91//! # }
92//! # fn demo() -> std::result::Result<(), Box<dyn std::error::Error>> {
93//! let sandbox = tempfile::tempdir()?;
94//! let root = sandbox.path();
95//! let candidate = root.join("example.txt");
96//! fs::write(&candidate, "data")?;
97//! // ❌ Canonicalize-then-check is subject to TOCTOU (CVE-2022-21658 class).
98//! let canonical_root = fs::canonicalize(root)?;
99//! canonicalize_then_check(&candidate, &canonical_root)?;
100//! Ok(())
101//! # }
102//! if let Err(issue) = demo() {
103//! panic!("{issue}");
104//! }
105//! ```
106//!
107//! ```rust
108//! # fn lexical_only(candidate: &str) -> bool {
109//! let norm = candidate.replace("\\", "/");
110//! norm.starts_with("/safe/")
111//! # }
112//! // ❌ Lexical only: misses Windows 8.3 short name aliasing (e.g., PROGRA~1).
113//! assert!(lexical_only("/safe/PROGRA~1"));
114//! ```
115//!
116//! strict‑path centralizes normalization, canonicalization, and boundary checks in a single auditable
117//! pipeline, with anchored canonicalization for virtual roots and explicit APIs that make the intended
118//! dimension (strict vs virtual) visible. The result is a type‑level guarantee: if a `StrictPath<Marker>`
119//! exists, it is proven to be within its boundary.
120//!
121//! ## Path Types and Their Relationships
122//!
123//! - **`StrictPath`**: The core security primitive - a validated, system-facing path that proves
124//! the wrapped filesystem path is within the predefined boundary. If a `StrictPath` exists,
125//! it is mathematical proof that the path is safe.
126//! - **`VirtualPath`**: Extends `StrictPath` with a virtual-root view (treating the PathBoundary
127//! as "/"), adding user-friendly operations while preserving all `StrictPath` security guarantees.
128//!
129//! ## Design Philosophy: PathBoundary as Foundation
130//!
131//! The `PathBoundary` represents the secure foundation or starting point from which all path operations begin.
132//! Think of it as establishing a safe boundary (like `/home/users/alice`) and then performing validated
133//! operations from that foundation. When you call `path_boundary.strict_join("documents/file.txt")`,
134//! you're building outward from the secure boundary with validated path construction.
135//!
136//! ## When to Use Which Type
137//!
138//! **Use `VirtualRoot`/`VirtualPath` for isolation and sandboxing:**
139//! - User uploads, per-user data directories, tenant-specific storage
140//! - Web applications serving user files, document management systems
141//! - Plugin systems, template engines, user-generated content
142//! - Any case where users should see a clean "/" root and not the real filesystem structure
143//!
144//! **Use `PathBoundary`/`StrictPath` for shared system spaces:**
145//! - Application configuration, shared caches, system logs
146//! - Temporary directories, build outputs, asset processing
147//! - Cases where you need the real system path for interoperability or debugging
148//! - When working with existing APIs that expect system paths
149//!
150//! Both types support I/O. The key difference is the user experience: `VirtualPath` provides isolation
151//! and clean virtual paths, while `StrictPath` maintains system path semantics for shared resources.
152//!
153//! ## 🔑 Critical Design Decision: StrictPath vs Path/PathBuf
154//!
155//! **The Key Principle: Use `StrictPath` when you DON'T control the path source**
156//!
157//! ```rust
158//! # use strict_path::{PathBoundary, StrictPath, VirtualRoot, VirtualPath};
159//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
160//! // ✅ USE StrictPath - External/untrusted input (you don't control the source)
161//! // Encode guarantees in the signature: pass the boundary and the untrusted segment
162//! fn handle_user_config(boundary: &PathBoundary, config_name: &str) -> Result<(), Box<dyn std::error::Error>> {
163//! let config_path: StrictPath = boundary.strict_join(config_name)?; // Validate!
164//! let _content = config_path.read_to_string()?;
165//! Ok(())
166//! }
167//!
168//! // ✅ USE VirtualRoot - External/untrusted input for user-facing paths
169//! // Encode guarantees in the signature: pass the virtual root and the untrusted segment
170//! fn process_upload(uploads: &VirtualRoot, user_filename: &str) -> Result<(), Box<dyn std::error::Error>> {
171//! let safe_file: VirtualPath = uploads.virtual_join(user_filename)?; // Sandbox!
172//! safe_file.write(b"data")?;
173//! Ok(())
174//! }
175//!
176//! // ✅ USE Path/PathBuf - Internal/controlled paths (you generate the path)
177//! fn create_backup() -> std::path::PathBuf {
178//! use std::path::PathBuf;
179//! let timestamp = "20240101_120000"; // Simulated timestamp
180//! PathBuf::from(format!("backups/backup_{}.sql", timestamp)) // You control this
181//! }
182//!
183//! fn get_log_file() -> &'static std::path::Path {
184//! std::path::Path::new("/var/log/myapp/app.log") // Hardcoded, you control this
185//! }
186//! # Ok(()) }
187//! ```
188//!
189//! **Decision Matrix:**
190//! - **External Input** (config files, CLI args, API requests, user uploads) → `StrictPath`/`VirtualPath`
191//! - **Internal Generation** (timestamps, IDs, hardcoded paths, system APIs) → `Path`/`PathBuf`
192//! - **Unknown Origin** → `StrictPath`/`VirtualPath` (err on the side of security)
193//! - **Performance Critical + Trusted** → `Path`/`PathBuf` (avoid validation overhead)
194//!
195//! This principle ensures security where it matters while avoiding unnecessary overhead for paths you generate and control.
196//!
197//! ### Analogy: Prepared statements for paths
198//!
199//! Think of `StrictPath`/`VirtualPath` like prepared statements for SQL:
200//! - The `PathBoundary`/`VirtualRoot` you construct is the prepared statement — it encodes the policy and allowable scope.
201//! - The untrusted filename/path segment is the bound parameter — passed into `strict_join`/`virtual_join` where it’s validated or clamped.
202//! - Injection attempts become inert — attackers can’t “change the query” or escape the boundary; inputs are treated as data, not structure.
203//!
204//! ### Example: Isolation vs Shared System Space
205//!
206//! ```rust
207//! use strict_path::{StrictPath, VirtualPath};
208//!
209//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
210//! // ISOLATION: User upload directory - users see clean "/" paths
211//! // Note: `.with_root()` requires the directory to already exist. Use
212//! // `.with_root_create()` if you want it to be created automatically.
213//! # std::fs::create_dir_all("uploads/user_42")?; // hidden setup
214//! let user_root: VirtualPath = VirtualPath::with_root("uploads/user_42")?;
215//! let user_file: VirtualPath = user_root.virtual_join("documents/report.pdf")?;
216//!
217//! // User sees: "/documents/report.pdf" (clean, isolated)
218//! println!("User sees: {}", user_file.virtualpath_display());
219//! user_file.create_parent_dir_all()?;
220//! user_file.write(b"user content")?;
221//!
222//! // SHARED SYSTEM: Application cache - you see real system paths
223//! // Note: `.with_boundary()` requires an existing directory. Prefer
224//! // `.with_boundary_create()` to auto-create the boundary as needed.
225//! # std::fs::create_dir_all("app_cache")?; // hidden setup
226//! let cache_root: StrictPath = StrictPath::with_boundary("app_cache")?;
227//! let cache_file: StrictPath = cache_root.strict_join("build/output.json")?;
228//!
229//! // Developer sees: "app_cache/build/output.json" (real system path)
230//! println!("System path: {}", cache_file.strictpath_display());
231//! cache_file.create_parent_dir_all()?;
232//! cache_file.write(b"cache data")?;
233//!
234//! # user_root.remove_dir_all().ok(); cache_root.remove_dir_all().ok();
235//! # Ok(()) }
236//! ```
237//!
238//! ## Filter vs Sandbox: Conceptual Difference
239//!
240//! **`StrictPath` acts like a security filter** - it validates that a specific path is safe and
241//! within boundaries, but operates on actual filesystem paths. Perfect for **shared system spaces**
242//! where you need safety while maintaining system-level path semantics (logs, configs, caches).
243//!
244//! **`VirtualPath` acts like a complete sandbox** - it encapsulates the filtering (via the underlying
245//! `StrictPath`) while presenting a virtualized, user-friendly view where the PathBoundary root appears as "/".
246//! Users can specify any path they want, and it gets automatically clamped to stay safe. Perfect for
247//! **isolation scenarios** where you want to hide the underlying filesystem structure from users
248//! (uploads, per-user directories, tenant storage).
249//!
250//! ## Unified Signatures (Explicit Borrow)
251//!
252//! Prefer marker-specific signatures that accept `&StrictPath<Marker>` and borrow strict view with `as_unvirtual()`.
253//! This keeps conversions explicit and avoids vague conversions.
254//!
255//!
256//! ```rust
257//! use strict_path::{StrictPath, VirtualPath};
258//!
259//! // Write ONE function that works with both types
260//! fn process_file(path: &StrictPath) -> std::io::Result<String> {
261//! path.read_to_string()
262//! }
263//!
264//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
265//! let jpath: StrictPath = StrictPath::with_boundary("./data")?.strict_join("config.toml")?;
266//! let vpath: VirtualPath = VirtualPath::with_root("./data")?.virtual_join("config.toml")?;
267//!
268//! let _ = process_file(&jpath)?; // StrictPath
269//! process_file(vpath.as_unvirtual())?; // VirtualPath -> borrow strict view explicitly
270//! # Ok(()) }
271//! ```
272//!
273//! This keeps conversions explicit by dimension and aligns with the crate's security model.
274//!
275//! The core security guarantee is that all paths are mathematically proven to stay within their
276//! designated boundaries, neutralizing traversal attacks like `../../../etc/passwd`.
277//!
278//! ## Type-System Guarantees in Function Signatures
279//!
280//! Use marker types to encode policy directly in your APIs. Callers must supply the right
281//! `StrictPath<Marker>` or the code simply won’t compile.
282//!
283//! ```rust
284//! # use strict_path::{PathBoundary, StrictPath};
285//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
286//! // Define semantic markers
287//! struct PublicAssets;
288//! struct UserUploads;
289//!
290//! // Policy roots
291//! # std::fs::create_dir_all("./assets")?;
292//! # std::fs::create_dir_all("./uploads")?;
293//! let assets = PathBoundary::<PublicAssets>::try_new("./assets")?;
294//! let uploads = PathBoundary::<UserUploads>::try_new("./uploads")?;
295//!
296//! // Safe paths — existence itself proves they’re inside their boundary
297//! let css: StrictPath<PublicAssets> = assets.strict_join("style.css")?;
298//! let avatar: StrictPath<UserUploads> = uploads.strict_join("avatar.jpg")?;
299//!
300//! // Encode guarantees in the signature
301//! fn serve_public_asset(file: &StrictPath<PublicAssets>) { /* ... */ }
302//!
303//! serve_public_asset(&css); // ✅ OK
304//! // serve_public_asset(&avatar); // ❌ Compile error (wrong marker)
305//! # std::fs::remove_dir_all("./assets").ok();
306//! # std::fs::remove_dir_all("./uploads").ok();
307//! # Ok(()) }
308//! ```
309//!
310//! Contracts like these push policy to the type system: if a `StrictPath<Marker>` exists,
311//! it cannot reference anything outside the associated boundary.
312//!
313//! ## About This Crate: StrictPath and VirtualPath
314//!
315//! `StrictPath` is a system-facing filesystem path type, mathematically proven (via
316//! canonicalization, boundary checks, and type-state) to remain inside a configured PathBoundary directory.
317//! `VirtualPath` wraps a `StrictPath` and therefore guarantees everything a `StrictPath` guarantees -
318//! plus a rooted, forward-slashed virtual view (treating the PathBoundary as "/") and safe virtual
319//! operations (joins/parents/file-name/ext) that preserve clamping and hide the real system path.
320//! With `VirtualPath`, users are free to specify any path they like while you still guarantee it
321//! cannot leak outside the underlying restriction.
322//!
323//! Construct them via the sugar constructors (`StrictPath::with_boundary(_create)`,
324//! `VirtualPath::with_root(_create)`) for most flows. Use `PathBoundary`/`VirtualRoot` directly when
325//! you need to reuse policy across many paths or pass the policy as a parameter. Ingest untrusted
326//! paths as `VirtualPath` for UI/UX and safe joins; perform I/O from either type.
327//!
328//! ## Security Foundation
329//!
330//! Built on [`soft-canonicalize`](https://crates.io/crates/soft-canonicalize), this crate inherits
331//! protection against documented CVEs including:
332//! - **CVE-2025-8088** (NTFS ADS path traversal), **CVE-2022-21658** (TOCTOU attacks)
333//! - **CVE-2019-9855, CVE-2020-12279** and others (Windows 8.3 short name vulnerabilities)
334//! - Path traversal, symlink attacks, Unicode normalization bypasses, and race conditions
335//!
336//! This isn't simple string comparison-paths are fully canonicalized and boundary-checked
337//! against known attack patterns from real-world vulnerabilities.
338//!
339//! Guidance
340//! - Accept untrusted input via `VirtualPath::with_root(..).virtual_join(..)` (or keep a `VirtualRoot`
341//! and call `virtual_join(..)`) to obtain a `VirtualPath`.
342//! - Perform I/O directly on `VirtualPath` or on `StrictPath`. Unvirtualize only when you need a
343//! `StrictPath` explicitly (e.g., for a signature that requires it or for system-facing logs).
344//! - For `AsRef<Path>` interop, pass `interop_path()` from either type (no allocation).
345//!
346//! Switching views (upgrade/downgrade)
347//! - Prefer staying in one dimension for a given flow:
348//! - Virtual view: `VirtualPath` + `virtualpath_*` ops and direct I/O.
349//! - System view: `StrictPath` + `StrictPath_*` ops and direct I/O.
350//! - Edge cases: upgrade with `StrictPath::virtualize()` or downgrade with `VirtualPath::unvirtual()`
351//! to access the other view's operations explicitly.
352//!
353//! Markers and type inference
354//! - All public types are generic over a `Marker` with a default of `()`.
355//! - Inference usually works once a value is bound:
356//! - `let vp: VirtualPath = VirtualPath::with_root("root")?.virtual_join("a.txt")?;`
357//! - When inference needs help, annotate the type or use an empty turbofish:
358//! - Or use explicit `VirtualRoot` when you want to reuse policy across paths: `let vroot: VirtualRoot<()> = VirtualRoot::try_new("root")?;`
359//! - With custom markers, annotate as needed:
360//! - `struct UserFiles; let vroot: VirtualRoot<UserFiles> = VirtualRoot::try_new("uploads")?;`
361//! - `let uploads = VirtualRoot::try_new::<UserFiles>("uploads")?;`
362
363//! ### Examples: Encode Guarantees in Signatures
364//!
365//! ```rust
366//! # use strict_path::VirtualPath;
367//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
368//! // Cloud storage per-user PathBoundary
369//! let user_id = 42u32;
370//! let root = format!("./cloud_user_{user_id}");
371//! let vp_root: VirtualPath = VirtualPath::with_root_create(&root)?;
372//!
373//! // Accept untrusted input, then pass VirtualPath by reference to functions
374//! let requested = "projects/2025/report.pdf";
375//! let vp: VirtualPath = vp_root.virtual_join(requested)?; // Stays inside ./cloud_user_42
376//! // Ensure parent directory exists before writing
377//! vp.create_parent_dir_all()?;
378//!
379//! fn save_doc(p: &VirtualPath) -> std::io::Result<()> { p.write(b"user file content") }
380//! save_doc(&vp)?; // Compiler enforces correct usage via the type
381//! println!("virtual: {}", vp.virtualpath_display());
382//!
383//! # // Cleanup
384//! # vp_root.remove_dir_all().ok();
385//! # Ok(()) }
386//! ```
387//!
388//! ```rust
389//! # use strict_path::VirtualPath;
390//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
391//! // Web/E-mail templates resolved in a user-scoped virtual root
392//! # let user_id = 7u32;
393//! let tpl_root = format!("./tpl_space_{user_id}");
394//! let templates: VirtualPath = VirtualPath::with_root_create(&tpl_root)?;
395//! let tpl: VirtualPath = templates.virtual_join("emails/welcome.html")?;
396//! fn render(p: &VirtualPath) -> std::io::Result<String> { p.read_to_string() }
397//! let _ = render(&tpl);
398//!
399//! # templates.remove_dir_all().ok();
400//! # Ok(()) }
401//! ```
402//!
403//! ## Quickstart: User-Facing Virtual Paths (with signatures)
404//!
405//! ```rust
406//! use strict_path::VirtualPath;
407//!
408//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
409//! // 1. Create a virtual root (sugar), which corresponds to a real directory.
410//! let root = VirtualPath::with_root_create("user_data")?;
411//!
412//! // 2. Create a virtual path from user input. Traversal attacks are neutralized.
413//! let virtual_path: VirtualPath = root.virtual_join("documents/report.pdf")?;
414//! let attack_path: VirtualPath = root.virtual_join("../../../etc/hosts")?;
415//!
416//! // 3. Displaying the path is always safe and shows the virtual view.
417//! assert_eq!(virtual_path.virtualpath_display().to_string(), "/documents/report.pdf");
418//! assert_eq!(attack_path.virtualpath_display().to_string(), "/etc/hosts"); // Clamped, not escaped
419//!
420//! // 4. Prefer signatures requiring `VirtualPath` for operations.
421//! fn ensure_dir(p: &VirtualPath) -> std::io::Result<()> { p.create_dir_all() }
422//! ensure_dir(&virtual_path)?;
423//! assert!(virtual_path.exists());
424//!
425//! root.remove_dir_all()?;
426//! # Ok(())
427//! # }
428//! ```
429//!
430//! ## Key Features
431//!
432//! - Two Views: `VirtualPath` extends `StrictPath` with a virtual-root UX; both support I/O.
433//! - Mathematical Guarantees: Rust's type system proves security at compile time.
434//! - Zero Attack Surface: No `Deref` to `Path`, validation cannot be bypassed.
435//! - Built-in Safe I/O: `StrictPath` provides safe file operations.
436//! - Multi-PathBoundary Safety: Marker types prevent cross-PathBoundary contamination at compile time.
437//! - Type-History Design: Internal pattern ensures paths carry proof of validation stages.
438//! - Cross-Platform: Works on Windows, macOS, and Linux.
439//!
440//! Display/Debug semantics
441//! - No implicit `Display` on `VirtualPath`. Use the explicit wrapper: `vpath.virtualpath_display()`
442//! to show a rooted, forward‑slashed virtual path (e.g., "/a/b.txt").
443//! - `Debug` for `VirtualPath` is developer‑facing and verbose (derived): it includes the inner
444//! `StrictPath` (system path and PathBoundary root) and the virtual view for diagnostics.
445//!
446//! ### Example: Display vs Debug
447//! ```rust
448//! # use strict_path::{VirtualRoot, VirtualPath};
449//!
450//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
451//! let vp_root: VirtualPath = VirtualPath::with_root_create("vp_demo")?;
452//! let vp: VirtualPath = vp_root.virtual_join("users/alice/report.txt")?;
453//!
454//! // Display is user-facing, rooted, forward-slashed
455//! assert_eq!(vp.virtualpath_display().to_string(), "/users/alice/report.txt");
456//!
457//! // Debug is developer-facing and verbose
458//! let dbg = format!("{:?}", vp);
459//! assert!(dbg.contains("VirtualPath"));
460//! assert!(dbg.contains("system_path"));
461//! assert!(dbg.contains("virtual"));
462//!
463//! # vp_root.remove_dir_all().ok();
464//! # Ok(()) }
465//! ```
466//!
467//! ## When to Use Which Type
468//!
469//! | Use Case | Type | Example |
470//! | -------------------------------------- | -------------------------- | ----------------------------------------------------------- |
471//! | Displaying a path in a UI or log | `VirtualPath` | `println!("File: {}", virtual_path.virtualpath_display());` |
472//! | Manipulating a path based on user view | `VirtualPath` | `virtual_path.virtualpath_parent()` |
473//! | Reading or writing a file | `VirtualPath` or `StrictPath` | `virtual_path.read()?` or `strict_path.read()?` |
474//! | Integrating with an external API | Either (borrow `&OsStr`) | `external_api(virtual_path.interop_path())` |
475//!
476//! ## Multi-PathBoundary Type Safety
477//!
478//! Use marker types to prevent paths from different restrictions from being used interchangeably.
479//!
480//! ```rust
481//! use strict_path::{PathBoundary, StrictPath, VirtualPath};
482//!
483//! struct StaticAssets;
484//! struct UserUploads;
485//!
486//! fn serve_asset(asset: &StrictPath<StaticAssets>) -> Result<Vec<u8>, std::io::Error> {
487//! asset.read()
488//! }
489//!
490//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
491//! # let css_root: VirtualPath<StaticAssets> = VirtualPath::with_root_create("assets")?;
492//! # let css_pre: VirtualPath<StaticAssets> = css_root.virtual_join("style.css")?;
493//! # css_pre.create_parent_dir_all()?; css_pre.write("body{}")?;
494//! # let uploads_root: VirtualPath<UserUploads> = VirtualPath::with_root_create("uploads")?;
495//! let css_file: VirtualPath<StaticAssets> =
496//! VirtualPath::with_root("assets")?.virtual_join("style.css")?;
497//! let avatar_file: VirtualPath<UserUploads> =
498//! VirtualPath::with_root("uploads")?.virtual_join("avatar.jpg")?;
499//!
500//! serve_asset(css_file.as_unvirtual())?; // ✅ Correct type
501//! // serve_asset(avatar_file.as_unvirtual())?; // ❌ Compile error: wrong marker type!
502//! # css_root.remove_dir_all().ok(); uploads_root.remove_dir_all().ok();
503//! # Ok(())
504//! # }
505//! ```
506//!
507//! ## Security Guarantees
508//!
509//! All `..` components are clamped, symbolic links are resolved, and the final real path is
510//! validated against the PathBoundary boundary. Path traversal attacks are prevented by construction.
511//!
512//! ## Security Limitations
513//!
514//! This library operates at the **path level**, not the operating system level. While it provides
515//! strong protection against path traversal attacks using symlinks and standard directory
516//! navigation, it **cannot protect against** certain privileged operations:
517//!
518//! - **Hard Links**: If a file is hard-linked outside the restricted path, accessing it through the
519//! PathBoundary will still reach the original file data. Hard links create multiple filesystem entries
520//! pointing to the same inode.
521//! - **Mount Points**: If a filesystem mount is introduced (by a system administrator or attacker
522//! with sufficient privileges) that redirects a path within the PathBoundary to an external location,
523//! this library cannot detect or prevent access through that mount.
524//!
525//! **Important**: These attack vectors require **high system privileges** (typically
526//! root/administrator access) to execute. If an attacker has such privileges on your system, they
527//! can bypass most application-level security measures anyway. This library effectively protects
528//! against the much more common and practical symlink-based traversal attacks that don't require
529//! special privileges.
530//!
531//! Our symlink resolution via [`soft-canonicalize`](https://crates.io/crates/soft-canonicalize)
532//! handles the most accessible attack vectors that malicious users can create without elevated
533//! system access.
534//!
535//! ### Windows-only hardening: DOS 8.3 short names
536//!
537//! On Windows, paths like `PROGRA~1` are DOS 8.3 short-name aliases. To prevent ambiguity,
538//! this crate rejects paths containing non-existent components that look like 8.3 short names
539//! with a dedicated error, `StrictPathError::WindowsShortName`.
540//!
541//! ## Why We Don't Expose `Path`/`PathBuf`
542//!
543//! Exposing raw `Path` or `PathBuf` encourages use of std path methods (`join`, `parent`, ...)
544//! that bypass this crate's virtual-root clamping and boundary checks.
545//!
546//! - `join` danger: `std::path::Path::join` has no notion of a virtual root. Joining an
547//! absolute path, or a path with enough `..` components, can override or conceptually
548//! escape the intended root. That undermines the guarantees of `StrictPath`/`VirtualPath`.
549//! **Critical:** `std::path::Path::join("/absolute")` completely replaces the base path,
550//! making it the #1 cause of path traversal vulnerabilities. Our `strict_join` validates
551//! the result stays within PathBoundary bounds, while `virtual_join` clamps absolute paths
552//! to the virtual root.
553//! Use `StrictPath::strict_join(...)` or `VirtualPath::virtual_join(...)` instead.
554//! - `parent` ambiguity: `Path::parent` ignores PathBoundary/virtual semantics; our
555//! `strictpath_parent()` and `virtualpath_parent()` preserve the correct behavior.
556//! - Predictability: Users unfamiliar with the crate may accidentally mix virtual and
557//! system semantics if they are handed a raw `Path`.
558//!
559//! What to use instead:
560//! - Passing to external APIs: Prefer `strict_path.interop_path()` which borrows the
561//! inner system-facing path as `&OsStr` (implements `AsRef<Path>`). This is the cheapest and most
562//! correct way to interoperate without exposing risky methods.
563//! - Ownership escape hatches: Use `.unvirtual()` (to get a `StrictPath`) or `.unstrict()`
564//! (to get an owned `PathBuf`) explicitly and sparingly. These are deliberate, opt-in
565//! operations to make potential risk obvious in code review.
566//!
567//! Explicit method names (rationale)
568//! - Operation names encode their dimension so intent is obvious:
569//! - `p.join(..)` (std) - unsafe on untrusted input; can escape the restriction.
570//! - `jp.strict_join(..)` - safe, validated system-path join.
571//! - `vp.virtual_join(..)` - safe, clamped virtual-path join.
572//! - This naming applies broadly: `*_parent`, `*_with_file_name`, `*_with_extension`,
573//! `*_starts_with`, `*_ends_with`, etc.
574//! - This makes API abuse easy to spot even when type declarations aren't visible.
575//!
576//! Safe rename/move
577//! ```rust
578//! # use strict_path::PathBoundary;
579//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
580//! // Strict (system-facing): validate destination via strict_join, then rename
581//! let td = tempfile::tempdir()?;
582//! let boundary: PathBoundary = PathBoundary::try_new_create(td.path())?;
583//! let file = boundary.strict_join("logs/app.log")?;
584//! file.create_parent_dir_all()?;
585//! file.write("ok")?;
586//!
587//! // Rename within the same directory (no implicit parent creation)
588//! // Relative destinations are resolved against the parent (sibling rename)
589//! file.strict_rename("app.old")?;
590//! let renamed = boundary.strict_join("logs/app.old")?;
591//! assert_eq!(renamed.read_to_string()?, "ok");
592//!
593//! // Virtual (user-facing): clamp + validate destination before rename
594//! let v = renamed.clone().virtualize();
595//! v.virtual_rename("app.archived")?;
596//! let v2 = boundary.strict_join("logs/app.archived")?.virtualize();
597//! assert!(v2.exists());
598//! # Ok(()) }
599//! ```
600//!
601//! Safe copy
602//! ```rust
603//! # use strict_path::PathBoundary;
604//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
605//! // Strict (system-facing): copy a file to a sibling name (no implicit parent creation)
606//! let td = tempfile::tempdir()?;
607//! let boundary: PathBoundary = PathBoundary::try_new_create(td.path())?;
608//! let src = boundary.strict_join("docs/a.txt")?;
609//! src.create_parent_dir_all()?;
610//! src.write("copy me")?;
611//!
612//! let bytes = src.strict_copy("b.txt")?; // resolved against parent directory
613//! assert_eq!(bytes, "copy me".len() as u64);
614//! let dst = boundary.strict_join("docs/b.txt")?;
615//! assert_eq!(dst.read_to_string()?, "copy me");
616//!
617//! // Virtual (user-facing): clamp + validate destination before copy
618//! let v = dst.clone().virtualize();
619//! let bytes = v.virtual_copy("c.txt")?; // sibling within the same virtual parent
620//! assert_eq!(bytes, "copy me".len() as u64);
621//! let vcopy = boundary.strict_join("docs/c.txt")?.virtualize();
622//! assert!(vcopy.exists());
623//! assert_eq!(vcopy.read_to_string()?, "copy me");
624//! # Ok(()) }
625//! ```
626//!
627//! Why `&OsStr` works well:
628//! - `OsStr`/`OsString` are OS-native string types; you don't lose platform-specific data.
629//! - `Path` is just a thin wrapper over `OsStr`. Borrowing `&OsStr` is the straightest,
630//! allocation-free, and semantically correct way to pass a path to `AsRef<Path>` APIs.
631//!
632//! ## Common Pitfalls (and How to Avoid Them)
633//!
634//! - **NEVER wrap our secure types in `Path::new()` or `PathBuf::from()`**.
635//! This is a critical anti-pattern that bypasses all security guarantees.
636//! ```rust
637//! # use strict_path::*;
638//! fn some_external_api<P: AsRef<std::path::Path>>(_path: P) {}
639//! fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
640//! let restriction = PathBoundary::<()>::try_new(".")?;
641//! let safe_path = restriction.strict_join("file.txt")?;
642//! // ❌ DANGEROUS: Wrapping secure types defeats the purpose
643//! let _dangerous = std::path::Path::new(safe_path.interop_path());
644//! let _also_bad = std::path::PathBuf::from(safe_path.interop_path());
645//!
646//! // ✅ CORRECT: Use interop_path() directly for external APIs
647//! some_external_api(safe_path.interop_path()); // AsRef<Path> satisfied
648//!
649//! // ✅ CORRECT: Use our secure operations
650//! let _child = safe_path.strict_join("subfile.txt")?;
651//! Ok(())
652//! }
653//! if let Err(issue) = run() {
654//! panic!("{issue}");
655//! }
656//! ```
657//! - **NEVER use `.interop_path().to_string_lossy()` for display purposes**.
658//! This mixes interop concerns with display concerns. Use proper display methods:
659//! ```rust
660//! # use strict_path::*;
661//! fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
662//! let restriction = PathBoundary::<()>::try_new(".")?;
663//! let safe_path = restriction.strict_join("file.txt")?;
664//! // ❌ ANTI-PATTERN: Wrong method for display
665//! println!("{}", safe_path.interop_path().to_string_lossy());
666//!
667//! // ✅ CORRECT: Use proper display methods
668//! println!("{}", safe_path.strictpath_display());
669//! Ok(())
670//! }
671//! if let Err(issue) = run() {
672//! panic!("{issue}");
673//! }
674//! ```
675//!
676//! ### Tell‑offs and fixes
677//! - Validating only constants → validate real external segments (HTTP/DB/manifest/archive entries); use `boundary.interop_path()` for root discovery.
678//! - Constructing boundaries/roots inside helpers → accept `&PathBoundary`/`&VirtualRoot` and the untrusted segment, or a `&StrictPath`/`&VirtualPath`.
679//! - Wrapping secure types (`Path::new(sp.interop_path())`) → pass `interop_path()` directly.
680//! - `interop_path().as_ref()` or `as_unvirtual().interop_path()` → `interop_path()` is enough; both `VirtualRoot`/`VirtualPath` expose it.
681//! - Using std path ops on leaked values → use `strict_join`/`virtual_join`, `strictpath_parent`/`virtualpath_parent`.
682//! - Raw `&str` parameters for safe helpers → take `&StrictPath<_>`/`&VirtualPath<_>` or (boundary/root + segment).
683//! - Do not leak raw `Path`/`PathBuf` from `StrictPath` or `VirtualPath`.
684//! Use `interop_path()` when an external API needs `AsRef<Path>`.
685//! - Do not call `Path::join`/`Path::parent` on leaked paths — they ignore PathBoundary/virtual semantics.
686//! Use `strict_join`/`strictpath_parent` and `virtual_join`/`virtualpath_parent`.
687//! - Avoid `.unvirtual()`/`.unstrict()` unless you explicitly need ownership for the specific type.
688//! Prefer borrowing with `interop_path()` for interop.
689//! - Virtual strings are rooted. For UI/logging, use `vp.virtualpath_display()` or `vp.virtualpath_display().to_string()`.
690//! No borrowed `&str` accessors are exposed for virtual paths.
691//! - Creating a restriction: `PathBoundary::try_new(..)` requires the directory to exist.
692//! Use `PathBoundary::try_new_create(..)` if it may be missing.
693//! - Windows: 8.3 short names (e.g., `PROGRA~1`) are rejected to avoid ambiguous resolution.
694//! - Markers matter. Functions should take `StrictPath<MyMarker>` for their domain to prevent cross-PathBoundary mixing.
695//!
696//! ## Escape Hatches and Best Practices
697//!
698//! Prefer passing references to the inner system path instead of taking ownership:
699//! - If an external API accepts `AsRef<Path>`, pass `strict_path.interop_path()`.
700//! - Avoid `.unstrict()` unless you explicitly need an owned `PathBuf`.
701//!
702//! ```rust
703//! # use strict_path::PathBoundary;
704//! # fn external_api<P: AsRef<std::path::Path>>(_p: P) {}
705//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
706//! let restriction = PathBoundary::try_new_create("./safe")?;
707//! let jp = restriction.strict_join("file.txt")?;
708//!
709//! // Preferred: borrow as &OsStr (implements AsRef<Path>)
710//! external_api(jp.interop_path());
711//!
712//! // Escape hatches (use sparingly):
713//! let owned: std::path::PathBuf = jp.clone().unstrict();
714//! let v: strict_path::VirtualPath = jp.clone().virtualize();
715//! let back: strict_path::StrictPath = v.clone().unvirtual();
716//! let owned_again: std::path::PathBuf = v.unvirtual().unstrict();
717//! # // Cleanup created PathBoundary directory for doctest hygiene
718//! # let root_cleanup: strict_path::StrictPath = strict_path::StrictPath::with_boundary("./safe")?;
719//! # root_cleanup.remove_dir_all().ok();
720//! # Ok(()) }
721//! ```
722//!
723//! ## API Reference (Concise)
724//!
725//! For a minimal, copy-pastable guide to the API (optimized for both humans and LLMs),
726//! see the repository reference:
727//! <https://github.com/DK26/strict-path-rs/blob/main/LLM_API_REFERENCE.md>
728//!
729//! This link is provided here so readers coming from docs.rs can easily discover it.
730#![forbid(unsafe_code)]
731
732pub mod error;
733pub mod path;
734pub mod validator;
735#[cfg(feature = "serde")]
736pub mod serde_ext {
737 //! Serde helpers and notes.
738 //!
739 //! Built‑in `Serialize` (feature `serde`):
740 //! - `StrictPath` → system path string
741 //! - `VirtualPath` → virtual root string (e.g., "/a/b.txt")
742 //!
743 //! Deserialization requires context (a `PathBoundary` or `VirtualRoot`). Use the context helpers
744 //! below to deserialize with context, or deserialize to `String` and validate explicitly.
745 //!
746 //! Example: Deserialize a single `StrictPath` with context
747 //! ```rust
748 //! use strict_path::{PathBoundary, StrictPath};
749 //! use strict_path::serde_ext::WithBoundary;
750 //! use serde::de::DeserializeSeed;
751 //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
752 //! # let td = tempfile::tempdir()?;
753 //! let boundary: PathBoundary = PathBoundary::try_new(td.path())?;
754 //! let mut de = serde_json::Deserializer::from_str("\"a/b.txt\"");
755 //! let jp: StrictPath = WithBoundary(&boundary).deserialize(&mut de)?;
756 //! // OS-agnostic assertion: file name should be "b.txt"
757 //! assert_eq!(jp.strictpath_file_name().unwrap().to_string_lossy(), "b.txt");
758 //! # Ok(()) }
759 //! ```
760 //!
761 //! Example: Deserialize a single `VirtualPath` with context
762 //! ```rust
763 //! use strict_path::{VirtualPath, VirtualRoot};
764 //! use strict_path::serde_ext::WithVirtualRoot;
765 //! use serde::de::DeserializeSeed;
766 //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
767 //! # let td = tempfile::tempdir()?;
768 //! let vroot: VirtualRoot = VirtualRoot::try_new(td.path())?;
769 //! let mut de = serde_json::Deserializer::from_str("\"a/b.txt\"");
770 //! let vp: VirtualPath = WithVirtualRoot(&vroot).deserialize(&mut de)?;
771 //! assert_eq!(vp.virtualpath_display().to_string(), "/a/b.txt");
772 //! # Ok(()) }
773 //! ```
774
775 use crate::{
776 path::strict_path::StrictPath, path::virtual_path::VirtualPath,
777 validator::virtual_root::VirtualRoot, PathBoundary,
778 };
779 use serde::de::DeserializeSeed;
780 use serde::Deserialize;
781
782 /// Deserialize a `StrictPath` with PathBoundary context.
783 pub struct WithBoundary<'a, Marker>(pub &'a PathBoundary<Marker>);
784
785 impl<'a, 'de, Marker> DeserializeSeed<'de> for WithBoundary<'a, Marker> {
786 type Value = StrictPath<Marker>;
787 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
788 where
789 D: serde::Deserializer<'de>,
790 {
791 let s = String::deserialize(deserializer)?;
792 self.0.strict_join(s).map_err(serde::de::Error::custom)
793 }
794 }
795
796 /// Deserialize a `VirtualPath` with virtual root context.
797 pub struct WithVirtualRoot<'a, Marker>(pub &'a VirtualRoot<Marker>);
798
799 impl<'a, 'de, Marker> DeserializeSeed<'de> for WithVirtualRoot<'a, Marker> {
800 type Value = VirtualPath<Marker>;
801 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
802 where
803 D: serde::Deserializer<'de>,
804 {
805 let s = String::deserialize(deserializer)?;
806 self.0.virtual_join(s).map_err(serde::de::Error::custom)
807 }
808 }
809}
810
811// Public exports
812pub use error::StrictPathError;
813pub use path::{strict_path::StrictPath, virtual_path::VirtualPath};
814pub use validator::path_boundary::PathBoundary;
815pub use validator::virtual_root::VirtualRoot;
816
817/// Result type alias for this crate's operations.
818pub type Result<T> = std::result::Result<T, StrictPathError>;
819
820#[cfg(test)]
821mod tests;