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#[cfg(feature = "tempfile")]
9use std::sync::Arc;
10
11// keep feature-gated TempDir RAII field using Arc from std::sync
12#[cfg(feature = "tempfile")]
13use tempfile::TempDir;
14
15/// SUMMARY:
16/// Provide a user‑facing virtual root that produces `VirtualPath` values clamped to a boundary.
17#[derive(Clone)]
18pub struct VirtualRoot<Marker = ()> {
19    pub(crate) root: PathBoundary<Marker>,
20    // Held only to tie RAII of temp directories to the VirtualRoot lifetime
21    #[cfg(feature = "tempfile")]
22    pub(crate) _temp_dir: Option<Arc<TempDir>>, // mirrors RAII when constructed from temp
23    pub(crate) _marker: PhantomData<Marker>,
24}
25
26impl<Marker> VirtualRoot<Marker> {
27    // no extra constructors; use PathBoundary::virtualize() or VirtualRoot::try_new
28    /// SUMMARY:
29    /// Create a `VirtualRoot` from an existing directory.
30    ///
31    /// PARAMETERS:
32    /// - `root_path` (`AsRef<Path>`): Existing directory to anchor the virtual root.
33    ///
34    /// RETURNS:
35    /// - `Result<VirtualRoot<Marker>>`: New virtual root with clamped operations.
36    ///
37    /// ERRORS:
38    /// - `StrictPathError::InvalidRestriction`: Root invalid or cannot be canonicalized.
39    ///
40    /// EXAMPLE:
41    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
42    /// ```rust
43    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
44    /// use strict_path::VirtualRoot;
45    /// let tmp_dir = tempfile::tempdir()?;
46    /// let tmp_dir = VirtualRoot::<()>::try_new(tmp_dir)?; // Clean variable shadowing
47    /// # Ok(())
48    /// # }
49    /// ```
50    #[inline]
51    pub fn try_new<P: AsRef<Path>>(root_path: P) -> Result<Self> {
52        let root = PathBoundary::try_new(root_path)?;
53        Ok(Self {
54            root,
55            #[cfg(feature = "tempfile")]
56            _temp_dir: None,
57            _marker: PhantomData,
58        })
59    }
60
61    /// SUMMARY:
62    /// Create a `VirtualRoot` backed by a unique temporary directory with RAII cleanup.
63    ///
64    /// # Example
65    /// ```
66    /// # #[cfg(feature = "tempfile")] {
67    /// use strict_path::VirtualRoot;
68    ///
69    /// let uploads_root = VirtualRoot::<()>::try_new_temp()?;
70    /// let tenant_file = uploads_root.virtual_join("tenant/document.pdf")?;
71    /// let display = tenant_file.virtualpath_display().to_string();
72    /// assert!(display.starts_with("/"));
73    /// # }
74    /// # Ok::<(), Box<dyn std::error::Error>>(())
75    /// ```
76    #[cfg(feature = "tempfile")]
77    #[inline]
78    pub fn try_new_temp() -> Result<Self> {
79        let root = PathBoundary::try_new_temp()?;
80        let temp_dir = root.temp_dir_arc();
81        Ok(Self {
82            root,
83            #[cfg(feature = "tempfile")]
84            _temp_dir: temp_dir,
85            _marker: PhantomData,
86        })
87    }
88
89    /// SUMMARY:
90    /// Create a `VirtualRoot` in a temporary directory with a custom prefix and RAII cleanup.
91    ///
92    /// # Example
93    /// ```
94    /// # #[cfg(feature = "tempfile")] {
95    /// use strict_path::VirtualRoot;
96    ///
97    /// let session_root = VirtualRoot::<()>::try_new_temp_with_prefix("session")?;
98    /// let export_path = session_root.virtual_join("exports/report.txt")?;
99    /// let display = export_path.virtualpath_display().to_string();
100    /// assert!(display.starts_with("/exports"));
101    /// # }
102    /// # Ok::<(), Box<dyn std::error::Error>>(())
103    /// ```
104    #[cfg(feature = "tempfile")]
105    #[inline]
106    pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
107        let root = PathBoundary::try_new_temp_with_prefix(prefix)?;
108        let temp_dir = root.temp_dir_arc();
109        Ok(Self {
110            root,
111            #[cfg(feature = "tempfile")]
112            _temp_dir: temp_dir,
113            _marker: PhantomData,
114        })
115    }
116
117    /// SUMMARY:
118    /// Return filesystem metadata for the underlying root directory.
119    #[inline]
120    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
121        self.root.metadata()
122    }
123
124    /// SUMMARY:
125    /// Consume this virtual root and return the rooted `VirtualPath` ("/").
126    ///
127    /// PARAMETERS:
128    /// - _none_
129    ///
130    /// RETURNS:
131    /// - `Result<VirtualPath<Marker>>`: Virtual root path clamped to this boundary.
132    ///
133    /// ERRORS:
134    /// - `StrictPathError::PathResolutionError`: Canonicalization fails (root removed or inaccessible).
135    /// - `StrictPathError::PathEscapesBoundary`: Root moved outside the boundary between checks.
136    ///
137    /// EXAMPLE:
138    /// ```rust
139    /// # use strict_path::{VirtualPath, VirtualRoot};
140    /// # let root = std::env::temp_dir().join("into-virtualpath-example");
141    /// # std::fs::create_dir_all(&root)?;
142    /// let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
143    /// let root_virtual: VirtualPath = vroot.into_virtualpath()?;
144    /// assert_eq!(root_virtual.virtualpath_display().to_string(), "/");
145    /// # std::fs::remove_dir_all(&root)?;
146    /// # Ok::<_, Box<dyn std::error::Error>>(())
147    /// ```
148    #[inline]
149    pub fn into_virtualpath(self) -> Result<VirtualPath<Marker>> {
150        let strict_root = self.root.into_strictpath()?;
151        Ok(strict_root.virtualize())
152    }
153
154    /// SUMMARY:
155    /// Consume this virtual root and substitute a new marker type.
156    ///
157    /// DETAILS:
158    /// Mirrors [`crate::PathBoundary::change_marker`], [`crate::StrictPath::change_marker`], and
159    /// [`crate::VirtualPath::change_marker`]. Use this when encoding proven authorization
160    /// into the type system (e.g., after validating a user's permissions). The
161    /// consumption makes marker changes explicit during code review.
162    ///
163    /// PARAMETERS:
164    /// - `NewMarker` (type parameter): Marker to associate with the virtual root.
165    ///
166    /// RETURNS:
167    /// - `VirtualRoot<NewMarker>`: Same underlying root, rebranded with `NewMarker`.
168    ///
169    /// EXAMPLE:
170    /// ```rust
171    /// # use strict_path::VirtualRoot;
172    /// # let root_dir = std::env::temp_dir().join("vroot-change-marker-example");
173    /// # std::fs::create_dir_all(&root_dir)?;
174    /// struct UserFiles;
175    /// struct ReadOnly;
176    /// struct ReadWrite;
177    ///
178    /// let read_root: VirtualRoot<(UserFiles, ReadOnly)> = VirtualRoot::try_new(&root_dir)?;
179    ///
180    /// // After authorization check...
181    /// let write_root: VirtualRoot<(UserFiles, ReadWrite)> = read_root.change_marker();
182    /// # std::fs::remove_dir_all(&root_dir)?;
183    /// # Ok::<_, Box<dyn std::error::Error>>(())
184    /// ```
185    #[inline]
186    pub fn change_marker<NewMarker>(self) -> VirtualRoot<NewMarker> {
187        let VirtualRoot {
188            root,
189            #[cfg(feature = "tempfile")]
190            _temp_dir,
191            ..
192        } = self;
193
194        VirtualRoot {
195            root: root.change_marker(),
196            #[cfg(feature = "tempfile")]
197            _temp_dir,
198            _marker: PhantomData,
199        }
200    }
201
202    /// SUMMARY:
203    /// Create a symbolic link at `link_path` pointing to this root's underlying directory.
204    pub fn virtual_symlink(
205        &self,
206        link_path: &crate::path::virtual_path::VirtualPath<Marker>,
207    ) -> std::io::Result<()> {
208        let root = self
209            .root
210            .clone()
211            .into_strictpath()
212            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
213
214        root.strict_symlink(link_path.as_unvirtual())
215    }
216
217    /// SUMMARY:
218    /// Create a hard link at `link_path` pointing to this root's underlying directory.
219    pub fn virtual_hard_link(
220        &self,
221        link_path: &crate::path::virtual_path::VirtualPath<Marker>,
222    ) -> std::io::Result<()> {
223        let root = self
224            .root
225            .clone()
226            .into_strictpath()
227            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
228
229        root.strict_hard_link(link_path.as_unvirtual())
230    }
231
232    /// SUMMARY:
233    /// Read directory entries at the virtual root (discovery). Re‑join names through virtual/strict APIs before I/O.
234    #[inline]
235    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
236        self.root.read_dir()
237    }
238
239    /// SUMMARY:
240    /// Remove the underlying root directory (non‑recursive); fails if not empty.
241    #[inline]
242    pub fn remove_dir(&self) -> std::io::Result<()> {
243        self.root.remove_dir()
244    }
245
246    /// SUMMARY:
247    /// Recursively remove the underlying root directory and all its contents.
248    #[inline]
249    pub fn remove_dir_all(&self) -> std::io::Result<()> {
250        self.root.remove_dir_all()
251    }
252
253    /// SUMMARY:
254    /// Ensure the directory exists (create if missing), then return a `VirtualRoot`.
255    ///
256    /// EXAMPLE:
257    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
258    /// ```rust
259    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
260    /// use strict_path::VirtualRoot;
261    /// let tmp_dir = tempfile::tempdir()?;
262    /// let tmp_dir = VirtualRoot::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
263    /// # Ok(())
264    /// # }
265    /// ```
266    #[inline]
267    pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
268        let root = PathBoundary::try_new_create(root_path)?;
269        Ok(Self {
270            root,
271            #[cfg(feature = "tempfile")]
272            _temp_dir: None,
273            _marker: PhantomData,
274        })
275    }
276
277    /// SUMMARY:
278    /// Join a candidate path to this virtual root, producing a clamped `VirtualPath`.
279    ///
280    /// DETAILS:
281    /// This is the security gateway for virtual paths. Absolute paths (starting with `"/"`) are
282    /// automatically clamped to the virtual root, ensuring paths cannot escape the sandbox.
283    /// For example, `"/etc/config"` becomes `vroot/etc/config`, and traversal attempts like
284    /// `"../../../../etc/passwd"` are clamped to `vroot/etc/passwd`. This clamping behavior is
285    /// what makes the `virtual_` dimension safe for user-facing operations.
286    ///
287    /// PARAMETERS:
288    /// - `candidate_path` (`AsRef<Path>`): Virtual path to resolve and clamp. Absolute paths
289    ///   are interpreted relative to the virtual root, not the system root.
290    ///
291    /// RETURNS:
292    /// - `Result<VirtualPath<Marker>>`: Clamped, validated path within the virtual root.
293    ///
294    /// ERRORS:
295    /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
296    ///
297    /// EXAMPLE:
298    /// ```rust
299    /// # use strict_path::VirtualRoot;
300    /// # let td = tempfile::tempdir().unwrap();
301    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
302    ///
303    /// // Absolute paths are clamped to virtual root, not system root
304    /// let path1 = vroot.virtual_join("/etc/config")?;
305    /// assert_eq!(path1.virtualpath_display().to_string(), "/etc/config");
306    ///
307    /// // Traversal attempts are also clamped
308    /// let path2 = vroot.virtual_join("../../../etc/passwd")?;
309    /// assert_eq!(path2.virtualpath_display().to_string(), "/etc/passwd");
310    ///
311    /// // Both paths are safely within the virtual root on the actual filesystem
312    /// # Ok::<(), Box<dyn std::error::Error>>(())
313    /// ```
314    #[inline]
315    pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
316        // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
317        let user_candidate = candidate_path.as_ref().to_path_buf();
318        let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
319
320        // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
321        let validated = anchored.boundary_check(self.root.stated_path())?;
322
323        // 3) Construct a StrictPath directly and then virtualize
324        let jp = crate::path::strict_path::StrictPath::new(
325            std::sync::Arc::new(self.root.clone()),
326            validated,
327        );
328        Ok(jp.virtualize())
329    }
330
331    /// Returns the underlying path boundary root as a system path.
332    #[inline]
333    pub(crate) fn path(&self) -> &Path {
334        self.root.path()
335    }
336
337    /// SUMMARY:
338    /// Return the virtual root path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
339    #[inline]
340    pub fn interop_path(&self) -> &std::ffi::OsStr {
341        self.root.interop_path()
342    }
343
344    /// Returns true if the underlying path boundary root exists.
345    #[inline]
346    pub fn exists(&self) -> bool {
347        self.root.exists()
348    }
349
350    /// SUMMARY:
351    /// Borrow the underlying `PathBoundary`.
352    #[inline]
353    pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
354        &self.root
355    }
356
357    /// SUMMARY:
358    /// Consume this `VirtualRoot` and return the underlying `PathBoundary` (symmetry with `virtualize`).
359    #[inline]
360    pub fn unvirtual(self) -> PathBoundary<Marker> {
361        self.root
362    }
363
364    // OS Standard Directory Constructors
365    //
366    // Creates virtual roots in OS standard directories following platform conventions.
367    // Applications see clean virtual paths ("/config.toml") while the system manages
368    // the actual location (e.g., "~/.config/myapp/config.toml").
369
370    /// Creates a virtual root in the OS standard config directory.
371    ///
372    /// **Cross-Platform Behavior:**
373    /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
374    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
375    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
376    #[cfg(feature = "dirs")]
377    pub fn try_new_os_config(app_name: &str) -> Result<Self> {
378        let root = crate::PathBoundary::try_new_os_config(app_name)?;
379        Ok(Self {
380            root,
381            #[cfg(feature = "tempfile")]
382            _temp_dir: None,
383            _marker: PhantomData,
384        })
385    }
386
387    /// Creates a virtual root in the OS standard data directory.
388    #[cfg(feature = "dirs")]
389    pub fn try_new_os_data(app_name: &str) -> Result<Self> {
390        let root = crate::PathBoundary::try_new_os_data(app_name)?;
391        Ok(Self {
392            root,
393            #[cfg(feature = "tempfile")]
394            _temp_dir: None,
395            _marker: PhantomData,
396        })
397    }
398
399    /// Creates a virtual root in the OS standard cache directory.
400    #[cfg(feature = "dirs")]
401    pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
402        let root = crate::PathBoundary::try_new_os_cache(app_name)?;
403        Ok(Self {
404            root,
405            #[cfg(feature = "tempfile")]
406            _temp_dir: None,
407            _marker: PhantomData,
408        })
409    }
410
411    /// Creates a virtual root in the OS local config directory.
412    #[cfg(feature = "dirs")]
413    pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
414        let root = crate::PathBoundary::try_new_os_config_local(app_name)?;
415        Ok(Self {
416            root,
417            #[cfg(feature = "tempfile")]
418            _temp_dir: None,
419            _marker: PhantomData,
420        })
421    }
422
423    /// Creates a virtual root in the OS local data directory.
424    #[cfg(feature = "dirs")]
425    pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
426        let root = crate::PathBoundary::try_new_os_data_local(app_name)?;
427        Ok(Self {
428            root,
429            #[cfg(feature = "tempfile")]
430            _temp_dir: None,
431            _marker: PhantomData,
432        })
433    }
434
435    /// Creates a virtual root in the user's home directory.
436    #[cfg(feature = "dirs")]
437    pub fn try_new_os_home() -> Result<Self> {
438        let root = crate::PathBoundary::try_new_os_home()?;
439        Ok(Self {
440            root,
441            #[cfg(feature = "tempfile")]
442            _temp_dir: None,
443            _marker: PhantomData,
444        })
445    }
446
447    /// Creates a virtual root in the user's desktop directory.
448    #[cfg(feature = "dirs")]
449    pub fn try_new_os_desktop() -> Result<Self> {
450        let root = crate::PathBoundary::try_new_os_desktop()?;
451        Ok(Self {
452            root,
453            #[cfg(feature = "tempfile")]
454            _temp_dir: None,
455            _marker: PhantomData,
456        })
457    }
458
459    /// Creates a virtual root in the user's documents directory.
460    #[cfg(feature = "dirs")]
461    pub fn try_new_os_documents() -> Result<Self> {
462        let root = crate::PathBoundary::try_new_os_documents()?;
463        Ok(Self {
464            root,
465            #[cfg(feature = "tempfile")]
466            _temp_dir: None,
467            _marker: PhantomData,
468        })
469    }
470
471    /// Creates a virtual root in the user's downloads directory.
472    #[cfg(feature = "dirs")]
473    pub fn try_new_os_downloads() -> Result<Self> {
474        let root = crate::PathBoundary::try_new_os_downloads()?;
475        Ok(Self {
476            root,
477            #[cfg(feature = "tempfile")]
478            _temp_dir: None,
479            _marker: PhantomData,
480        })
481    }
482
483    /// Creates a virtual root in the user's pictures directory.
484    #[cfg(feature = "dirs")]
485    pub fn try_new_os_pictures() -> Result<Self> {
486        let root = crate::PathBoundary::try_new_os_pictures()?;
487        Ok(Self {
488            root,
489            #[cfg(feature = "tempfile")]
490            _temp_dir: None,
491            _marker: PhantomData,
492        })
493    }
494
495    /// Creates a virtual root in the user's music/audio directory.
496    #[cfg(feature = "dirs")]
497    pub fn try_new_os_audio() -> Result<Self> {
498        let root = crate::PathBoundary::try_new_os_audio()?;
499        Ok(Self {
500            root,
501            #[cfg(feature = "tempfile")]
502            _temp_dir: None,
503            _marker: PhantomData,
504        })
505    }
506
507    /// Creates a virtual root in the user's videos directory.
508    #[cfg(feature = "dirs")]
509    pub fn try_new_os_videos() -> Result<Self> {
510        let root = crate::PathBoundary::try_new_os_videos()?;
511        Ok(Self {
512            root,
513            #[cfg(feature = "tempfile")]
514            _temp_dir: None,
515            _marker: PhantomData,
516        })
517    }
518
519    /// Creates a virtual root in the OS executable directory (Linux only).
520    #[cfg(feature = "dirs")]
521    pub fn try_new_os_executables() -> Result<Self> {
522        let root = crate::PathBoundary::try_new_os_executables()?;
523        Ok(Self {
524            root,
525            #[cfg(feature = "tempfile")]
526            _temp_dir: None,
527            _marker: PhantomData,
528        })
529    }
530
531    /// Creates a virtual root in the OS runtime directory (Linux only).
532    #[cfg(feature = "dirs")]
533    pub fn try_new_os_runtime() -> Result<Self> {
534        let root = crate::PathBoundary::try_new_os_runtime()?;
535        Ok(Self {
536            root,
537            #[cfg(feature = "tempfile")]
538            _temp_dir: None,
539            _marker: PhantomData,
540        })
541    }
542
543    /// Creates a virtual root in the OS state directory (Linux only).
544    #[cfg(feature = "dirs")]
545    pub fn try_new_os_state(app_name: &str) -> Result<Self> {
546        let root = crate::PathBoundary::try_new_os_state(app_name)?;
547        Ok(Self {
548            root,
549            #[cfg(feature = "tempfile")]
550            _temp_dir: None,
551            _marker: PhantomData,
552        })
553    }
554
555    /// SUMMARY:
556    /// Create a virtual root using the `app-path` strategy (portable app‑relative directory),
557    /// optionally honoring an environment variable override.
558    ///
559    /// PARAMETERS:
560    /// - `subdir` (`AsRef<Path>`): Subdirectory path relative to the executable location (or to the
561    ///   directory specified by the environment override). Accepts any path‑like value via `AsRef<Path>`.
562    /// - `env_override` (Option<&str>): Optional environment variable name to check first; when set
563    ///   and the variable is present, its value is used as the root base instead of the executable directory.
564    ///
565    /// RETURNS:
566    /// - `Result<VirtualRoot<Marker>>`: Virtual root whose underlying `PathBoundary` is created if missing
567    ///   and proven safe; all subsequent `virtual_join` operations are clamped to this root.
568    ///
569    /// ERRORS:
570    /// - `StrictPathError::InvalidRestriction`: If `app-path` resolution fails or the directory cannot be created/validated.
571    ///
572    /// EXAMPLE:
573    /// ```rust
574    /// # #[cfg(feature = "app-path")] {
575    /// use strict_path::VirtualRoot;
576    ///
577    /// // Create ./data relative to the executable (portable layout)
578    /// let vroot = VirtualRoot::<()>::try_new_app_path("data", None)?;
579    /// let vp = vroot.virtual_join("docs/report.txt")?;
580    /// assert_eq!(vp.virtualpath_display().to_string(), "/docs/report.txt");
581    ///
582    /// // With environment override: respects MYAPP_DATA_DIR when set
583    /// let _vroot = VirtualRoot::<()>::try_new_app_path("data", Some("MYAPP_DATA_DIR"))?;
584    /// # }
585    /// # Ok::<(), Box<dyn std::error::Error>>(())
586    /// ```
587    #[cfg(feature = "app-path")]
588    pub fn try_new_app_path<P: AsRef<Path>>(subdir: P, env_override: Option<&str>) -> Result<Self> {
589        let root = crate::PathBoundary::try_new_app_path(subdir, env_override)?;
590        Ok(Self {
591            root,
592            #[cfg(feature = "tempfile")]
593            _temp_dir: None,
594            _marker: PhantomData,
595        })
596    }
597
598    /// SUMMARY:
599    /// Create a virtual root via `app-path`, always consulting a specific environment variable
600    /// before falling back to the executable‑relative directory.
601    ///
602    /// PARAMETERS:
603    /// - `subdir` (`AsRef<Path>`): Subdirectory path used with `app-path` resolution.
604    /// - `env_override` (&str): Environment variable name to check first for the root base.
605    ///
606    /// RETURNS:
607    /// - `Result<VirtualRoot<Marker>>`: New virtual root anchored using `app-path` semantics.
608    ///
609    /// ERRORS:
610    /// - `StrictPathError::InvalidRestriction`: If resolution fails or the directory can't be created/validated.
611    ///
612    /// EXAMPLE:
613    /// ```rust
614    /// # #[cfg(feature = "app-path")] {
615    /// use strict_path::VirtualRoot;
616    /// let _vroot = VirtualRoot::<()>::try_new_app_path_with_env("cache", "MYAPP_CACHE_DIR")?;
617    /// # }
618    /// # Ok::<(), Box<dyn std::error::Error>>(())
619    /// ```
620    #[cfg(feature = "app-path")]
621    pub fn try_new_app_path_with_env<P: AsRef<Path>>(
622        subdir: P,
623        env_override: &str,
624    ) -> Result<Self> {
625        Self::try_new_app_path(subdir, Some(env_override))
626    }
627}
628
629impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
630    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
631        write!(f, "{}", self.path().display())
632    }
633}
634
635impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
636    fn as_ref(&self) -> &Path {
637        self.path()
638    }
639}
640
641impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
642    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
643        f.debug_struct("VirtualRoot")
644            .field("root", &self.path())
645            .field("marker", &std::any::type_name::<Marker>())
646            .finish()
647    }
648}
649
650impl<Marker> Eq for VirtualRoot<Marker> {}
651
652impl<M1, M2> PartialEq<VirtualRoot<M2>> for VirtualRoot<M1> {
653    #[inline]
654    fn eq(&self, other: &VirtualRoot<M2>) -> bool {
655        self.path() == other.path()
656    }
657}
658
659impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
660    #[inline]
661    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
662        self.path().hash(state);
663    }
664}
665
666impl<Marker> PartialOrd for VirtualRoot<Marker> {
667    #[inline]
668    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
669        Some(self.cmp(other))
670    }
671}
672
673impl<Marker> Ord for VirtualRoot<Marker> {
674    #[inline]
675    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
676        self.path().cmp(other.path())
677    }
678}
679
680impl<M1, M2> PartialEq<crate::PathBoundary<M2>> for VirtualRoot<M1> {
681    #[inline]
682    fn eq(&self, other: &crate::PathBoundary<M2>) -> bool {
683        self.path() == other.path()
684    }
685}
686
687impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
688    #[inline]
689    fn eq(&self, other: &std::path::Path) -> bool {
690        // Compare as virtual root path (always "/")
691        // VirtualRoot represents the virtual "/" regardless of underlying system path
692        let other_str = other.to_string_lossy();
693
694        #[cfg(windows)]
695        let other_normalized = other_str.replace('\\', "/");
696        #[cfg(not(windows))]
697        let other_normalized = other_str.to_string();
698
699        let normalized_other = if other_normalized.starts_with('/') {
700            other_normalized
701        } else {
702            format!("/{}", other_normalized)
703        };
704
705        "/" == normalized_other
706    }
707}
708
709impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
710    #[inline]
711    fn eq(&self, other: &std::path::PathBuf) -> bool {
712        self.eq(other.as_path())
713    }
714}
715
716impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
717    #[inline]
718    fn eq(&self, other: &&std::path::Path) -> bool {
719        self.eq(*other)
720    }
721}
722
723impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
724    #[inline]
725    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
726        // Compare as virtual root path (always "/")
727        let other_str = other.to_string_lossy();
728
729        // Handle empty path specially - "/" is greater than ""
730        if other_str.is_empty() {
731            return Some(std::cmp::Ordering::Greater);
732        }
733
734        #[cfg(windows)]
735        let other_normalized = other_str.replace('\\', "/");
736        #[cfg(not(windows))]
737        let other_normalized = other_str.to_string();
738
739        let normalized_other = if other_normalized.starts_with('/') {
740            other_normalized
741        } else {
742            format!("/{}", other_normalized)
743        };
744
745        Some("/".cmp(&normalized_other))
746    }
747}
748
749impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
750    #[inline]
751    fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
752        self.partial_cmp(*other)
753    }
754}
755
756impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
757    #[inline]
758    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
759        self.partial_cmp(other.as_path())
760    }
761}
762
763impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
764    type Err = crate::StrictPathError;
765
766    /// Parse a VirtualRoot from a string path for universal ergonomics.
767    ///
768    /// Creates the directory if it doesn't exist, enabling seamless integration
769    /// with any string-parsing context (clap, config files, environment variables, etc.):
770    /// ```rust
771    /// # use strict_path::VirtualRoot;
772    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
773    /// let temp_dir = tempfile::tempdir()?;
774    /// let virtual_path = temp_dir.path().join("virtual_dir");
775    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
776    /// assert!(virtual_path.exists());
777    /// # Ok(())
778    /// # }
779    /// ```
780    #[inline]
781    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
782        Self::try_new_create(path)
783    }
784}