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