strict_path/
lib.rs

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