strict_path/validator/
virtual_root.rs

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