strict_path/path/
virtual_path.rs

1// Content copied from original src/path/virtual_path.rs
2use crate::error::StrictPathError;
3use crate::path::strict_path::StrictPath;
4use crate::validator::path_history::{Canonicalized, PathHistory};
5use crate::PathBoundary;
6use crate::Result;
7use std::ffi::OsStr;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10use std::path::{Path, PathBuf};
11
12/// SUMMARY:
13/// Hold a user‑facing path clamped to a virtual root (`"/"`) over a `PathBoundary`.
14///
15/// DETAILS:
16/// `virtualpath_display()` shows rooted, forward‑slashed paths (e.g., `"/a/b.txt"`).
17/// Use virtual manipulation methods to compose paths while preserving clamping, then convert to
18/// `StrictPath` with `unvirtual()` for system‑facing I/O.
19#[derive(Clone)]
20pub struct VirtualPath<Marker = ()> {
21    inner: StrictPath<Marker>,
22    virtual_path: PathBuf,
23}
24
25#[inline]
26fn clamp<Marker, H>(
27    restriction: &PathBoundary<Marker>,
28    anchored: PathHistory<(H, Canonicalized)>,
29) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
30    restriction.strict_join(anchored.into_inner())
31}
32
33impl<Marker> VirtualPath<Marker> {
34    /// SUMMARY:
35    /// Create the virtual root (`"/"`) for the given filesystem root.
36    pub fn with_root<P: AsRef<Path>>(root: P) -> Result<Self> {
37        let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
38        vroot.into_virtualpath()
39    }
40
41    /// SUMMARY:
42    /// Create the virtual root, creating the filesystem root if missing.
43    pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
44        let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
45        vroot.into_virtualpath()
46    }
47    #[inline]
48    pub(crate) fn new(strict_path: StrictPath<Marker>) -> Self {
49        fn compute_virtual<Marker>(
50            system_path: &std::path::Path,
51            restriction: &crate::PathBoundary<Marker>,
52        ) -> std::path::PathBuf {
53            use std::ffi::OsString;
54            use std::path::Component;
55
56            #[cfg(windows)]
57            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
58                let s = p.as_os_str().to_string_lossy();
59                if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
60                    return std::path::PathBuf::from(trimmed);
61                }
62                if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
63                    return std::path::PathBuf::from(trimmed);
64                }
65                std::path::PathBuf::from(s.to_string())
66            }
67
68            #[cfg(not(windows))]
69            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
70                p.to_path_buf()
71            }
72
73            let system_norm = strip_verbatim(system_path);
74            let jail_norm = strip_verbatim(restriction.path());
75
76            if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
77                let mut cleaned = std::path::PathBuf::new();
78                for comp in stripped.components() {
79                    if let Component::Normal(name) = comp {
80                        let s = name.to_string_lossy();
81                        let cleaned_s = s.replace(['\n', ';'], "_");
82                        if cleaned_s == s {
83                            cleaned.push(name);
84                        } else {
85                            cleaned.push(OsString::from(cleaned_s));
86                        }
87                    }
88                }
89                return cleaned;
90            }
91
92            let mut strictpath_comps: Vec<_> = system_norm
93                .components()
94                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
95                .collect();
96            let mut boundary_comps: Vec<_> = jail_norm
97                .components()
98                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
99                .collect();
100
101            #[cfg(windows)]
102            fn comp_eq(a: &Component, b: &Component) -> bool {
103                match (a, b) {
104                    (Component::Normal(x), Component::Normal(y)) => {
105                        x.to_string_lossy().to_ascii_lowercase()
106                            == y.to_string_lossy().to_ascii_lowercase()
107                    }
108                    _ => false,
109                }
110            }
111
112            #[cfg(not(windows))]
113            fn comp_eq(a: &Component, b: &Component) -> bool {
114                a == b
115            }
116
117            while !strictpath_comps.is_empty()
118                && !boundary_comps.is_empty()
119                && comp_eq(&strictpath_comps[0], &boundary_comps[0])
120            {
121                strictpath_comps.remove(0);
122                boundary_comps.remove(0);
123            }
124
125            let mut vb = std::path::PathBuf::new();
126            for c in strictpath_comps {
127                if let Component::Normal(name) = c {
128                    let s = name.to_string_lossy();
129                    let cleaned = s.replace(['\n', ';'], "_");
130                    if cleaned == s {
131                        vb.push(name);
132                    } else {
133                        vb.push(OsString::from(cleaned));
134                    }
135                }
136            }
137            vb
138        }
139
140        let virtual_path = compute_virtual(strict_path.path(), strict_path.boundary());
141
142        Self {
143            inner: strict_path,
144            virtual_path,
145        }
146    }
147
148    /// SUMMARY:
149    /// Convert this `VirtualPath` back into a system‑facing `StrictPath`.
150    #[inline]
151    pub fn unvirtual(self) -> StrictPath<Marker> {
152        self.inner
153    }
154
155    /// SUMMARY:
156    /// Change the compile-time marker while keeping the virtual and strict views in sync.
157    ///
158    /// WHEN TO USE:
159    /// - After authenticating/authorizing a user and granting them access to a virtual path
160    /// - When escalating or downgrading permissions (e.g., ReadOnly → ReadWrite)
161    /// - When reinterpreting a path's domain (e.g., TempStorage → UserUploads)
162    ///
163    /// WHEN NOT TO USE:
164    /// - When converting between path types - conversions preserve markers automatically
165    /// - When the current marker already matches your needs - no transformation needed
166    /// - When you haven't verified authorization - NEVER change markers without checking permissions
167    ///
168    /// PARAMETERS:
169    /// - `_none_`
170    ///
171    /// RETURNS:
172    /// - `VirtualPath<NewMarker>`: Same clamped path encoded with the new marker.
173    ///
174    /// ERRORS:
175    /// - `_none_`
176    ///
177    /// SECURITY:
178    /// This method performs no permission checks. Only elevate markers after verifying real
179    /// authorization out-of-band.
180    ///
181    /// EXAMPLE:
182    /// ```rust
183    /// # use strict_path::VirtualPath;
184    /// # struct GuestAccess;
185    /// # struct UserAccess;
186    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-example");
187    /// # std::fs::create_dir_all(&root_dir)?;
188    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir)?;
189    /// // Simulated authorization: verify user credentials before granting access
190    /// fn grant_user_access(user_token: &str, path: VirtualPath<GuestAccess>) -> Option<VirtualPath<UserAccess>> {
191    ///     if user_token == "valid-token-12345" {
192    ///         Some(path.change_marker())  // ✅ Only after token validation
193    ///     } else {
194    ///         None  // ❌ Invalid token
195    ///     }
196    /// }
197    ///
198    /// // Untrusted input from request/CLI/config/etc.
199    /// let requested_file = "docs/readme.md";
200    /// let guest_path: VirtualPath<GuestAccess> = guest_root.virtual_join(requested_file)?;
201    /// let user_path = grant_user_access("valid-token-12345", guest_path).expect("authorized");
202    /// assert_eq!(user_path.virtualpath_display().to_string(), "/docs/readme.md");
203    /// # std::fs::remove_dir_all(&root_dir)?;
204    /// # Ok::<_, Box<dyn std::error::Error>>(())
205    /// ```
206    ///
207    /// **Type Safety Guarantee:**
208    ///
209    /// The following code **fails to compile** because you cannot pass a path with one marker
210    /// type to a function expecting a different marker type. This compile-time check enforces
211    /// that permission changes are explicit and cannot be bypassed accidentally.
212    ///
213    /// ```compile_fail
214    /// # use strict_path::VirtualPath;
215    /// # struct GuestAccess;
216    /// # struct EditorAccess;
217    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-deny");
218    /// # std::fs::create_dir_all(&root_dir).unwrap();
219    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir).unwrap();
220    /// fn require_editor(_: VirtualPath<EditorAccess>) {}
221    /// let guest_file = guest_root.virtual_join("docs/manual.txt").unwrap();
222    /// // ❌ Compile error: expected `VirtualPath<EditorAccess>`, found `VirtualPath<GuestAccess>`
223    /// require_editor(guest_file);
224    /// ```
225    #[inline]
226    pub fn change_marker<NewMarker>(self) -> VirtualPath<NewMarker> {
227        let VirtualPath {
228            inner,
229            virtual_path,
230        } = self;
231
232        VirtualPath {
233            inner: inner.change_marker(),
234            virtual_path,
235        }
236    }
237
238    /// SUMMARY:
239    /// Consume and return the `VirtualRoot` for its boundary (no directory creation).
240    ///
241    /// RETURNS:
242    /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory.
243    ///
244    /// ERRORS:
245    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` when the
246    ///   strict path does not exist or is not a directory.
247    #[inline]
248    pub fn try_into_root(self) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
249        Ok(self.inner.try_into_boundary()?.virtualize())
250    }
251
252    /// SUMMARY:
253    /// Consume and return a `VirtualRoot`, creating the underlying directory if missing.
254    ///
255    /// RETURNS:
256    /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory
257    ///   (created if necessary).
258    ///
259    /// ERRORS:
260    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` or directory
261    ///   creation failures wrapped in `InvalidRestriction`.
262    #[inline]
263    pub fn try_into_root_create(
264        self,
265    ) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
266        let strict_path = self.inner;
267        let boundary = strict_path.try_into_boundary_create()?;
268        Ok(boundary.virtualize())
269    }
270
271    /// SUMMARY:
272    /// Borrow the underlying system‑facing `StrictPath` (no allocation).
273    #[inline]
274    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
275        &self.inner
276    }
277
278    /// SUMMARY:
279    /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
280    #[inline]
281    pub fn interop_path(&self) -> &OsStr {
282        self.inner.interop_path()
283    }
284
285    /// SUMMARY:
286    /// Join a virtual path segment (virtual semantics) and re‑validate within the same restriction.
287    ///
288    /// DETAILS:
289    /// Applies virtual path clamping: absolute paths are interpreted relative to the virtual root,
290    /// and traversal attempts are clamped to prevent escaping the boundary. This method maintains
291    /// the security guarantee that all `VirtualPath` instances stay within their virtual root.
292    ///
293    /// PARAMETERS:
294    /// - `path` (`impl AsRef<Path>`): Path segment to join. Absolute paths are clamped to virtual root.
295    ///
296    /// RETURNS:
297    /// - `Result<VirtualPath<Marker>>`: New virtual path within the same restriction.
298    ///
299    /// EXAMPLE:
300    /// ```rust
301    /// # use strict_path::VirtualRoot;
302    /// # let td = tempfile::tempdir().unwrap();
303    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
304    /// let base = vroot.virtual_join("data")?;
305    ///
306    /// // Absolute paths are clamped to virtual root
307    /// let abs = base.virtual_join("/etc/config")?;
308    /// assert_eq!(abs.virtualpath_display().to_string(), "/etc/config");
309    /// # Ok::<(), Box<dyn std::error::Error>>(())
310    /// ```
311    #[inline]
312    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
313        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
314        let candidate = self.virtual_path.join(path.as_ref());
315        let anchored = crate::validator::path_history::PathHistory::new(candidate)
316            .canonicalize_anchored(self.inner.boundary())?;
317        let boundary_path = clamp(self.inner.boundary(), anchored)?;
318        Ok(VirtualPath::new(boundary_path))
319    }
320
321    // No local clamping helpers; virtual flows should route through
322    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
323
324    /// SUMMARY:
325    /// Return the parent virtual path, or `None` at the virtual root.
326    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
327        match self.virtual_path.parent() {
328            Some(parent_virtual_path) => {
329                let anchored = crate::validator::path_history::PathHistory::new(
330                    parent_virtual_path.to_path_buf(),
331                )
332                .canonicalize_anchored(self.inner.boundary())?;
333                let validated_path = clamp(self.inner.boundary(), anchored)?;
334                Ok(Some(VirtualPath::new(validated_path)))
335            }
336            None => Ok(None),
337        }
338    }
339
340    /// SUMMARY:
341    /// Return a new virtual path with file name changed, preserving clamping.
342    #[inline]
343    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
344        let candidate = self.virtual_path.with_file_name(file_name);
345        let anchored = crate::validator::path_history::PathHistory::new(candidate)
346            .canonicalize_anchored(self.inner.boundary())?;
347        let validated_path = clamp(self.inner.boundary(), anchored)?;
348        Ok(VirtualPath::new(validated_path))
349    }
350
351    /// SUMMARY:
352    /// Return a new virtual path with the extension changed, preserving clamping.
353    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
354        if self.virtual_path.file_name().is_none() {
355            return Err(StrictPathError::path_escapes_boundary(
356                self.virtual_path.clone(),
357                self.inner.boundary().path().to_path_buf(),
358            ));
359        }
360
361        let candidate = self.virtual_path.with_extension(extension);
362        let anchored = crate::validator::path_history::PathHistory::new(candidate)
363            .canonicalize_anchored(self.inner.boundary())?;
364        let validated_path = clamp(self.inner.boundary(), anchored)?;
365        Ok(VirtualPath::new(validated_path))
366    }
367
368    /// SUMMARY:
369    /// Return the file name component of the virtual path, if any.
370    #[inline]
371    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
372        self.virtual_path.file_name()
373    }
374
375    /// SUMMARY:
376    /// Return the file stem of the virtual path, if any.
377    #[inline]
378    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
379        self.virtual_path.file_stem()
380    }
381
382    /// SUMMARY:
383    /// Return the extension of the virtual path, if any.
384    #[inline]
385    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
386        self.virtual_path.extension()
387    }
388
389    /// SUMMARY:
390    /// Return `true` if the virtual path starts with the given prefix (virtual semantics).
391    #[inline]
392    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
393        self.virtual_path.starts_with(p)
394    }
395
396    /// SUMMARY:
397    /// Return `true` if the virtual path ends with the given suffix (virtual semantics).
398    #[inline]
399    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
400        self.virtual_path.ends_with(p)
401    }
402
403    /// SUMMARY:
404    /// Return a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt").
405    #[inline]
406    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
407        VirtualPathDisplay(self)
408    }
409
410    /// SUMMARY:
411    /// Return `true` if the underlying system path exists.
412    #[inline]
413    pub fn exists(&self) -> bool {
414        self.inner.exists()
415    }
416
417    /// SUMMARY:
418    /// Return `true` if the underlying system path is a file.
419    #[inline]
420    pub fn is_file(&self) -> bool {
421        self.inner.is_file()
422    }
423
424    /// SUMMARY:
425    /// Return `true` if the underlying system path is a directory.
426    #[inline]
427    pub fn is_dir(&self) -> bool {
428        self.inner.is_dir()
429    }
430
431    /// SUMMARY:
432    /// Return metadata for the underlying system path.
433    #[inline]
434    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
435        self.inner.metadata()
436    }
437
438    /// SUMMARY:
439    /// Read the file contents as `String` from the underlying system path.
440    #[inline]
441    pub fn read_to_string(&self) -> std::io::Result<String> {
442        self.inner.read_to_string()
443    }
444
445    /// SUMMARY:
446    /// Read raw bytes from the underlying system path.
447    #[inline]
448    pub fn read(&self) -> std::io::Result<Vec<u8>> {
449        self.inner.read()
450    }
451
452    /// SUMMARY:
453    /// Return metadata for the underlying system path without following symlinks.
454    #[inline]
455    pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
456        self.inner.symlink_metadata()
457    }
458
459    /// SUMMARY:
460    /// Set permissions on the file or directory at this path.
461    ///
462    /// PARAMETERS:
463    /// - `perm` (`std::fs::Permissions`): The permissions to set.
464    ///
465    /// RETURNS:
466    /// - `io::Result<()>`: Success or I/O error.
467    #[inline]
468    pub fn set_permissions(&self, perm: std::fs::Permissions) -> std::io::Result<()> {
469        self.inner.set_permissions(perm)
470    }
471
472    /// SUMMARY:
473    /// Check if the path exists, returning an error on permission issues.
474    ///
475    /// DETAILS:
476    /// Unlike `exists()` which returns `false` on permission errors, this method
477    /// distinguishes between "path does not exist" (`Ok(false)`) and "cannot check
478    /// due to permission error" (`Err(...)`).
479    ///
480    /// RETURNS:
481    /// - `Ok(true)`: Path exists
482    /// - `Ok(false)`: Path does not exist
483    /// - `Err(...)`: Permission or other I/O error prevented the check
484    #[inline]
485    pub fn try_exists(&self) -> std::io::Result<bool> {
486        self.inner.try_exists()
487    }
488
489    /// SUMMARY:
490    /// Create an empty file if it doesn't exist, or update the modification time if it does.
491    ///
492    /// DETAILS:
493    /// This is a convenience method combining file creation and mtime update.
494    /// Uses `OpenOptions` with `create(true).write(true)` which creates the file
495    /// if missing or opens it for writing if it exists, updating mtime on close.
496    ///
497    /// RETURNS:
498    /// - `io::Result<()>`: Success or I/O error.
499    pub fn touch(&self) -> std::io::Result<()> {
500        self.inner.touch()
501    }
502
503    /// SUMMARY:
504    /// Read directory entries (discovery). Re‑join names with `virtual_join(...)` to preserve clamping.
505    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
506        self.inner.read_dir()
507    }
508
509    /// SUMMARY:
510    /// Read directory entries as validated `VirtualPath` values (auto re-joins each entry).
511    ///
512    /// DETAILS:
513    /// Unlike `read_dir()` which returns raw `std::fs::DirEntry`, this method automatically
514    /// validates each directory entry through `virtual_join()`, returning an iterator of
515    /// `Result<VirtualPath<Marker>>`. This eliminates the need for manual re-validation loops
516    /// while preserving the virtual path semantics.
517    ///
518    /// PARAMETERS:
519    /// - _none_
520    ///
521    /// RETURNS:
522    /// - `io::Result<VirtualReadDir<Marker>>`: Iterator yielding validated `VirtualPath` entries.
523    ///
524    /// ERRORS:
525    /// - `std::io::Error`: If the directory cannot be read.
526    /// - Each yielded item may also be `Err` if validation fails for that entry.
527    ///
528    /// EXAMPLE:
529    /// ```rust
530    /// # use strict_path::{VirtualRoot, VirtualPath};
531    /// # let temp = tempfile::tempdir()?;
532    /// # let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
533    /// # let dir = vroot.virtual_join("uploads")?;
534    /// # dir.create_dir_all()?;
535    /// # vroot.virtual_join("uploads/file1.txt")?.write("a")?;
536    /// # vroot.virtual_join("uploads/file2.txt")?.write("b")?;
537    /// // Iterate with automatic validation
538    /// for entry in dir.virtual_read_dir()? {
539    ///     let child: VirtualPath = entry?;
540    ///     println!("{}", child.virtualpath_display());
541    /// }
542    /// # Ok::<_, Box<dyn std::error::Error>>(())
543    /// ```
544    pub fn virtual_read_dir(&self) -> std::io::Result<VirtualReadDir<'_, Marker>> {
545        let inner = std::fs::read_dir(self.inner.path())?;
546        Ok(VirtualReadDir {
547            inner,
548            parent: self,
549        })
550    }
551
552    /// SUMMARY:
553    /// Write bytes to the underlying system path. Accepts `&str`, `String`, `&[u8]`, `Vec<u8]`, etc.
554    #[inline]
555    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
556        self.inner.write(contents)
557    }
558
559    /// SUMMARY:
560    /// Append bytes to the underlying system path (create if missing). Accepts `&str`, `&[u8]`, etc.
561    ///
562    /// PARAMETERS:
563    /// - `data` (`AsRef<[u8]>`): Bytes to append to the file.
564    ///
565    /// RETURNS:
566    /// - `()`: Returns nothing on success.
567    ///
568    /// ERRORS:
569    /// - `std::io::Error`: Propagates OS errors when the file cannot be opened or written.
570    ///
571    /// EXAMPLE:
572    /// ```rust
573    /// # use strict_path::VirtualRoot;
574    /// # let root = std::env::temp_dir().join("strict-path-vpath-append");
575    /// # std::fs::create_dir_all(&root)?;
576    /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
577    /// // Untrusted input from request/CLI/config/etc.
578    /// let log_file = "logs/activity.log";
579    /// let vpath = vroot.virtual_join(log_file)?;
580    /// vpath.create_parent_dir_all()?;
581    /// vpath.append("[2025-01-01] Operation A\n")?;
582    /// vpath.append("[2025-01-01] Operation B\n")?;
583    /// let contents = vpath.read_to_string()?;
584    /// assert!(contents.contains("Operation A"));
585    /// assert!(contents.contains("Operation B"));
586    /// # std::fs::remove_dir_all(&root)?;
587    /// # Ok::<_, Box<dyn std::error::Error>>(())
588    /// ```
589    #[inline]
590    pub fn append<C: AsRef<[u8]>>(&self, data: C) -> std::io::Result<()> {
591        self.inner.append(data)
592    }
593
594    /// SUMMARY:
595    /// Create or truncate the file at this virtual path and return a writable handle.
596    ///
597    /// PARAMETERS:
598    /// - _none_
599    ///
600    /// RETURNS:
601    /// - `std::fs::File`: Writable handle scoped to the same virtual root restriction.
602    ///
603    /// ERRORS:
604    /// - `std::io::Error`: Propagates operating-system errors when the parent directory is missing or file creation fails.
605    ///
606    /// EXAMPLE:
607    /// ```rust
608    /// # use strict_path::VirtualRoot;
609    /// # use std::io::Write;
610    /// # let root = std::env::temp_dir().join("strict-path-virtual-create-file");
611    /// # std::fs::create_dir_all(&root)?;
612    /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
613    /// let report = vroot.virtual_join("reports/summary.txt")?;
614    /// report.create_parent_dir_all()?;
615    /// let mut file = report.create_file()?;
616    /// file.write_all(b"summary")?;
617    /// # std::fs::remove_dir_all(&root)?;
618    /// # Ok::<_, Box<dyn std::error::Error>>(())
619    /// ```
620    #[inline]
621    pub fn create_file(&self) -> std::io::Result<std::fs::File> {
622        self.inner.create_file()
623    }
624
625    /// SUMMARY:
626    /// Open the file at this virtual path in read-only mode.
627    ///
628    /// PARAMETERS:
629    /// - _none_
630    ///
631    /// RETURNS:
632    /// - `std::fs::File`: Read-only handle scoped to the same virtual root restriction.
633    ///
634    /// ERRORS:
635    /// - `std::io::Error`: Propagates operating-system errors when the file is missing or inaccessible.
636    ///
637    /// EXAMPLE:
638    /// ```rust
639    /// # use strict_path::VirtualRoot;
640    /// # use std::io::{Read, Write};
641    /// # let root = std::env::temp_dir().join("strict-path-virtual-open-file");
642    /// # std::fs::create_dir_all(&root)?;
643    /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
644    /// let report = vroot.virtual_join("reports/summary.txt")?;
645    /// report.create_parent_dir_all()?;
646    /// report.write("summary")?;
647    /// let mut file = report.open_file()?;
648    /// let mut contents = String::new();
649    /// file.read_to_string(&mut contents)?;
650    /// assert_eq!(contents, "summary");
651    /// # std::fs::remove_dir_all(&root)?;
652    /// # Ok::<_, Box<dyn std::error::Error>>(())
653    /// ```
654    #[inline]
655    pub fn open_file(&self) -> std::io::Result<std::fs::File> {
656        self.inner.open_file()
657    }
658
659    /// SUMMARY:
660    /// Return an options builder for advanced file opening (read+write, append, exclusive create, etc.).
661    ///
662    /// PARAMETERS:
663    /// - _none_
664    ///
665    /// RETURNS:
666    /// - `StrictOpenOptions<Marker>`: Builder to configure file opening options.
667    ///
668    /// EXAMPLE:
669    /// ```rust
670    /// # use strict_path::VirtualRoot;
671    /// # use std::io::{Read, Write, Seek, SeekFrom};
672    /// # let root = std::env::temp_dir().join("vpath-open-with-example");
673    /// # std::fs::create_dir_all(&root)?;
674    /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
675    /// // Untrusted input from request/CLI/config/etc.
676    /// let data_file = "cache/state.bin";
677    /// let cache_path = vroot.virtual_join(data_file)?;
678    /// cache_path.create_parent_dir_all()?;
679    ///
680    /// // Open with read+write access, create if missing
681    /// let mut file = cache_path.open_with()
682    ///     .read(true)
683    ///     .write(true)
684    ///     .create(true)
685    ///     .open()?;
686    /// file.write_all(b"state")?;
687    /// file.seek(SeekFrom::Start(0))?;
688    /// let mut buf = [0u8; 5];
689    /// file.read_exact(&mut buf)?;
690    /// assert_eq!(&buf, b"state");
691    /// # std::fs::remove_dir_all(&root)?;
692    /// # Ok::<_, Box<dyn std::error::Error>>(())
693    /// ```
694    #[inline]
695    pub fn open_with(&self) -> crate::path::strict_path::StrictOpenOptions<'_, Marker> {
696        self.inner.open_with()
697    }
698
699    /// SUMMARY:
700    /// Create all directories in the underlying system path if missing.
701    #[inline]
702    pub fn create_dir_all(&self) -> std::io::Result<()> {
703        self.inner.create_dir_all()
704    }
705
706    /// SUMMARY:
707    /// Create the directory at this virtual location (non‑recursive). Fails if parent missing.
708    #[inline]
709    pub fn create_dir(&self) -> std::io::Result<()> {
710        self.inner.create_dir()
711    }
712
713    /// SUMMARY:
714    /// Create only the immediate parent of this virtual path (non‑recursive). `Ok(())` at virtual root.
715    #[inline]
716    pub fn create_parent_dir(&self) -> std::io::Result<()> {
717        match self.virtualpath_parent() {
718            Ok(Some(parent)) => parent.create_dir(),
719            Ok(None) => Ok(()),
720            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
721            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
722        }
723    }
724
725    /// SUMMARY:
726    /// Recursively create all missing directories up to the immediate parent. `Ok(())` at virtual root.
727    #[inline]
728    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
729        match self.virtualpath_parent() {
730            Ok(Some(parent)) => parent.create_dir_all(),
731            Ok(None) => Ok(()),
732            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
733            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
734        }
735    }
736
737    /// SUMMARY:
738    /// Remove the file at the underlying system path.
739    #[inline]
740    pub fn remove_file(&self) -> std::io::Result<()> {
741        self.inner.remove_file()
742    }
743
744    /// SUMMARY:
745    /// Remove the directory at the underlying system path.
746    #[inline]
747    pub fn remove_dir(&self) -> std::io::Result<()> {
748        self.inner.remove_dir()
749    }
750
751    /// SUMMARY:
752    /// Recursively remove the directory and its contents at the underlying system path.
753    #[inline]
754    pub fn remove_dir_all(&self) -> std::io::Result<()> {
755        self.inner.remove_dir_all()
756    }
757
758    /// SUMMARY:
759    /// Create a symlink at `link_path` pointing to this virtual path (same virtual root required).
760    ///
761    /// DETAILS:
762    /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
763    /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/config"`
764    /// passed to `virtual_join()` are automatically clamped to `vroot/etc/config`, ensuring symlinks
765    /// cannot escape the virtual root boundary.
766    ///
767    /// EXAMPLE:
768    /// ```rust
769    /// # use strict_path::VirtualRoot;
770    /// # let td = tempfile::tempdir().unwrap();
771    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
772    ///
773    /// // Create target file
774    /// let target = vroot.virtual_join("/etc/config/app.conf")?;
775    /// target.create_parent_dir_all()?;
776    /// target.write(b"config data")?;
777    ///
778    /// // Ensure link parent directory exists (Windows requires this for symlink creation)
779    /// let link = vroot.virtual_join("/links/config.link")?;
780    /// link.create_parent_dir_all()?;
781    ///
782    /// // Create symlink - may fail on Windows without Developer Mode/admin privileges
783    /// if let Err(e) = target.virtual_symlink("/links/config.link") {
784    ///     // Skip test if we don't have symlink privileges (Windows ERROR_PRIVILEGE_NOT_HELD = 1314)
785    ///     #[cfg(windows)]
786    ///     if e.raw_os_error() == Some(1314) { return Ok(()); }
787    ///     return Err(e.into());
788    /// }
789    ///
790    /// assert_eq!(link.read_to_string()?, "config data");
791    /// # Ok::<(), Box<dyn std::error::Error>>(())
792    /// ```
793    pub fn virtual_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
794        let link_ref = link_path.as_ref();
795        let validated_link = if link_ref.is_absolute() {
796            match self.virtual_join(link_ref) {
797                Ok(p) => p,
798                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
799            }
800        } else {
801            // Resolve as sibling
802            let parent = match self.virtualpath_parent() {
803                Ok(Some(p)) => p,
804                Ok(None) => match self
805                    .inner
806                    .boundary()
807                    .clone()
808                    .virtualize()
809                    .into_virtualpath()
810                {
811                    Ok(root) => root,
812                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
813                },
814                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
815            };
816            match parent.virtual_join(link_ref) {
817                Ok(p) => p,
818                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
819            }
820        };
821
822        self.inner.strict_symlink(validated_link.inner.path())
823    }
824
825    /// SUMMARY:
826    /// Read the target of a symbolic link and return it as a validated `VirtualPath`.
827    ///
828    /// DESIGN NOTE:
829    /// This method has limited practical use because `virtual_join` resolves symlinks
830    /// during canonicalization. A `VirtualPath` obtained via `virtual_join("/link")` already
831    /// points to the symlink's target, not the symlink itself.
832    ///
833    /// To read a symlink target before validation, use `std::fs::read_link` on the raw
834    /// path, then validate the target with `virtual_join`:
835    ///
836    /// EXAMPLE:
837    /// ```rust
838    /// use strict_path::VirtualRoot;
839    ///
840    /// let temp = tempfile::tempdir()?;
841    /// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
842    ///
843    /// // Create a target file
844    /// let target = vroot.virtual_join("/data/target.txt")?;
845    /// target.create_parent_dir_all()?;
846    /// target.write("secret")?;
847    ///
848    /// // Create symlink (may fail on Windows without Developer Mode)
849    /// if target.virtual_symlink("/data/link.txt").is_ok() {
850    ///     // virtual_join resolves symlinks: link.txt -> target.txt
851    ///     let resolved = vroot.virtual_join("/data/link.txt")?;
852    ///     assert_eq!(resolved.virtualpath_display().to_string(), "/data/target.txt");
853    ///     // The resolved path reads the target file's content
854    ///     assert_eq!(resolved.read_to_string()?, "secret");
855    /// }
856    /// # Ok::<(), Box<dyn std::error::Error>>(())
857    /// ```
858    pub fn virtual_read_link(&self) -> std::io::Result<Self> {
859        // Read the raw symlink target
860        let raw_target = std::fs::read_link(self.inner.path())?;
861
862        // If the target is relative, resolve it relative to the symlink's parent
863        let resolved_target = if raw_target.is_relative() {
864            match self.inner.path().parent() {
865                Some(parent) => parent.join(&raw_target),
866                None => raw_target,
867            }
868        } else {
869            raw_target
870        };
871
872        // Validate through virtual_join which clamps escapes
873        // We need to compute the relative path from the virtual root
874        let vroot = self.inner.boundary().clone().virtualize();
875        vroot
876            .virtual_join(resolved_target)
877            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
878    }
879
880    /// SUMMARY:
881    /// Create a hard link at `link_path` pointing to this virtual path (same virtual root required).
882    ///
883    /// DETAILS:
884    /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
885    /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/data"`
886    /// passed to `virtual_join()` are automatically clamped to `vroot/etc/data`, ensuring hard links
887    /// cannot escape the virtual root boundary.
888    ///
889    /// EXAMPLE:
890    /// ```rust
891    /// # use strict_path::VirtualRoot;
892    /// # let td = tempfile::tempdir().unwrap();
893    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
894    ///
895    /// // Create target file
896    /// let target = vroot.virtual_join("/shared/data.dat")?;
897    /// target.create_parent_dir_all()?;
898    /// target.write(b"shared data")?;
899    ///
900    /// // Ensure link parent directory exists (Windows requires this for hard link creation)
901    /// let link = vroot.virtual_join("/backup/data.dat")?;
902    /// link.create_parent_dir_all()?;
903    ///
904    /// // Create hard link
905    /// target.virtual_hard_link("/backup/data.dat")?;
906    ///
907    /// // Read through link path, verify through target (hard link behavior)
908    /// link.write(b"modified")?;
909    /// assert_eq!(target.read_to_string()?, "modified");
910    /// # Ok::<(), Box<dyn std::error::Error>>(())
911    /// ```
912    pub fn virtual_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
913        let link_ref = link_path.as_ref();
914        let validated_link = if link_ref.is_absolute() {
915            match self.virtual_join(link_ref) {
916                Ok(p) => p,
917                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
918            }
919        } else {
920            // Resolve as sibling
921            let parent = match self.virtualpath_parent() {
922                Ok(Some(p)) => p,
923                Ok(None) => match self
924                    .inner
925                    .boundary()
926                    .clone()
927                    .virtualize()
928                    .into_virtualpath()
929                {
930                    Ok(root) => root,
931                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
932                },
933                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
934            };
935            match parent.virtual_join(link_ref) {
936                Ok(p) => p,
937                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
938            }
939        };
940
941        self.inner.strict_hard_link(validated_link.inner.path())
942    }
943
944    /// SUMMARY:
945    /// Create a Windows NTFS directory junction at `link_path` pointing to this virtual path.
946    ///
947    /// DETAILS:
948    /// - Windows-only and behind the `junctions` feature.
949    /// - Directory-only semantics; both paths must share the same virtual root.
950    #[cfg(all(windows, feature = "junctions"))]
951    pub fn virtual_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
952        // Mirror virtual semantics used by symlink/hard-link helpers:
953        // - Absolute paths are interpreted in the VIRTUAL namespace and clamped to this root
954        // - Relative paths are resolved as siblings (or from the virtual root when at root)
955        let link_ref = link_path.as_ref();
956        let validated_link = if link_ref.is_absolute() {
957            match self.virtual_join(link_ref) {
958                Ok(p) => p,
959                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
960            }
961        } else {
962            let parent = match self.virtualpath_parent() {
963                Ok(Some(p)) => p,
964                Ok(None) => match self
965                    .inner
966                    .boundary()
967                    .clone()
968                    .virtualize()
969                    .into_virtualpath()
970                {
971                    Ok(root) => root,
972                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
973                },
974                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
975            };
976            match parent.virtual_join(link_ref) {
977                Ok(p) => p,
978                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
979            }
980        };
981
982        // Delegate to strict helper after validating link location in virtual space
983        self.inner.strict_junction(validated_link.inner.path())
984    }
985
986    /// SUMMARY:
987    /// Rename/move within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
988    ///
989    /// DETAILS:
990    /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
991    /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
992    /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
993    /// Parent directories are not created automatically.
994    ///
995    /// PARAMETERS:
996    /// - `dest` (`impl AsRef<Path>`): Destination path. Absolute paths like `"/archive/file.txt"`
997    ///   are clamped to `vroot/archive/file.txt`.
998    ///
999    /// EXAMPLE:
1000    /// ```rust
1001    /// # use strict_path::VirtualRoot;
1002    /// # let td = tempfile::tempdir().unwrap();
1003    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
1004    ///
1005    /// let source = vroot.virtual_join("temp/file.txt")?;
1006    /// source.create_parent_dir_all()?;
1007    /// source.write(b"content")?;
1008    ///
1009    /// // Absolute destination path is clamped to virtual root
1010    /// let dest_dir = vroot.virtual_join("/archive")?;
1011    /// dest_dir.create_dir_all()?;
1012    /// source.virtual_rename("/archive/file.txt")?;
1013    ///
1014    /// let renamed = vroot.virtual_join("/archive/file.txt")?;
1015    /// assert_eq!(renamed.read_to_string()?, "content");
1016    /// # Ok::<(), Box<dyn std::error::Error>>(())
1017    /// ```
1018    pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
1019        let dest_ref = dest.as_ref();
1020        let dest_v = if dest_ref.is_absolute() {
1021            match self.virtual_join(dest_ref) {
1022                Ok(p) => p,
1023                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1024            }
1025        } else {
1026            // Resolve as sibling under the current virtual parent (or root if at "/")
1027            let parent = match self.virtualpath_parent() {
1028                Ok(Some(p)) => p,
1029                Ok(None) => match self
1030                    .inner
1031                    .boundary()
1032                    .clone()
1033                    .virtualize()
1034                    .into_virtualpath()
1035                {
1036                    Ok(root) => root,
1037                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1038                },
1039                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1040            };
1041            match parent.virtual_join(dest_ref) {
1042                Ok(p) => p,
1043                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1044            }
1045        };
1046
1047        // Perform the actual rename via StrictPath
1048        self.inner.strict_rename(dest_v.inner.path())
1049    }
1050
1051    /// SUMMARY:
1052    /// Copy within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
1053    ///
1054    /// DETAILS:
1055    /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
1056    /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
1057    /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
1058    /// Parent directories are not created automatically. Returns the number of bytes copied.
1059    ///
1060    /// PARAMETERS:
1061    /// - `dest` (`impl AsRef<Path>`): Destination path. Absolute paths like `"/backup/file.txt"`
1062    ///   are clamped to `vroot/backup/file.txt`.
1063    ///
1064    /// RETURNS:
1065    /// - `u64`: Number of bytes copied.
1066    ///
1067    /// EXAMPLE:
1068    /// ```rust
1069    /// # use strict_path::VirtualRoot;
1070    /// # let td = tempfile::tempdir().unwrap();
1071    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
1072    ///
1073    /// let source = vroot.virtual_join("data/source.txt")?;
1074    /// source.create_parent_dir_all()?;
1075    /// source.write(b"data to copy")?;
1076    ///
1077    /// // Absolute destination path is clamped to virtual root
1078    /// let dest_dir = vroot.virtual_join("/backup")?;
1079    /// dest_dir.create_dir_all()?;
1080    /// let bytes = source.virtual_copy("/backup/copy.txt")?;
1081    ///
1082    /// let copied = vroot.virtual_join("/backup/copy.txt")?;
1083    /// assert_eq!(copied.read_to_string()?, "data to copy");
1084    /// assert_eq!(bytes, 12);
1085    /// # Ok::<(), Box<dyn std::error::Error>>(())
1086    /// ```
1087    pub fn virtual_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
1088        let dest_ref = dest.as_ref();
1089        let dest_v = if dest_ref.is_absolute() {
1090            match self.virtual_join(dest_ref) {
1091                Ok(p) => p,
1092                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1093            }
1094        } else {
1095            // Resolve as sibling under the current virtual parent (or root if at "/")
1096            let parent = match self.virtualpath_parent() {
1097                Ok(Some(p)) => p,
1098                Ok(None) => match self
1099                    .inner
1100                    .boundary()
1101                    .clone()
1102                    .virtualize()
1103                    .into_virtualpath()
1104                {
1105                    Ok(root) => root,
1106                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1107                },
1108                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1109            };
1110            match parent.virtual_join(dest_ref) {
1111                Ok(p) => p,
1112                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1113            }
1114        };
1115
1116        // Perform the actual copy via StrictPath
1117        std::fs::copy(self.inner.path(), dest_v.inner.path())
1118    }
1119}
1120
1121pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
1122
1123impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
1124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1125        // Ensure leading slash and normalize to forward slashes for display
1126        let s_lossy = self.0.virtual_path.to_string_lossy();
1127        let s_norm: std::borrow::Cow<'_, str> = {
1128            #[cfg(windows)]
1129            {
1130                std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
1131            }
1132            #[cfg(not(windows))]
1133            {
1134                std::borrow::Cow::Borrowed(&s_lossy)
1135            }
1136        };
1137        if s_norm.starts_with('/') {
1138            write!(f, "{s_norm}")
1139        } else {
1140            write!(f, "/{s_norm}")
1141        }
1142    }
1143}
1144
1145impl<Marker> fmt::Debug for VirtualPath<Marker> {
1146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1147        f.debug_struct("VirtualPath")
1148            .field("system_path", &self.inner.path())
1149            .field("virtual", &format!("{}", self.virtualpath_display()))
1150            .field("boundary", &self.inner.boundary().path())
1151            .field("marker", &std::any::type_name::<Marker>())
1152            .finish()
1153    }
1154}
1155
1156impl<Marker> PartialEq for VirtualPath<Marker> {
1157    #[inline]
1158    fn eq(&self, other: &Self) -> bool {
1159        self.inner.path() == other.inner.path()
1160    }
1161}
1162
1163impl<Marker> Eq for VirtualPath<Marker> {}
1164
1165impl<Marker> Hash for VirtualPath<Marker> {
1166    #[inline]
1167    fn hash<H: Hasher>(&self, state: &mut H) {
1168        self.inner.path().hash(state);
1169    }
1170}
1171
1172impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
1173    #[inline]
1174    fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
1175        self.inner.path() == other.path()
1176    }
1177}
1178
1179impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
1180    #[inline]
1181    fn eq(&self, other: &T) -> bool {
1182        // Compare virtual paths - the user-facing representation
1183        // If you want system path comparison, use as_unvirtual()
1184        let virtual_str = format!("{}", self.virtualpath_display());
1185        let other_str = other.as_ref().to_string_lossy();
1186
1187        // Normalize both to forward slashes and ensure leading slash
1188        let normalized_virtual = virtual_str.as_str();
1189
1190        #[cfg(windows)]
1191        let other_normalized = other_str.replace('\\', "/");
1192        #[cfg(not(windows))]
1193        let other_normalized = other_str.to_string();
1194
1195        let normalized_other = if other_normalized.starts_with('/') {
1196            other_normalized
1197        } else {
1198            format!("/{}", other_normalized)
1199        };
1200
1201        normalized_virtual == normalized_other
1202    }
1203}
1204
1205impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
1206    #[inline]
1207    fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
1208        // Compare virtual paths - the user-facing representation
1209        let virtual_str = format!("{}", self.virtualpath_display());
1210        let other_str = other.as_ref().to_string_lossy();
1211
1212        // Normalize both to forward slashes and ensure leading slash
1213        let normalized_virtual = virtual_str.as_str();
1214
1215        #[cfg(windows)]
1216        let other_normalized = other_str.replace('\\', "/");
1217        #[cfg(not(windows))]
1218        let other_normalized = other_str.to_string();
1219
1220        let normalized_other = if other_normalized.starts_with('/') {
1221            other_normalized
1222        } else {
1223            format!("/{}", other_normalized)
1224        };
1225
1226        Some(normalized_virtual.cmp(&normalized_other))
1227    }
1228}
1229
1230impl<Marker> PartialOrd for VirtualPath<Marker> {
1231    #[inline]
1232    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1233        Some(self.cmp(other))
1234    }
1235}
1236
1237impl<Marker> Ord for VirtualPath<Marker> {
1238    #[inline]
1239    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1240        self.inner.path().cmp(other.inner.path())
1241    }
1242}
1243
1244// ============================================================
1245// VirtualReadDir — Iterator for validated virtual directory entries
1246// ============================================================
1247
1248/// SUMMARY:
1249/// Iterator over directory entries that yields validated `VirtualPath` values.
1250///
1251/// DETAILS:
1252/// Created by `VirtualPath::virtual_read_dir()`. Each iteration automatically validates
1253/// the directory entry through `virtual_join()`, so you get `VirtualPath` values directly
1254/// instead of raw `std::fs::DirEntry` that would require manual re-validation.
1255///
1256/// EXAMPLE:
1257/// ```rust
1258/// # use strict_path::{VirtualRoot, VirtualPath};
1259/// # let temp = tempfile::tempdir()?;
1260/// # let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
1261/// # let dir = vroot.virtual_join("assets")?;
1262/// # dir.create_dir_all()?;
1263/// # vroot.virtual_join("assets/logo.png")?.write(b"PNG")?;
1264/// for entry in dir.virtual_read_dir()? {
1265///     let child: VirtualPath = entry?;
1266///     if child.is_file() {
1267///         println!("File: {}", child.virtualpath_display());
1268///     }
1269/// }
1270/// # Ok::<_, Box<dyn std::error::Error>>(())
1271/// ```
1272pub struct VirtualReadDir<'a, Marker> {
1273    inner: std::fs::ReadDir,
1274    parent: &'a VirtualPath<Marker>,
1275}
1276
1277impl<Marker> std::fmt::Debug for VirtualReadDir<'_, Marker> {
1278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1279        f.debug_struct("VirtualReadDir")
1280            .field("parent", &self.parent.virtualpath_display().to_string())
1281            .finish_non_exhaustive()
1282    }
1283}
1284
1285impl<Marker: Clone> Iterator for VirtualReadDir<'_, Marker> {
1286    type Item = std::io::Result<VirtualPath<Marker>>;
1287
1288    fn next(&mut self) -> Option<Self::Item> {
1289        match self.inner.next()? {
1290            Ok(entry) => {
1291                let file_name = entry.file_name();
1292                match self.parent.virtual_join(file_name) {
1293                    Ok(virtual_path) => Some(Ok(virtual_path)),
1294                    Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
1295                }
1296            }
1297            Err(e) => Some(Err(e)),
1298        }
1299    }
1300}