Skip to main content

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