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    /// PARAMETERS:
281    /// - `candidate_path` (`AsRef<Path>`): Virtual path to resolve and clamp.
282    ///
283    /// RETURNS:
284    /// - `Result<VirtualPath<Marker>>`: Clamped, validated path within the virtual root.
285    ///
286    /// ERRORS:
287    /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
288    #[inline]
289    pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
290        // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
291        let user_candidate = candidate_path.as_ref().to_path_buf();
292        let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
293
294        // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
295        let validated = anchored.boundary_check(self.root.stated_path())?;
296
297        // 3) Construct a StrictPath directly and then virtualize
298        let jp = crate::path::strict_path::StrictPath::new(
299            std::sync::Arc::new(self.root.clone()),
300            validated,
301        );
302        Ok(jp.virtualize())
303    }
304
305    /// Returns the underlying path boundary root as a system path.
306    #[inline]
307    pub(crate) fn path(&self) -> &Path {
308        self.root.path()
309    }
310
311    /// SUMMARY:
312    /// Return the virtual root path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
313    #[inline]
314    pub fn interop_path(&self) -> &std::ffi::OsStr {
315        self.root.interop_path()
316    }
317
318    /// Returns true if the underlying path boundary root exists.
319    #[inline]
320    pub fn exists(&self) -> bool {
321        self.root.exists()
322    }
323
324    /// SUMMARY:
325    /// Borrow the underlying `PathBoundary`.
326    #[inline]
327    pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
328        &self.root
329    }
330
331    /// SUMMARY:
332    /// Consume this `VirtualRoot` and return the underlying `PathBoundary` (symmetry with `virtualize`).
333    #[inline]
334    pub fn unvirtual(self) -> PathBoundary<Marker> {
335        self.root
336    }
337
338    // OS Standard Directory Constructors
339    //
340    // Creates virtual roots in OS standard directories following platform conventions.
341    // Applications see clean virtual paths ("/config.toml") while the system manages
342    // the actual location (e.g., "~/.config/myapp/config.toml").
343
344    /// Creates a virtual root in the OS standard config directory.
345    ///
346    /// **Cross-Platform Behavior:**
347    /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
348    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
349    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
350    #[cfg(feature = "dirs")]
351    pub fn try_new_os_config(app_name: &str) -> Result<Self> {
352        let root = crate::PathBoundary::try_new_os_config(app_name)?;
353        Ok(Self {
354            root,
355            #[cfg(feature = "tempfile")]
356            _temp_dir: None,
357            _marker: PhantomData,
358        })
359    }
360
361    /// Creates a virtual root in the OS standard data directory.
362    #[cfg(feature = "dirs")]
363    pub fn try_new_os_data(app_name: &str) -> Result<Self> {
364        let root = crate::PathBoundary::try_new_os_data(app_name)?;
365        Ok(Self {
366            root,
367            #[cfg(feature = "tempfile")]
368            _temp_dir: None,
369            _marker: PhantomData,
370        })
371    }
372
373    /// Creates a virtual root in the OS standard cache directory.
374    #[cfg(feature = "dirs")]
375    pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
376        let root = crate::PathBoundary::try_new_os_cache(app_name)?;
377        Ok(Self {
378            root,
379            #[cfg(feature = "tempfile")]
380            _temp_dir: None,
381            _marker: PhantomData,
382        })
383    }
384
385    /// Creates a virtual root in the OS local config directory.
386    #[cfg(feature = "dirs")]
387    pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
388        let root = crate::PathBoundary::try_new_os_config_local(app_name)?;
389        Ok(Self {
390            root,
391            #[cfg(feature = "tempfile")]
392            _temp_dir: None,
393            _marker: PhantomData,
394        })
395    }
396
397    /// Creates a virtual root in the OS local data directory.
398    #[cfg(feature = "dirs")]
399    pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
400        let root = crate::PathBoundary::try_new_os_data_local(app_name)?;
401        Ok(Self {
402            root,
403            #[cfg(feature = "tempfile")]
404            _temp_dir: None,
405            _marker: PhantomData,
406        })
407    }
408
409    /// Creates a virtual root in the user's home directory.
410    #[cfg(feature = "dirs")]
411    pub fn try_new_os_home() -> Result<Self> {
412        let root = crate::PathBoundary::try_new_os_home()?;
413        Ok(Self {
414            root,
415            #[cfg(feature = "tempfile")]
416            _temp_dir: None,
417            _marker: PhantomData,
418        })
419    }
420
421    /// Creates a virtual root in the user's desktop directory.
422    #[cfg(feature = "dirs")]
423    pub fn try_new_os_desktop() -> Result<Self> {
424        let root = crate::PathBoundary::try_new_os_desktop()?;
425        Ok(Self {
426            root,
427            #[cfg(feature = "tempfile")]
428            _temp_dir: None,
429            _marker: PhantomData,
430        })
431    }
432
433    /// Creates a virtual root in the user's documents directory.
434    #[cfg(feature = "dirs")]
435    pub fn try_new_os_documents() -> Result<Self> {
436        let root = crate::PathBoundary::try_new_os_documents()?;
437        Ok(Self {
438            root,
439            #[cfg(feature = "tempfile")]
440            _temp_dir: None,
441            _marker: PhantomData,
442        })
443    }
444
445    /// Creates a virtual root in the user's downloads directory.
446    #[cfg(feature = "dirs")]
447    pub fn try_new_os_downloads() -> Result<Self> {
448        let root = crate::PathBoundary::try_new_os_downloads()?;
449        Ok(Self {
450            root,
451            #[cfg(feature = "tempfile")]
452            _temp_dir: None,
453            _marker: PhantomData,
454        })
455    }
456
457    /// Creates a virtual root in the user's pictures directory.
458    #[cfg(feature = "dirs")]
459    pub fn try_new_os_pictures() -> Result<Self> {
460        let root = crate::PathBoundary::try_new_os_pictures()?;
461        Ok(Self {
462            root,
463            #[cfg(feature = "tempfile")]
464            _temp_dir: None,
465            _marker: PhantomData,
466        })
467    }
468
469    /// Creates a virtual root in the user's music/audio directory.
470    #[cfg(feature = "dirs")]
471    pub fn try_new_os_audio() -> Result<Self> {
472        let root = crate::PathBoundary::try_new_os_audio()?;
473        Ok(Self {
474            root,
475            #[cfg(feature = "tempfile")]
476            _temp_dir: None,
477            _marker: PhantomData,
478        })
479    }
480
481    /// Creates a virtual root in the user's videos directory.
482    #[cfg(feature = "dirs")]
483    pub fn try_new_os_videos() -> Result<Self> {
484        let root = crate::PathBoundary::try_new_os_videos()?;
485        Ok(Self {
486            root,
487            #[cfg(feature = "tempfile")]
488            _temp_dir: None,
489            _marker: PhantomData,
490        })
491    }
492
493    /// Creates a virtual root in the OS executable directory (Linux only).
494    #[cfg(feature = "dirs")]
495    pub fn try_new_os_executables() -> Result<Self> {
496        let root = crate::PathBoundary::try_new_os_executables()?;
497        Ok(Self {
498            root,
499            #[cfg(feature = "tempfile")]
500            _temp_dir: None,
501            _marker: PhantomData,
502        })
503    }
504
505    /// Creates a virtual root in the OS runtime directory (Linux only).
506    #[cfg(feature = "dirs")]
507    pub fn try_new_os_runtime() -> Result<Self> {
508        let root = crate::PathBoundary::try_new_os_runtime()?;
509        Ok(Self {
510            root,
511            #[cfg(feature = "tempfile")]
512            _temp_dir: None,
513            _marker: PhantomData,
514        })
515    }
516
517    /// Creates a virtual root in the OS state directory (Linux only).
518    #[cfg(feature = "dirs")]
519    pub fn try_new_os_state(app_name: &str) -> Result<Self> {
520        let root = crate::PathBoundary::try_new_os_state(app_name)?;
521        Ok(Self {
522            root,
523            #[cfg(feature = "tempfile")]
524            _temp_dir: None,
525            _marker: PhantomData,
526        })
527    }
528
529    /// SUMMARY:
530    /// Create a virtual root using the `app-path` strategy (portable app‑relative directory),
531    /// optionally honoring an environment variable override.
532    ///
533    /// PARAMETERS:
534    /// - `subdir` (`AsRef<Path>`): Subdirectory path relative to the executable location (or to the
535    ///   directory specified by the environment override). Accepts any path‑like value via `AsRef<Path>`.
536    /// - `env_override` (Option<&str>): Optional environment variable name to check first; when set
537    ///   and the variable is present, its value is used as the root base instead of the executable directory.
538    ///
539    /// RETURNS:
540    /// - `Result<VirtualRoot<Marker>>`: Virtual root whose underlying `PathBoundary` is created if missing
541    ///   and proven safe; all subsequent `virtual_join` operations are clamped to this root.
542    ///
543    /// ERRORS:
544    /// - `StrictPathError::InvalidRestriction`: If `app-path` resolution fails or the directory cannot be created/validated.
545    ///
546    /// EXAMPLE:
547    /// ```rust
548    /// # #[cfg(feature = "app-path")] {
549    /// use strict_path::VirtualRoot;
550    ///
551    /// // Create ./data relative to the executable (portable layout)
552    /// let vroot = VirtualRoot::<()>::try_new_app_path("data", None)?;
553    /// let vp = vroot.virtual_join("docs/report.txt")?;
554    /// assert_eq!(vp.virtualpath_display().to_string(), "/docs/report.txt");
555    ///
556    /// // With environment override: respects MYAPP_DATA_DIR when set
557    /// let _vroot = VirtualRoot::<()>::try_new_app_path("data", Some("MYAPP_DATA_DIR"))?;
558    /// # }
559    /// # Ok::<(), Box<dyn std::error::Error>>(())
560    /// ```
561    #[cfg(feature = "app-path")]
562    pub fn try_new_app_path<P: AsRef<Path>>(subdir: P, env_override: Option<&str>) -> Result<Self> {
563        let root = crate::PathBoundary::try_new_app_path(subdir, env_override)?;
564        Ok(Self {
565            root,
566            #[cfg(feature = "tempfile")]
567            _temp_dir: None,
568            _marker: PhantomData,
569        })
570    }
571
572    /// SUMMARY:
573    /// Create a virtual root via `app-path`, always consulting a specific environment variable
574    /// before falling back to the executable‑relative directory.
575    ///
576    /// PARAMETERS:
577    /// - `subdir` (`AsRef<Path>`): Subdirectory path used with `app-path` resolution.
578    /// - `env_override` (&str): Environment variable name to check first for the root base.
579    ///
580    /// RETURNS:
581    /// - `Result<VirtualRoot<Marker>>`: New virtual root anchored using `app-path` semantics.
582    ///
583    /// ERRORS:
584    /// - `StrictPathError::InvalidRestriction`: If resolution fails or the directory can't be created/validated.
585    ///
586    /// EXAMPLE:
587    /// ```rust
588    /// # #[cfg(feature = "app-path")] {
589    /// use strict_path::VirtualRoot;
590    /// let _vroot = VirtualRoot::<()>::try_new_app_path_with_env("cache", "MYAPP_CACHE_DIR")?;
591    /// # }
592    /// # Ok::<(), Box<dyn std::error::Error>>(())
593    /// ```
594    #[cfg(feature = "app-path")]
595    pub fn try_new_app_path_with_env<P: AsRef<Path>>(
596        subdir: P,
597        env_override: &str,
598    ) -> Result<Self> {
599        Self::try_new_app_path(subdir, Some(env_override))
600    }
601}
602
603impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
604    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
605        write!(f, "{}", self.path().display())
606    }
607}
608
609impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
610    fn as_ref(&self) -> &Path {
611        self.path()
612    }
613}
614
615impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
616    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617        f.debug_struct("VirtualRoot")
618            .field("root", &self.path())
619            .field("marker", &std::any::type_name::<Marker>())
620            .finish()
621    }
622}
623
624impl<Marker> Eq for VirtualRoot<Marker> {}
625
626impl<M1, M2> PartialEq<VirtualRoot<M2>> for VirtualRoot<M1> {
627    #[inline]
628    fn eq(&self, other: &VirtualRoot<M2>) -> bool {
629        self.path() == other.path()
630    }
631}
632
633impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
634    #[inline]
635    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
636        self.path().hash(state);
637    }
638}
639
640impl<Marker> PartialOrd for VirtualRoot<Marker> {
641    #[inline]
642    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
643        Some(self.cmp(other))
644    }
645}
646
647impl<Marker> Ord for VirtualRoot<Marker> {
648    #[inline]
649    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
650        self.path().cmp(other.path())
651    }
652}
653
654impl<M1, M2> PartialEq<crate::PathBoundary<M2>> for VirtualRoot<M1> {
655    #[inline]
656    fn eq(&self, other: &crate::PathBoundary<M2>) -> bool {
657        self.path() == other.path()
658    }
659}
660
661impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
662    #[inline]
663    fn eq(&self, other: &std::path::Path) -> bool {
664        // Compare as virtual root path (always "/")
665        // VirtualRoot represents the virtual "/" regardless of underlying system path
666        let other_str = other.to_string_lossy();
667
668        #[cfg(windows)]
669        let other_normalized = other_str.replace('\\', "/");
670        #[cfg(not(windows))]
671        let other_normalized = other_str.to_string();
672
673        let normalized_other = if other_normalized.starts_with('/') {
674            other_normalized
675        } else {
676            format!("/{}", other_normalized)
677        };
678
679        "/" == normalized_other
680    }
681}
682
683impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
684    #[inline]
685    fn eq(&self, other: &std::path::PathBuf) -> bool {
686        self.eq(other.as_path())
687    }
688}
689
690impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
691    #[inline]
692    fn eq(&self, other: &&std::path::Path) -> bool {
693        self.eq(*other)
694    }
695}
696
697impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
698    #[inline]
699    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
700        // Compare as virtual root path (always "/")
701        let other_str = other.to_string_lossy();
702
703        // Handle empty path specially - "/" is greater than ""
704        if other_str.is_empty() {
705            return Some(std::cmp::Ordering::Greater);
706        }
707
708        #[cfg(windows)]
709        let other_normalized = other_str.replace('\\', "/");
710        #[cfg(not(windows))]
711        let other_normalized = other_str.to_string();
712
713        let normalized_other = if other_normalized.starts_with('/') {
714            other_normalized
715        } else {
716            format!("/{}", other_normalized)
717        };
718
719        Some("/".cmp(&normalized_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        self.partial_cmp(*other)
727    }
728}
729
730impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
731    #[inline]
732    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
733        self.partial_cmp(other.as_path())
734    }
735}
736
737impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
738    type Err = crate::StrictPathError;
739
740    /// Parse a VirtualRoot from a string path for universal ergonomics.
741    ///
742    /// Creates the directory if it doesn't exist, enabling seamless integration
743    /// with any string-parsing context (clap, config files, environment variables, etc.):
744    /// ```rust
745    /// # use strict_path::VirtualRoot;
746    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
747    /// let temp_dir = tempfile::tempdir()?;
748    /// let virtual_path = temp_dir.path().join("virtual_dir");
749    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
750    /// assert!(virtual_path.exists());
751    /// # Ok(())
752    /// # }
753    /// ```
754    #[inline]
755    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
756        Self::try_new_create(path)
757    }
758}