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}