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    /// Remove the underlying root directory (non‑recursive); fails if not empty.
201    #[inline]
202    pub fn remove_dir(&self) -> std::io::Result<()> {
203        self.root.remove_dir()
204    }
205
206    /// SUMMARY:
207    /// Recursively remove the underlying root directory and all its contents.
208    #[inline]
209    pub fn remove_dir_all(&self) -> std::io::Result<()> {
210        self.root.remove_dir_all()
211    }
212
213    /// SUMMARY:
214    /// Ensure the directory exists (create if missing), then return a `VirtualRoot`.
215    ///
216    /// EXAMPLE:
217    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
218    /// ```rust
219    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
220    /// use strict_path::VirtualRoot;
221    /// let vroot = VirtualRoot::<()>::try_new_create("./data")?;
222    /// # Ok(())
223    /// # }
224    /// ```
225    #[inline]
226    pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
227        let root = PathBoundary::try_new_create(root_path)?;
228        Ok(Self {
229            root,
230            _marker: PhantomData,
231        })
232    }
233
234    /// SUMMARY:
235    /// Join a candidate path to this virtual root, producing a clamped `VirtualPath`.
236    ///
237    /// DETAILS:
238    /// This is the security gateway for virtual paths. Absolute paths (starting with `"/"`) are
239    /// automatically clamped to the virtual root, ensuring paths cannot escape the sandbox.
240    /// For example, `"/etc/config"` becomes `vroot/etc/config`, and traversal attempts like
241    /// `"../../../../etc/passwd"` are clamped to `vroot/etc/passwd`. This clamping behavior is
242    /// what makes the `virtual_` dimension safe for user-facing operations.
243    ///
244    /// PARAMETERS:
245    /// - `candidate_path` (`AsRef<Path>`): Virtual path to resolve and clamp. Absolute paths
246    ///   are interpreted relative to the virtual root, not the system root.
247    ///
248    /// RETURNS:
249    /// - `Result<VirtualPath<Marker>>`: Clamped, validated path within the virtual root.
250    ///
251    /// ERRORS:
252    /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
253    ///
254    /// EXAMPLE:
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    #[inline]
274    pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
275        // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
276        let user_candidate = candidate_path.as_ref().to_path_buf();
277        let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
278
279        // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
280        let validated = anchored.boundary_check(self.root.stated_path())?;
281
282        // 3) Construct a StrictPath directly and then virtualize
283        let jp = crate::path::strict_path::StrictPath::new(
284            std::sync::Arc::new(self.root.clone()),
285            validated,
286        );
287        Ok(jp.virtualize())
288    }
289
290    /// Returns the underlying path boundary root as a system path.
291    #[inline]
292    pub(crate) fn path(&self) -> &Path {
293        self.root.path()
294    }
295
296    /// SUMMARY:
297    /// Return the virtual root path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
298    #[inline]
299    pub fn interop_path(&self) -> &std::ffi::OsStr {
300        self.root.interop_path()
301    }
302
303    /// Returns true if the underlying path boundary root exists.
304    #[inline]
305    pub fn exists(&self) -> bool {
306        self.root.exists()
307    }
308
309    /// SUMMARY:
310    /// Borrow the underlying `PathBoundary`.
311    #[inline]
312    pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
313        &self.root
314    }
315
316    /// SUMMARY:
317    /// Consume this `VirtualRoot` and return the underlying `PathBoundary` (symmetry with `virtualize`).
318    #[inline]
319    pub fn unvirtual(self) -> PathBoundary<Marker> {
320        self.root
321    }
322
323    // OS Standard Directory Constructors
324    //
325    // Creates virtual roots in OS standard directories following platform conventions.
326    // Applications see clean virtual paths ("/config.toml") while the system manages
327    // the actual location (e.g., "~/.config/myapp/config.toml").
328}
329
330impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        write!(f, "{}", self.path().display())
333    }
334}
335
336impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
337    fn as_ref(&self) -> &Path {
338        self.path()
339    }
340}
341
342impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
343    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344        f.debug_struct("VirtualRoot")
345            .field("root", &self.path())
346            .field("marker", &std::any::type_name::<Marker>())
347            .finish()
348    }
349}
350
351impl<Marker> Eq for VirtualRoot<Marker> {}
352
353impl<M1, M2> PartialEq<VirtualRoot<M2>> for VirtualRoot<M1> {
354    #[inline]
355    fn eq(&self, other: &VirtualRoot<M2>) -> bool {
356        self.path() == other.path()
357    }
358}
359
360impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
361    #[inline]
362    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
363        self.path().hash(state);
364    }
365}
366
367impl<Marker> PartialOrd for VirtualRoot<Marker> {
368    #[inline]
369    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
370        Some(self.cmp(other))
371    }
372}
373
374impl<Marker> Ord for VirtualRoot<Marker> {
375    #[inline]
376    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
377        self.path().cmp(other.path())
378    }
379}
380
381impl<M1, M2> PartialEq<crate::PathBoundary<M2>> for VirtualRoot<M1> {
382    #[inline]
383    fn eq(&self, other: &crate::PathBoundary<M2>) -> bool {
384        self.path() == other.path()
385    }
386}
387
388impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
389    #[inline]
390    fn eq(&self, other: &std::path::Path) -> bool {
391        // Compare as virtual root path (always "/")
392        // VirtualRoot represents the virtual "/" regardless of underlying system path
393        let other_str = other.to_string_lossy();
394
395        #[cfg(windows)]
396        let other_normalized = other_str.replace('\\', "/");
397        #[cfg(not(windows))]
398        let other_normalized = other_str.to_string();
399
400        let normalized_other = if other_normalized.starts_with('/') {
401            other_normalized
402        } else {
403            format!("/{}", other_normalized)
404        };
405
406        "/" == normalized_other
407    }
408}
409
410impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
411    #[inline]
412    fn eq(&self, other: &std::path::PathBuf) -> bool {
413        self.eq(other.as_path())
414    }
415}
416
417impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
418    #[inline]
419    fn eq(&self, other: &&std::path::Path) -> bool {
420        self.eq(*other)
421    }
422}
423
424impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
425    #[inline]
426    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
427        // Compare as virtual root path (always "/")
428        let other_str = other.to_string_lossy();
429
430        // Handle empty path specially - "/" is greater than ""
431        if other_str.is_empty() {
432            return Some(std::cmp::Ordering::Greater);
433        }
434
435        #[cfg(windows)]
436        let other_normalized = other_str.replace('\\', "/");
437        #[cfg(not(windows))]
438        let other_normalized = other_str.to_string();
439
440        let normalized_other = if other_normalized.starts_with('/') {
441            other_normalized
442        } else {
443            format!("/{}", other_normalized)
444        };
445
446        Some("/".cmp(&normalized_other))
447    }
448}
449
450impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
451    #[inline]
452    fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
453        self.partial_cmp(*other)
454    }
455}
456
457impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
458    #[inline]
459    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
460        self.partial_cmp(other.as_path())
461    }
462}
463
464impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
465    type Err = crate::StrictPathError;
466
467    /// Parse a VirtualRoot from a string path for universal ergonomics.
468    ///
469    /// Creates the directory if it doesn't exist, enabling seamless integration
470    /// with any string-parsing context (clap, config files, environment variables, etc.):
471    /// ```rust
472    /// # use strict_path::VirtualRoot;
473    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
474    /// let temp_dir = tempfile::tempdir()?;
475    /// let virtual_path = temp_dir.path().join("virtual_dir");
476    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
477    /// assert!(virtual_path.exists());
478    /// # Ok(())
479    /// # }
480    /// ```
481    #[inline]
482    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
483        Self::try_new_create(path)
484    }
485}