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}