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