strict_path/lib.rs
1//! # strict-path
2//!
3//! Strictly enforce path boundaries to prevent directory traversal attacks.
4//!
5//! This crate performs full normalization/canonicalization and boundary enforcement with:
6//! - Safe symlink/junction handling (including cycle detection)
7//! - Windows-specific quirks (8.3 short names, UNC and verbatim prefixes, ADS)
8//! - Robust Unicode normalization and mixed-separator handling across platforms
9//! - Canonicalized path proofs encoded in the type system
10//!
11//! If a `StrictPath<Marker>` value exists, it is already proven to be inside its
12//! designated boundary by construction β not by best-effort string checks.
13//!
14//! π **[Complete Guide & Examples](https://dk26.github.io/strict-path-rs/)** | π **[API Reference](https://docs.rs/strict-path)**
15//!
16//! ## Quick Start
17//!
18//! ```rust
19//! # use strict_path::StrictPath;
20//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! let temp = tempfile::tempdir()?;
22//! let safe: StrictPath = StrictPath::with_boundary(temp.path())?
23//! .strict_join("users/alice.txt")?; // Validated, stays inside boundary
24//!
25//! safe.create_parent_dir_all()?;
26//! safe.write("hello")?;
27//! safe.metadata()?;
28//! safe.remove_file()?;
29//! # Ok(()) }
30//! ```
31//!
32//! ## Core Types
33//!
34//! - **`StrictPath`** β The fundamental security primitive. Every `StrictPath` is mathematically proven
35//! to be within its designated boundary via canonicalization and type-level guarantees.
36//! - **`PathBoundary`** β Creates and validates `StrictPath` instances from external input.
37//! - **`VirtualPath`** (feature `virtual-path`) β Extends `StrictPath` with user-friendly virtual root
38//! semantics (treating the boundary as "/").
39//! - **`VirtualRoot`** (feature `virtual-path`) β Creates `VirtualPath` instances with containment semantics.
40//!
41//! **[β Read the security methodology](https://dk26.github.io/strict-path-rs/security_methodology.html)**
42//!
43//! ## When to Use Which Type
44//!
45//! **StrictPath (default)** β Detect & reject path escapes (90% of use cases):
46//! - Archive extraction, file uploads, config loading
47//! - Returns `Err(PathEscapesBoundary)` on escape attempts
48//!
49//! **VirtualPath (opt-in)** β Contain & redirect path escapes (10% of use cases):
50//! - Multi-tenant systems, malware sandboxes, security research
51//! - Silently clamps escapes within the virtual boundary
52//! - Requires `features = ["virtual-path"]` in `Cargo.toml`
53//!
54//! **[β Read the detailed comparison](https://dk26.github.io/strict-path-rs/best_practices.html)**
55//!
56//! ## Type-System Guarantees
57//!
58//! Use marker types to encode policy directly in your APIs:
59//!
60//! ```rust
61//! # use strict_path::{PathBoundary, StrictPath};
62//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
63//! struct PublicAssets;
64//! struct UserUploads;
65//!
66//! # std::fs::create_dir_all("./assets")?;
67//! # std::fs::create_dir_all("./uploads")?;
68//! let assets = PathBoundary::<PublicAssets>::try_new("./assets")?;
69//! let uploads = PathBoundary::<UserUploads>::try_new("./uploads")?;
70//!
71//! let css: StrictPath<PublicAssets> = assets.strict_join("style.css")?;
72//! let avatar: StrictPath<UserUploads> = uploads.strict_join("avatar.jpg")?;
73//!
74//! fn serve_public_asset(file: &StrictPath<PublicAssets>) { /* ... */ }
75//!
76//! serve_public_asset(&css); // β
OK
77//! // serve_public_asset(&avatar); // β Compile error (wrong marker)
78//! # std::fs::remove_dir_all("./assets").ok();
79//! # std::fs::remove_dir_all("./uploads").ok();
80//! # Ok(()) }
81//! ```
82//!
83//! ## Security Foundation
84//!
85//! Built on [`soft-canonicalize`](https://crates.io/crates/soft-canonicalize), this crate protects against:
86//! - **CVE-2025-8088** (NTFS ADS path traversal)
87//! - **CVE-2022-21658** (TOCTOU attacks)
88//! - **CVE-2019-9855, CVE-2020-12279** (Windows 8.3 short names)
89//! - Path traversal, symlink attacks, Unicode normalization bypasses, race conditions
90//!
91//! **[β Read attack surface analysis](https://dk26.github.io/strict-path-rs/security_methodology.html#attack-surface)**
92//!
93//! ## Interop with External APIs
94//!
95//! Use `.interop_path()` to pass paths to external APIs expecting `AsRef<Path>`:
96//!
97//! ```rust
98//! # use strict_path::PathBoundary;
99//! # fn external_api<P: AsRef<std::path::Path>>(_p: P) {}
100//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
101//! let restriction: PathBoundary = PathBoundary::try_new_create("./safe")?;
102//! let jp = restriction.strict_join("file.txt")?;
103//!
104//! // β
Preferred: borrow as &OsStr (implements AsRef<Path>)
105//! external_api(jp.interop_path());
106//!
107//! // Escape hatches (use sparingly):
108//! let owned: std::path::PathBuf = jp.clone().unstrict();
109//! # let root_cleanup: strict_path::StrictPath = strict_path::StrictPath::with_boundary("./safe")?;
110//! # root_cleanup.remove_dir_all().ok();
111//! # Ok(()) }
112//! ```
113//!
114//! **[β Read the anti-patterns guide](https://dk26.github.io/strict-path-rs/anti_patterns.html)**
115//!
116//! ## Critical Anti-Patterns
117//!
118//! - **NEVER wrap our types in `Path::new()` or `PathBuf::from()`** β defeats all security
119//! - **NEVER use std `Path::join`** on leaked paths β can escape boundaries
120//! - **Use `.interop_path()` directly** for external APIs β no need for `.as_ref()`
121//! - **Use proper display methods** β `.strictpath_display()` not `.interop_path().to_string_lossy()`
122//!
123//! **[β See full anti-patterns list](https://dk26.github.io/strict-path-rs/anti_patterns.html)**
124//!
125//! ## Feature Flags
126//!
127//! - `virtual-path` β Enables `VirtualRoot`/`VirtualPath` for containment scenarios
128//! - `serde` β Serialization support (deserialization requires context; see `serde_ext` module)
129//! - `dirs` β OS directory discovery (`PathBoundary::from_home_dir()`, etc.)
130//! - `tempfile` β RAII constructors for temporary boundaries
131//! - `app-path` β Application-specific directory patterns with env var overrides
132//!
133//! **[β Read the getting started guide](https://dk26.github.io/strict-path-rs/getting_started.html)**
134//!
135//! ## Additional Resources
136//!
137//! - **[LLM API Reference](https://github.com/DK26/strict-path-rs/blob/main/LLM_API_REFERENCE.md)** β
138//! Concise, copy-pastable reference optimized for AI assistants
139//! - **[Complete Guide](https://dk26.github.io/strict-path-rs/)** β Comprehensive documentation with examples
140//! - **[API Reference](https://docs.rs/strict-path)** β Full type and method documentation
141//! - **[Repository](https://github.com/DK26/strict-path-rs)** β Source code and issue tracker
142
143#![forbid(unsafe_code)]
144
145pub mod error;
146pub mod path;
147pub mod validator;
148
149#[cfg(feature = "serde")]
150pub mod serde_ext {
151 //! Serde helpers and notes.
152 //!
153 //! Builtβin `Serialize` (feature `serde`):
154 //! - `StrictPath` β system path string
155 //! - `VirtualPath` β virtual root string (e.g., "/a/b.txt")
156 //!
157 //! Deserialization requires context (a `PathBoundary` or `VirtualRoot`). Use the context helpers
158 //! below to deserialize with context, or deserialize to `String` and validate explicitly.
159 //!
160 //! Example: Deserialize a single `StrictPath` with context
161 //! ```rust
162 //! use strict_path::{PathBoundary, StrictPath};
163 //! use strict_path::serde_ext::WithBoundary;
164 //! use serde::de::DeserializeSeed;
165 //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
166 //! # let td = tempfile::tempdir()?;
167 //! let boundary: PathBoundary = PathBoundary::try_new(td.path())?;
168 //! let mut de = serde_json::Deserializer::from_str("\"a/b.txt\"");
169 //! let jp: StrictPath = WithBoundary(&boundary).deserialize(&mut de)?;
170 //! // OS-agnostic assertion: file name should be "b.txt"
171 //! assert_eq!(jp.strictpath_file_name().unwrap().to_string_lossy(), "b.txt");
172 //! # Ok(()) }
173 //! ```
174 //!
175 //! Example: Deserialize a single `VirtualPath` with context
176 //! ```rust
177 //! # #[cfg(feature = "virtual-path")] {
178 //! use strict_path::{VirtualPath, VirtualRoot};
179 //! use strict_path::serde_ext::WithVirtualRoot;
180 //! use serde::de::DeserializeSeed;
181 //! # fn main() -> Result<(), Box<dyn std::error::Error>> {
182 //! # let td = tempfile::tempdir()?;
183 //! let vroot: VirtualRoot = VirtualRoot::try_new(td.path())?;
184 //! let mut de = serde_json::Deserializer::from_str("\"a/b.txt\"");
185 //! let vp: VirtualPath = WithVirtualRoot(&vroot).deserialize(&mut de)?;
186 //! assert_eq!(vp.virtualpath_display().to_string(), "/a/b.txt");
187 //! # Ok(()) }
188 //! # }
189 //! ```
190
191 use crate::{path::strict_path::StrictPath, PathBoundary};
192 #[cfg(feature = "virtual-path")]
193 use crate::{path::virtual_path::VirtualPath, validator::virtual_root::VirtualRoot};
194 use serde::de::DeserializeSeed;
195 use serde::Deserialize;
196
197 /// Deserialize a `StrictPath` with PathBoundary context.
198 pub struct WithBoundary<'a, Marker>(pub &'a PathBoundary<Marker>);
199
200 impl<'a, 'de, Marker> DeserializeSeed<'de> for WithBoundary<'a, Marker> {
201 type Value = StrictPath<Marker>;
202 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
203 where
204 D: serde::Deserializer<'de>,
205 {
206 let s = String::deserialize(deserializer)?;
207 self.0.strict_join(s).map_err(serde::de::Error::custom)
208 }
209 }
210
211 /// Deserialize a `VirtualPath` with virtual root context.
212 #[cfg(feature = "virtual-path")]
213 pub struct WithVirtualRoot<'a, Marker>(pub &'a VirtualRoot<Marker>);
214
215 #[cfg(feature = "virtual-path")]
216 impl<'a, 'de, Marker> DeserializeSeed<'de> for WithVirtualRoot<'a, Marker> {
217 type Value = VirtualPath<Marker>;
218 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
219 where
220 D: serde::Deserializer<'de>,
221 {
222 let s = String::deserialize(deserializer)?;
223 self.0.virtual_join(s).map_err(serde::de::Error::custom)
224 }
225 }
226}
227
228// Public exports
229pub use error::StrictPathError;
230pub use path::strict_path::StrictPath;
231pub use validator::path_boundary::PathBoundary;
232
233#[cfg(feature = "virtual-path")]
234pub use path::virtual_path::VirtualPath;
235
236#[cfg(feature = "virtual-path")]
237pub use validator::virtual_root::VirtualRoot;
238
239/// Result type alias for this crate's operations.
240pub type Result<T> = std::result::Result<T, StrictPathError>;
241
242#[cfg(test)]
243mod tests;