strict_path/validator/virtual_root.rs
1//! `VirtualRoot<Marker>` — the factory for `VirtualPath` values clamped to a virtual root.
2//!
3//! A `VirtualRoot` wraps a `PathBoundary` and maps all paths into a virtual namespace
4//! rooted at `"/"`. Traversal past the root is clamped (not rejected): `virtual_join("../../x")`
5//! resolves to `"/x"` rather than escaping the real filesystem boundary. This makes
6//! `VirtualRoot` safe to expose to untrusted input even without returning errors.
7use crate::path::virtual_path::VirtualPath;
8use crate::validator::path_history::PathHistory;
9use crate::PathBoundary;
10use crate::Result;
11use std::marker::PhantomData;
12use std::path::Path;
13
14/// Provide a user‑facing virtual root that produces `VirtualPath` values clamped to a boundary.
15#[derive(Clone)]
16#[must_use = "a VirtualRoot is validated and ready to enforce virtual path restrictions — call .virtual_join() to validate untrusted input, .into_virtualpath() to get the root path, or pass to functions that accept &VirtualRoot<Marker>"]
17#[doc(alias = "jail")]
18#[doc(alias = "chroot")]
19#[doc(alias = "sandbox")]
20#[doc(alias = "contain")]
21pub struct VirtualRoot<Marker = ()> {
22 pub(crate) root: PathBoundary<Marker>,
23 pub(crate) _marker: PhantomData<Marker>,
24}
25
26impl<Marker> VirtualRoot<Marker> {
27 // no extra constructors; use PathBoundary::virtualize() or VirtualRoot::try_new
28 /// Create a `VirtualRoot` from an existing directory.
29 ///
30 /// # Errors
31 ///
32 /// - `StrictPathError::InvalidRestriction`: Root invalid or cannot be canonicalized.
33 ///
34 /// # Examples
35 ///
36 /// ```rust
37 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
38 /// use strict_path::VirtualRoot;
39 /// let vroot = VirtualRoot::<()>::try_new("./data")?;
40 /// # Ok(())
41 /// # }
42 /// ```
43 #[must_use = "this returns a Result containing the validated VirtualRoot — handle the Result to detect invalid root directories"]
44 #[inline]
45 pub fn try_new<P: AsRef<Path>>(root_path: P) -> Result<Self> {
46 let root = PathBoundary::try_new(root_path)?;
47 Ok(Self {
48 root,
49 _marker: PhantomData,
50 })
51 }
52
53 /// Return filesystem metadata for the underlying root directory.
54 #[inline]
55 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
56 self.root.metadata()
57 }
58
59 /// Consume this virtual root and return the rooted `VirtualPath` ("/").
60 ///
61 /// # Errors
62 ///
63 /// - `StrictPathError::PathResolutionError`: Canonicalization fails (root removed or inaccessible).
64 /// - `StrictPathError::PathEscapesBoundary`: Root moved outside the boundary between checks.
65 ///
66 /// # Examples
67 ///
68 /// ```rust
69 /// # use strict_path::{VirtualPath, VirtualRoot};
70 /// # let root = std::env::temp_dir().join("into-virtualpath-example");
71 /// # std::fs::create_dir_all(&root)?;
72 /// let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
73 /// let root_virtual: VirtualPath = vroot.into_virtualpath()?;
74 /// assert_eq!(root_virtual.virtualpath_display().to_string(), "/");
75 /// # std::fs::remove_dir_all(&root)?;
76 /// # Ok::<_, Box<dyn std::error::Error>>(())
77 /// ```
78 #[must_use = "into_virtualpath() consumes the VirtualRoot — use the returned VirtualPath for virtual path operations"]
79 #[inline]
80 pub fn into_virtualpath(self) -> Result<VirtualPath<Marker>> {
81 let strict_root = self.root.into_strictpath()?;
82 Ok(strict_root.virtualize())
83 }
84
85 /// Consume this virtual root and substitute a new marker type.
86 ///
87 /// Mirrors [`crate::PathBoundary::change_marker`], [`crate::StrictPath::change_marker`], and
88 /// [`crate::VirtualPath::change_marker`]. Use this when encoding proven authorization
89 /// into the type system (e.g., after validating a user's permissions). The
90 /// consumption makes marker changes explicit during code review.
91 ///
92 /// # Examples
93 ///
94 /// ```rust
95 /// # use strict_path::VirtualRoot;
96 /// # let root_dir = std::env::temp_dir().join("vroot-change-marker-example");
97 /// # std::fs::create_dir_all(&root_dir)?;
98 /// struct UserFiles;
99 /// struct ReadOnly;
100 /// struct ReadWrite;
101 ///
102 /// let read_root: VirtualRoot<(UserFiles, ReadOnly)> = VirtualRoot::try_new(&root_dir)?;
103 ///
104 /// // After authorization check...
105 /// let write_root: VirtualRoot<(UserFiles, ReadWrite)> = read_root.change_marker();
106 /// # std::fs::remove_dir_all(&root_dir)?;
107 /// # Ok::<_, Box<dyn std::error::Error>>(())
108 /// ```
109 #[must_use = "change_marker() consumes self — the original VirtualRoot is moved; use the returned VirtualRoot<NewMarker>"]
110 #[inline]
111 pub fn change_marker<NewMarker>(self) -> VirtualRoot<NewMarker> {
112 let VirtualRoot { root, .. } = self;
113
114 VirtualRoot {
115 root: root.change_marker(),
116 _marker: PhantomData,
117 }
118 }
119
120 /// Create a symbolic link at `link_path` pointing to this root's underlying directory.
121 ///
122 /// `link_path` is interpreted in the virtual dimension and resolved via `virtual_join()`
123 /// so that absolute virtual paths ("/links/a") are clamped within this virtual root and
124 /// relative paths are resolved relative to the virtual root.
125 pub fn virtual_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
126 // Resolve the link location in virtual space first (clamps/anchors under this root)
127 let link_ref = link_path.as_ref();
128 let validated_link = self.virtual_join(link_ref).map_err(std::io::Error::other)?;
129
130 // Obtain the strict target for the root directory
131 let root = self
132 .root
133 .clone()
134 .into_strictpath()
135 .map_err(std::io::Error::other)?;
136
137 root.strict_symlink(validated_link.as_unvirtual().path())
138 }
139
140 /// Create a hard link at `link_path` pointing to this root's underlying directory.
141 ///
142 /// The link location is resolved via `virtual_join()` to clamp/anchor within this root.
143 /// Note: Most platforms forbid directory hard links; expect an error from the OS.
144 pub fn virtual_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
145 let link_ref = link_path.as_ref();
146 let validated_link = self.virtual_join(link_ref).map_err(std::io::Error::other)?;
147
148 let root = self
149 .root
150 .clone()
151 .into_strictpath()
152 .map_err(std::io::Error::other)?;
153
154 root.strict_hard_link(validated_link.as_unvirtual().path())
155 }
156
157 /// Create a Windows NTFS directory junction at `link_path` pointing to this virtual root's directory.
158 ///
159 /// - Windows-only and behind the `junctions` feature.
160 #[cfg(all(windows, feature = "junctions"))]
161 pub fn virtual_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
162 let link_ref = link_path.as_ref();
163 let validated_link = self.virtual_join(link_ref).map_err(std::io::Error::other)?;
164
165 let root = self
166 .root
167 .clone()
168 .into_strictpath()
169 .map_err(std::io::Error::other)?;
170
171 root.strict_junction(validated_link.as_unvirtual().path())
172 }
173
174 /// Read directory entries at the virtual root (discovery). Re‑join names through virtual/strict APIs before I/O.
175 #[inline]
176 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
177 self.root.read_dir()
178 }
179
180 /// Iterate directory entries at the virtual root, yielding validated `VirtualPath` values.
181 ///
182 /// Unlike `read_dir()` which returns raw `std::fs::DirEntry` values requiring manual
183 /// re-validation, this method yields `VirtualPath` entries directly. Each entry is
184 /// automatically validated through `virtual_join()` so you can use it immediately
185 /// for I/O operations without additional validation.
186 ///
187 /// # Examples
188 ///
189 /// ```rust
190 /// use strict_path::VirtualRoot;
191 ///
192 /// # let temp = tempfile::tempdir()?;
193 /// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
194 /// # vroot.virtual_join("file.txt")?.write("test")?;
195 ///
196 /// // Auto-validated iteration - no manual re-join needed!
197 /// for entry in vroot.virtual_read_dir()? {
198 /// let child = entry?;
199 /// println!("Virtual: {}", child.virtualpath_display());
200 /// }
201 /// # Ok::<_, Box<dyn std::error::Error>>(())
202 /// ```
203 #[inline]
204 pub fn virtual_read_dir(&self) -> std::io::Result<VirtualRootReadDir<'_, Marker>> {
205 Ok(VirtualRootReadDir {
206 inner: self.root.read_dir()?,
207 vroot: self,
208 })
209 }
210
211 /// Remove the underlying root directory (non‑recursive); fails if not empty.
212 #[inline]
213 pub fn remove_dir(&self) -> std::io::Result<()> {
214 self.root.remove_dir()
215 }
216
217 /// Recursively remove the underlying root directory and all its contents.
218 #[inline]
219 pub fn remove_dir_all(&self) -> std::io::Result<()> {
220 self.root.remove_dir_all()
221 }
222
223 /// Ensure the directory exists (create if missing), then return a `VirtualRoot`.
224 ///
225 /// # Examples
226 ///
227 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
228 /// ```rust
229 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
230 /// use strict_path::VirtualRoot;
231 /// let vroot = VirtualRoot::<()>::try_new_create("./data")?;
232 /// # Ok(())
233 /// # }
234 /// ```
235 #[must_use = "this returns a Result containing the validated VirtualRoot — handle the Result to detect invalid root directories"]
236 #[inline]
237 pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
238 let root = PathBoundary::try_new_create(root_path)?;
239 Ok(Self {
240 root,
241 _marker: PhantomData,
242 })
243 }
244
245 /// Join a candidate path to this virtual root, producing a clamped `VirtualPath`.
246 ///
247 /// This is the security gateway for virtual paths. Absolute paths (starting with `"/"`) are
248 /// automatically clamped to the virtual root, ensuring paths cannot escape the sandbox.
249 /// For example, `"/etc/config"` becomes `vroot/etc/config`, and traversal attempts like
250 /// `"../../../../etc/passwd"` are clamped to `vroot/etc/passwd`. This clamping behavior is
251 /// what makes the `virtual_` dimension safe for user-facing operations.
252 ///
253 /// # Errors
254 ///
255 /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
256 ///
257 /// # Examples
258 ///
259 /// ```rust
260 /// # use strict_path::VirtualRoot;
261 /// # let td = tempfile::tempdir().unwrap();
262 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
263 ///
264 /// // Absolute paths are clamped to virtual root, not system root
265 /// let user_input_abs = "/etc/config"; // Untrusted input
266 /// let path1 = vroot.virtual_join(user_input_abs)?;
267 /// assert_eq!(path1.virtualpath_display().to_string(), "/etc/config");
268 ///
269 /// // Traversal attempts are also clamped
270 /// let attack_input = "../../../etc/passwd"; // Untrusted input
271 /// let path2 = vroot.virtual_join(attack_input)?;
272 /// assert_eq!(path2.virtualpath_display().to_string(), "/etc/passwd");
273 ///
274 /// // Both paths are safely within the virtual root on the actual filesystem
275 /// # Ok::<(), Box<dyn std::error::Error>>(())
276 /// ```
277 #[must_use = "virtual_join() validates untrusted input against the virtual root — always handle the Result to detect escape attempts"]
278 #[inline]
279 pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
280 // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
281 let user_candidate = candidate_path.as_ref().to_path_buf();
282 let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
283
284 // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
285 let validated = anchored.boundary_check(self.root.stated_path())?;
286
287 // 3) Construct a StrictPath directly and then virtualize
288 let jp = crate::path::strict_path::StrictPath::new(
289 std::sync::Arc::new(self.root.clone()),
290 validated,
291 );
292 Ok(jp.virtualize())
293 }
294
295 /// Returns the underlying path boundary root as a system path.
296 #[inline]
297 pub(crate) fn path(&self) -> &Path {
298 self.root.path()
299 }
300
301 /// Return the virtual root path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
302 #[must_use = "pass interop_path() directly to third-party APIs requiring AsRef<Path> — never wrap it in Path::new() or PathBuf::from() as that defeats boundary safety"]
303 #[inline]
304 pub fn interop_path(&self) -> &std::ffi::OsStr {
305 self.root.interop_path()
306 }
307
308 /// Returns true if the underlying path boundary root exists.
309 #[must_use]
310 #[inline]
311 pub fn exists(&self) -> bool {
312 self.root.exists()
313 }
314
315 /// Borrow the underlying `PathBoundary`.
316 #[must_use = "as_unvirtual() borrows the underlying PathBoundary — use it for strict operations or pass to functions accepting &PathBoundary<Marker>"]
317 #[inline]
318 pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
319 &self.root
320 }
321
322 /// Consume this `VirtualRoot` and return the underlying `PathBoundary` (symmetry with `virtualize`).
323 #[must_use = "unvirtual() consumes self — use the returned PathBoundary for strict path operations, or prefer .as_unvirtual() to borrow without consuming"]
324 #[inline]
325 pub fn unvirtual(self) -> PathBoundary<Marker> {
326 self.root
327 }
328
329 // OS Standard Directory Constructors
330 //
331 // Creates virtual roots in OS standard directories following platform conventions.
332 // Applications see clean virtual paths ("/config.toml") while the system manages
333 // the actual location (e.g., "~/.config/myapp/config.toml").
334}
335
336/// Display shows "/": The real system path must never appear in user-facing output
337/// (logs, API responses, error messages). Showing "/" reinforces that VirtualRoot
338/// represents a virtual namespace root, not a concrete filesystem location.
339impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 write!(f, "/")
342 }
343}
344
345impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 f.debug_struct("VirtualRoot")
348 .field("root", &self.path())
349 .field("marker", &std::any::type_name::<Marker>())
350 .finish()
351 }
352}
353
354impl<Marker> Eq for VirtualRoot<Marker> {}
355
356impl<M1, M2> PartialEq<VirtualRoot<M2>> for VirtualRoot<M1> {
357 #[inline]
358 fn eq(&self, other: &VirtualRoot<M2>) -> bool {
359 self.path() == other.path()
360 }
361}
362
363impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
364 #[inline]
365 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
366 self.path().hash(state);
367 }
368}
369
370impl<Marker> PartialOrd for VirtualRoot<Marker> {
371 #[inline]
372 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
373 Some(self.cmp(other))
374 }
375}
376
377impl<Marker> Ord for VirtualRoot<Marker> {
378 #[inline]
379 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
380 self.path().cmp(other.path())
381 }
382}
383
384impl<M1, M2> PartialEq<crate::PathBoundary<M2>> for VirtualRoot<M1> {
385 #[inline]
386 fn eq(&self, other: &crate::PathBoundary<M2>) -> bool {
387 self.path() == other.path()
388 }
389}
390
391/// compare against "/": VirtualRoot's public identity is the virtual namespace root.
392/// Comparing against the real system path would leak implementation details and break the
393/// abstraction — callers should never need to know the underlying directory.
394impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
395 #[inline]
396 fn eq(&self, other: &std::path::Path) -> bool {
397 // Compare as virtual root path (always "/")
398 // VirtualRoot represents the virtual "/" regardless of underlying system path
399 let other_str = other.to_string_lossy();
400
401 #[cfg(windows)]
402 let other_normalized = other_str.replace('\\', "/");
403 #[cfg(not(windows))]
404 let other_normalized = other_str.to_string();
405
406 let normalized_other = if other_normalized.starts_with('/') {
407 other_normalized
408 } else {
409 format!("/{other_normalized}")
410 };
411
412 "/" == normalized_other
413 }
414}
415
416impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
417 #[inline]
418 fn eq(&self, other: &std::path::PathBuf) -> bool {
419 self.eq(other.as_path())
420 }
421}
422
423impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
424 #[inline]
425 fn eq(&self, other: &&std::path::Path) -> bool {
426 self.eq(*other)
427 }
428}
429
430impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
431 #[inline]
432 fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
433 // Compare as virtual root path (always "/")
434 let other_str = other.to_string_lossy();
435
436 // Handle empty path specially - "/" is greater than ""
437 if other_str.is_empty() {
438 return Some(std::cmp::Ordering::Greater);
439 }
440
441 #[cfg(windows)]
442 let other_normalized = other_str.replace('\\', "/");
443 #[cfg(not(windows))]
444 let other_normalized = other_str.to_string();
445
446 let normalized_other = if other_normalized.starts_with('/') {
447 other_normalized
448 } else {
449 format!("/{other_normalized}")
450 };
451
452 Some("/".cmp(&normalized_other))
453 }
454}
455
456impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
457 #[inline]
458 fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
459 self.partial_cmp(*other)
460 }
461}
462
463impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
464 #[inline]
465 fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
466 self.partial_cmp(other.as_path())
467 }
468}
469
470impl<Marker> std::str::FromStr for VirtualRoot<Marker> {
471 type Err = crate::StrictPathError;
472
473 /// Forwards to [`try_new_create`](Self::try_new_create): creates the
474 /// target directory if missing, then canonicalizes and validates it as a
475 /// directory.
476 ///
477 /// Untrusted per-request paths (archive entries, user-supplied virtual
478 /// paths) are not `FromStr` input — validate those via
479 /// [`virtual_join`](Self::virtual_join) on a pre-constructed root.
480 ///
481 /// ```rust
482 /// # use strict_path::VirtualRoot;
483 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
484 /// # let temp_dir = tempfile::tempdir()?;
485 /// # let p = temp_dir.path().join("sandbox");
486 /// # let p = p.to_string_lossy().into_owned();
487 /// let vroot: VirtualRoot<()> = p.parse()?;
488 /// assert!(vroot.exists());
489 /// # Ok(())
490 /// # }
491 /// ```
492 #[inline]
493 fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
494 Self::try_new_create(path)
495 }
496}
497
498// ============================================================
499// VirtualRootReadDir — Iterator for validated virtual directory entries
500// ============================================================
501
502/// Iterator over directory entries that yields validated `VirtualPath` values.
503///
504/// Created by `VirtualRoot::virtual_read_dir()`. Each iteration automatically validates
505/// the directory entry through `virtual_join()`, so you get `VirtualPath` values directly
506/// instead of raw `std::fs::DirEntry` that would require manual re-validation.
507///
508/// # Examples
509///
510/// ```rust
511/// # use strict_path::VirtualRoot;
512/// # let temp = tempfile::tempdir()?;
513/// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
514/// # vroot.virtual_join("readme.md")?.write("# Docs")?;
515/// for entry in vroot.virtual_read_dir()? {
516/// let child = entry?;
517/// if child.is_file() {
518/// println!("Virtual: {}", child.virtualpath_display());
519/// }
520/// }
521/// # Ok::<_, Box<dyn std::error::Error>>(())
522/// ```
523pub struct VirtualRootReadDir<'a, Marker> {
524 inner: std::fs::ReadDir,
525 vroot: &'a VirtualRoot<Marker>,
526}
527
528impl<Marker> std::fmt::Debug for VirtualRootReadDir<'_, Marker> {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("VirtualRootReadDir")
531 .field("vroot", &"/")
532 .finish_non_exhaustive()
533 }
534}
535
536impl<Marker: Clone> Iterator for VirtualRootReadDir<'_, Marker> {
537 type Item = std::io::Result<crate::path::virtual_path::VirtualPath<Marker>>;
538
539 fn next(&mut self) -> Option<Self::Item> {
540 match self.inner.next()? {
541 Ok(entry) => {
542 let file_name = entry.file_name();
543 match self.vroot.virtual_join(file_name) {
544 Ok(virtual_path) => Some(Ok(virtual_path)),
545 Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
546 }
547 }
548 Err(e) => Some(Err(e)),
549 }
550 }
551}