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    /// Create a symbolic link at `link_path` pointing to this root's underlying directory.
126    pub fn virtual_symlink(
127        &self,
128        link_path: &crate::path::virtual_path::VirtualPath<Marker>,
129    ) -> std::io::Result<()> {
130        let root = self
131            .virtual_join("")
132            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
133
134        root.as_unvirtual().strict_symlink(link_path.as_unvirtual())
135    }
136
137    /// SUMMARY:
138    /// Create a hard link at `link_path` pointing to this root's underlying directory.
139    pub fn virtual_hard_link(
140        &self,
141        link_path: &crate::path::virtual_path::VirtualPath<Marker>,
142    ) -> std::io::Result<()> {
143        let root = self
144            .virtual_join("")
145            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
146
147        root.as_unvirtual()
148            .strict_hard_link(link_path.as_unvirtual())
149    }
150
151    /// SUMMARY:
152    /// Read directory entries at the virtual root (discovery). Re‑join names through virtual/strict APIs before I/O.
153    #[inline]
154    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
155        self.root.read_dir()
156    }
157
158    /// SUMMARY:
159    /// Remove the underlying root directory (non‑recursive); fails if not empty.
160    #[inline]
161    pub fn remove_dir(&self) -> std::io::Result<()> {
162        self.root.remove_dir()
163    }
164
165    /// SUMMARY:
166    /// Recursively remove the underlying root directory and all its contents.
167    #[inline]
168    pub fn remove_dir_all(&self) -> std::io::Result<()> {
169        self.root.remove_dir_all()
170    }
171
172    /// SUMMARY:
173    /// Ensure the directory exists (create if missing), then return a `VirtualRoot`.
174    ///
175    /// EXAMPLE:
176    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
177    /// ```rust
178    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
179    /// use strict_path::VirtualRoot;
180    /// let tmp_dir = tempfile::tempdir()?;
181    /// let tmp_dir = VirtualRoot::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
182    /// # Ok(())
183    /// # }
184    /// ```
185    #[inline]
186    pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
187        let root = PathBoundary::try_new_create(root_path)?;
188        Ok(Self {
189            root,
190            #[cfg(feature = "tempfile")]
191            _temp_dir: None,
192            _marker: PhantomData,
193        })
194    }
195
196    /// SUMMARY:
197    /// Join a candidate path to this virtual root, producing a clamped `VirtualPath`.
198    ///
199    /// PARAMETERS:
200    /// - `candidate_path` (`AsRef<Path>`): Virtual path to resolve and clamp.
201    ///
202    /// RETURNS:
203    /// - `Result<VirtualPath<Marker>>`: Clamped, validated path within the virtual root.
204    ///
205    /// ERRORS:
206    /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
207    #[inline]
208    pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
209        // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
210        let user_candidate = candidate_path.as_ref().to_path_buf();
211        let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
212
213        // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
214        let validated = anchored.boundary_check(self.root.stated_path())?;
215
216        // 3) Construct a StrictPath directly and then virtualize
217        let jp = crate::path::strict_path::StrictPath::new(
218            std::sync::Arc::new(self.root.clone()),
219            validated,
220        );
221        Ok(jp.virtualize())
222    }
223
224    /// Returns the underlying path boundary root as a system path.
225    #[inline]
226    pub(crate) fn path(&self) -> &Path {
227        self.root.path()
228    }
229
230    /// SUMMARY:
231    /// Return the virtual root path as `&OsStr` for allocation‑free `AsRef<Path>` interop.
232    #[inline]
233    pub fn interop_path(&self) -> &std::ffi::OsStr {
234        self.root.interop_path()
235    }
236
237    /// Returns true if the underlying path boundary root exists.
238    #[inline]
239    pub fn exists(&self) -> bool {
240        self.root.exists()
241    }
242
243    /// SUMMARY:
244    /// Borrow the underlying `PathBoundary`.
245    #[inline]
246    pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
247        &self.root
248    }
249
250    /// SUMMARY:
251    /// Consume this `VirtualRoot` and return the underlying `PathBoundary` (symmetry with `virtualize`).
252    #[inline]
253    pub fn unvirtual(self) -> PathBoundary<Marker> {
254        self.root
255    }
256
257    // OS Standard Directory Constructors
258    //
259    // Creates virtual roots in OS standard directories following platform conventions.
260    // Applications see clean virtual paths ("/config.toml") while the system manages
261    // the actual location (e.g., "~/.config/myapp/config.toml").
262
263    /// Creates a virtual root in the OS standard config directory.
264    ///
265    /// **Cross-Platform Behavior:**
266    /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
267    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
268    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
269    #[cfg(feature = "dirs")]
270    pub fn try_new_os_config(app_name: &str) -> Result<Self> {
271        let root = crate::PathBoundary::try_new_os_config(app_name)?;
272        Ok(Self {
273            root,
274            #[cfg(feature = "tempfile")]
275            _temp_dir: None,
276            _marker: PhantomData,
277        })
278    }
279
280    /// Creates a virtual root in the OS standard data directory.
281    #[cfg(feature = "dirs")]
282    pub fn try_new_os_data(app_name: &str) -> Result<Self> {
283        let root = crate::PathBoundary::try_new_os_data(app_name)?;
284        Ok(Self {
285            root,
286            #[cfg(feature = "tempfile")]
287            _temp_dir: None,
288            _marker: PhantomData,
289        })
290    }
291
292    /// Creates a virtual root in the OS standard cache directory.
293    #[cfg(feature = "dirs")]
294    pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
295        let root = crate::PathBoundary::try_new_os_cache(app_name)?;
296        Ok(Self {
297            root,
298            #[cfg(feature = "tempfile")]
299            _temp_dir: None,
300            _marker: PhantomData,
301        })
302    }
303
304    /// Creates a virtual root in the OS local config directory.
305    #[cfg(feature = "dirs")]
306    pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
307        let root = crate::PathBoundary::try_new_os_config_local(app_name)?;
308        Ok(Self {
309            root,
310            #[cfg(feature = "tempfile")]
311            _temp_dir: None,
312            _marker: PhantomData,
313        })
314    }
315
316    /// Creates a virtual root in the OS local data directory.
317    #[cfg(feature = "dirs")]
318    pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
319        let root = crate::PathBoundary::try_new_os_data_local(app_name)?;
320        Ok(Self {
321            root,
322            #[cfg(feature = "tempfile")]
323            _temp_dir: None,
324            _marker: PhantomData,
325        })
326    }
327
328    /// Creates a virtual root in the user's home directory.
329    #[cfg(feature = "dirs")]
330    pub fn try_new_os_home() -> Result<Self> {
331        let root = crate::PathBoundary::try_new_os_home()?;
332        Ok(Self {
333            root,
334            #[cfg(feature = "tempfile")]
335            _temp_dir: None,
336            _marker: PhantomData,
337        })
338    }
339
340    /// Creates a virtual root in the user's desktop directory.
341    #[cfg(feature = "dirs")]
342    pub fn try_new_os_desktop() -> Result<Self> {
343        let root = crate::PathBoundary::try_new_os_desktop()?;
344        Ok(Self {
345            root,
346            #[cfg(feature = "tempfile")]
347            _temp_dir: None,
348            _marker: PhantomData,
349        })
350    }
351
352    /// Creates a virtual root in the user's documents directory.
353    #[cfg(feature = "dirs")]
354    pub fn try_new_os_documents() -> Result<Self> {
355        let root = crate::PathBoundary::try_new_os_documents()?;
356        Ok(Self {
357            root,
358            #[cfg(feature = "tempfile")]
359            _temp_dir: None,
360            _marker: PhantomData,
361        })
362    }
363
364    /// Creates a virtual root in the user's downloads directory.
365    #[cfg(feature = "dirs")]
366    pub fn try_new_os_downloads() -> Result<Self> {
367        let root = crate::PathBoundary::try_new_os_downloads()?;
368        Ok(Self {
369            root,
370            #[cfg(feature = "tempfile")]
371            _temp_dir: None,
372            _marker: PhantomData,
373        })
374    }
375
376    /// Creates a virtual root in the user's pictures directory.
377    #[cfg(feature = "dirs")]
378    pub fn try_new_os_pictures() -> Result<Self> {
379        let root = crate::PathBoundary::try_new_os_pictures()?;
380        Ok(Self {
381            root,
382            #[cfg(feature = "tempfile")]
383            _temp_dir: None,
384            _marker: PhantomData,
385        })
386    }
387
388    /// Creates a virtual root in the user's music/audio directory.
389    #[cfg(feature = "dirs")]
390    pub fn try_new_os_audio() -> Result<Self> {
391        let root = crate::PathBoundary::try_new_os_audio()?;
392        Ok(Self {
393            root,
394            #[cfg(feature = "tempfile")]
395            _temp_dir: None,
396            _marker: PhantomData,
397        })
398    }
399
400    /// Creates a virtual root in the user's videos directory.
401    #[cfg(feature = "dirs")]
402    pub fn try_new_os_videos() -> Result<Self> {
403        let root = crate::PathBoundary::try_new_os_videos()?;
404        Ok(Self {
405            root,
406            #[cfg(feature = "tempfile")]
407            _temp_dir: None,
408            _marker: PhantomData,
409        })
410    }
411
412    /// Creates a virtual root in the OS executable directory (Linux only).
413    #[cfg(feature = "dirs")]
414    pub fn try_new_os_executables() -> Result<Self> {
415        let root = crate::PathBoundary::try_new_os_executables()?;
416        Ok(Self {
417            root,
418            #[cfg(feature = "tempfile")]
419            _temp_dir: None,
420            _marker: PhantomData,
421        })
422    }
423
424    /// Creates a virtual root in the OS runtime directory (Linux only).
425    #[cfg(feature = "dirs")]
426    pub fn try_new_os_runtime() -> Result<Self> {
427        let root = crate::PathBoundary::try_new_os_runtime()?;
428        Ok(Self {
429            root,
430            #[cfg(feature = "tempfile")]
431            _temp_dir: None,
432            _marker: PhantomData,
433        })
434    }
435
436    /// Creates a virtual root in the OS state directory (Linux only).
437    #[cfg(feature = "dirs")]
438    pub fn try_new_os_state(app_name: &str) -> Result<Self> {
439        let root = crate::PathBoundary::try_new_os_state(app_name)?;
440        Ok(Self {
441            root,
442            #[cfg(feature = "tempfile")]
443            _temp_dir: None,
444            _marker: PhantomData,
445        })
446    }
447
448    /// SUMMARY:
449    /// Create a virtual root using the `app-path` strategy (portable app‑relative directory),
450    /// optionally honoring an environment variable override.
451    ///
452    /// PARAMETERS:
453    /// - `subdir` (`AsRef<Path>`): Subdirectory path relative to the executable location (or to the
454    ///   directory specified by the environment override). Accepts any path‑like value via `AsRef<Path>`.
455    /// - `env_override` (Option<&str>): Optional environment variable name to check first; when set
456    ///   and the variable is present, its value is used as the root base instead of the executable directory.
457    ///
458    /// RETURNS:
459    /// - `Result<VirtualRoot<Marker>>`: Virtual root whose underlying `PathBoundary` is created if missing
460    ///   and proven safe; all subsequent `virtual_join` operations are clamped to this root.
461    ///
462    /// ERRORS:
463    /// - `StrictPathError::InvalidRestriction`: If `app-path` resolution fails or the directory cannot be created/validated.
464    ///
465    /// EXAMPLE:
466    /// ```rust
467    /// # #[cfg(feature = "app-path")] {
468    /// use strict_path::VirtualRoot;
469    ///
470    /// // Create ./data relative to the executable (portable layout)
471    /// let vroot = VirtualRoot::<()>::try_new_app_path("data", None)?;
472    /// let vp = vroot.virtual_join("docs/report.txt")?;
473    /// assert_eq!(vp.virtualpath_display().to_string(), "/docs/report.txt");
474    ///
475    /// // With environment override: respects MYAPP_DATA_DIR when set
476    /// let _vroot = VirtualRoot::<()>::try_new_app_path("data", Some("MYAPP_DATA_DIR"))?;
477    /// # }
478    /// # Ok::<(), Box<dyn std::error::Error>>(())
479    /// ```
480    #[cfg(feature = "app-path")]
481    pub fn try_new_app_path<P: AsRef<Path>>(subdir: P, env_override: Option<&str>) -> Result<Self> {
482        let root = crate::PathBoundary::try_new_app_path(subdir, env_override)?;
483        Ok(Self {
484            root,
485            #[cfg(feature = "tempfile")]
486            _temp_dir: None,
487            _marker: PhantomData,
488        })
489    }
490
491    /// SUMMARY:
492    /// Create a virtual root via `app-path`, always consulting a specific environment variable
493    /// before falling back to the executable‑relative directory.
494    ///
495    /// PARAMETERS:
496    /// - `subdir` (`AsRef<Path>`): Subdirectory path used with `app-path` resolution.
497    /// - `env_override` (&str): Environment variable name to check first for the root base.
498    ///
499    /// RETURNS:
500    /// - `Result<VirtualRoot<Marker>>`: New virtual root anchored using `app-path` semantics.
501    ///
502    /// ERRORS:
503    /// - `StrictPathError::InvalidRestriction`: If resolution fails or the directory can't be created/validated.
504    ///
505    /// EXAMPLE:
506    /// ```rust
507    /// # #[cfg(feature = "app-path")] {
508    /// use strict_path::VirtualRoot;
509    /// let _vroot = VirtualRoot::<()>::try_new_app_path_with_env("cache", "MYAPP_CACHE_DIR")?;
510    /// # }
511    /// # Ok::<(), Box<dyn std::error::Error>>(())
512    /// ```
513    #[cfg(feature = "app-path")]
514    pub fn try_new_app_path_with_env<P: AsRef<Path>>(
515        subdir: P,
516        env_override: &str,
517    ) -> Result<Self> {
518        Self::try_new_app_path(subdir, Some(env_override))
519    }
520}
521
522impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
523    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524        write!(f, "{}", self.path().display())
525    }
526}
527
528impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
529    fn as_ref(&self) -> &Path {
530        self.path()
531    }
532}
533
534impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        f.debug_struct("VirtualRoot")
537            .field("root", &self.path())
538            .field("marker", &std::any::type_name::<Marker>())
539            .finish()
540    }
541}
542
543impl<Marker> PartialEq for VirtualRoot<Marker> {
544    #[inline]
545    fn eq(&self, other: &Self) -> bool {
546        self.path() == other.path()
547    }
548}
549
550impl<Marker> Eq for VirtualRoot<Marker> {}
551
552impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
553    #[inline]
554    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
555        self.path().hash(state);
556    }
557}
558
559impl<Marker> PartialOrd for VirtualRoot<Marker> {
560    #[inline]
561    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
562        Some(self.cmp(other))
563    }
564}
565
566impl<Marker> Ord for VirtualRoot<Marker> {
567    #[inline]
568    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
569        self.path().cmp(other.path())
570    }
571}
572
573impl<Marker> PartialEq<crate::PathBoundary<Marker>> for VirtualRoot<Marker> {
574    #[inline]
575    fn eq(&self, other: &crate::PathBoundary<Marker>) -> bool {
576        self.path() == other.path()
577    }
578}
579
580impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
581    #[inline]
582    fn eq(&self, other: &std::path::Path) -> bool {
583        // Compare as virtual root path (always "/")
584        // VirtualRoot represents the virtual "/" regardless of underlying system path
585        let other_str = other.to_string_lossy();
586
587        #[cfg(windows)]
588        let other_normalized = other_str.replace('\\', "/");
589        #[cfg(not(windows))]
590        let other_normalized = other_str.to_string();
591
592        let normalized_other = if other_normalized.starts_with('/') {
593            other_normalized
594        } else {
595            format!("/{}", other_normalized)
596        };
597
598        "/" == normalized_other
599    }
600}
601
602impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
603    #[inline]
604    fn eq(&self, other: &std::path::PathBuf) -> bool {
605        self.eq(other.as_path())
606    }
607}
608
609impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
610    #[inline]
611    fn eq(&self, other: &&std::path::Path) -> bool {
612        self.eq(*other)
613    }
614}
615
616impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
617    #[inline]
618    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
619        // Compare as virtual root path (always "/")
620        let other_str = other.to_string_lossy();
621
622        // Handle empty path specially - "/" is greater than ""
623        if other_str.is_empty() {
624            return Some(std::cmp::Ordering::Greater);
625        }
626
627        #[cfg(windows)]
628        let other_normalized = other_str.replace('\\', "/");
629        #[cfg(not(windows))]
630        let other_normalized = other_str.to_string();
631
632        let normalized_other = if other_normalized.starts_with('/') {
633            other_normalized
634        } else {
635            format!("/{}", other_normalized)
636        };
637
638        Some("/".cmp(&normalized_other))
639    }
640}
641
642impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
643    #[inline]
644    fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
645        self.partial_cmp(*other)
646    }
647}
648
649impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
650    #[inline]
651    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
652        self.partial_cmp(other.as_path())
653    }
654}
655
656impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
657    type Err = crate::StrictPathError;
658
659    /// Parse a VirtualRoot from a string path for universal ergonomics.
660    ///
661    /// Creates the directory if it doesn't exist, enabling seamless integration
662    /// with any string-parsing context (clap, config files, environment variables, etc.):
663    /// ```rust
664    /// # use strict_path::VirtualRoot;
665    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
666    /// let temp_dir = tempfile::tempdir()?;
667    /// let virtual_path = temp_dir.path().join("virtual_dir");
668    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
669    /// assert!(virtual_path.exists());
670    /// # Ok(())
671    /// # }
672    /// ```
673    #[inline]
674    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
675        Self::try_new_create(path)
676    }
677}