Skip to main content

strict_path/path/virtual_path/
mod.rs

1//! `VirtualPath<Marker>` — a user-facing path clamped to a virtual root.
2//!
3//! `VirtualPath` wraps a `StrictPath` and adds a virtual path component rooted at `"/"`.
4//! The virtual path is what users see (e.g., `"/uploads/logo.png"`); the real system path
5//! is never exposed. Use `virtualpath_display()` for safe user-visible output and
6//! `as_unvirtual()` to obtain the underlying `StrictPath` for system-facing I/O.
7mod display;
8mod fs;
9mod iter;
10mod links;
11mod traits;
12
13pub use display::VirtualPathDisplay;
14pub use iter::VirtualReadDir;
15
16use crate::error::StrictPathError;
17use crate::path::strict_path::StrictPath;
18use crate::validator::path_history::{Canonicalized, PathHistory};
19use crate::PathBoundary;
20use crate::Result;
21use std::ffi::OsStr;
22use std::path::{Path, PathBuf};
23
24/// Hold a user‑facing path clamped to a virtual root (`"/"`) over a `PathBoundary`.
25///
26/// `virtualpath_display()` shows rooted, forward‑slashed paths (e.g., `"/a/b.txt"`).
27/// Use virtual manipulation methods to compose paths while preserving clamping, then convert to
28/// `StrictPath` with `unvirtual()` for system‑facing I/O.
29#[derive(Clone)]
30#[must_use = "a VirtualPath is boundary-validated and user-facing — use .virtualpath_display() for safe user output, .virtual_join() to compose paths, or .as_unvirtual() for system-facing I/O"]
31#[doc(alias = "jailed_path")]
32#[doc(alias = "sandboxed_path")]
33#[doc(alias = "contained_path")]
34pub struct VirtualPath<Marker = ()> {
35    pub(crate) inner: StrictPath<Marker>,
36    pub(crate) virtual_path: PathBuf,
37}
38
39/// Replace dangerous Unicode characters with `_` for safe Display output.
40///
41/// WHY: `virtualpath_display` is the one API surface whose output the crate
42/// actively encourages embedding in user-facing channels (HTTP responses,
43/// logs, terminal prints). The following categories are injection primitives:
44///   - C0 controls (< 0x20): `\n`/`\r` CRLF splitting, `\x1b` ANSI escapes, NUL
45///   - DEL (0x7F): terminal erase behavior on some emulators
46///   - C1 controls (0x80–0x9F): U+0085 NEL acts as newline in HTTP/XML/log parsers
47///   - U+2028/U+2029 (Line/Paragraph Separator): ECMAScript line terminators
48///   - Unicode directional overrides (U+202A–U+202E, U+2066–U+2069, U+200E/U+200F):
49///     visually reverse filename characters, enabling extension-spoofing attacks
50///   - `;` (semicolon): shell command separator — display output may feed a downstream shell reader
51fn sanitize_display_component(component: &str) -> String {
52    let mut out = String::with_capacity(component.len());
53    for ch in component.chars() {
54        let cp = ch as u32;
55        let needs_replace = ch == ';'
56            || cp < 0x20           // C0 controls (NUL through US)
57            || cp == 0x7F          // DEL
58            || (0x80..=0x9F).contains(&cp)  // C1 controls (NEL U+0085, CSI U+009B, etc.)
59            || cp == 0x2028        // LINE SEPARATOR  — ECMAScript line terminator
60            || cp == 0x2029        // PARAGRAPH SEPARATOR — ECMAScript line terminator
61            || cp == 0x200E        // LEFT-TO-RIGHT MARK
62            || cp == 0x200F        // RIGHT-TO-LEFT MARK
63            || (0x202A..=0x202E).contains(&cp)  // LRE, RLE, PDF, LRO, RLO (directional embedding/override)
64            || (0x2066..=0x2069).contains(&cp); // LRI, RLI, FSI, PDI (directional isolate)
65        if needs_replace {
66            out.push('_');
67        } else {
68            out.push(ch);
69        }
70    }
71    out
72}
73
74/// Re-validate a canonicalized path against the boundary after virtual-space manipulation.
75///
76/// Every virtual mutation (join, parent, with_*) produces a candidate in virtual space that
77/// must be re-anchored and boundary-checked before it becomes a real `StrictPath`. This
78/// centralizes that re-validation so each caller does not duplicate the check.
79#[inline]
80fn clamp<Marker, H>(
81    restriction: &PathBoundary<Marker>,
82    anchored: PathHistory<(H, Canonicalized)>,
83) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
84    restriction.strict_join(anchored.into_inner())
85}
86
87impl<Marker> VirtualPath<Marker> {
88    /// Create the virtual root (`"/"`) for the given filesystem root.
89    #[must_use = "this returns a Result containing the validated VirtualPath — handle the Result to detect invalid roots"]
90    pub fn with_root<P: AsRef<Path>>(root: P) -> Result<Self> {
91        let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
92        vroot.into_virtualpath()
93    }
94
95    /// Create the virtual root, creating the filesystem root if missing.
96    #[must_use = "this returns a Result containing the validated VirtualPath — handle the Result to detect invalid roots"]
97    pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
98        let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
99        vroot.into_virtualpath()
100    }
101
102    #[inline]
103    pub(crate) fn new(strict_path: StrictPath<Marker>) -> Self {
104        /// Derive the user-facing virtual path from the real system path and boundary.
105        ///
106        /// WHY: Users must never see the real host path (leaks tenant IDs, infra details).
107        /// This function strips the boundary prefix from the system path and sanitizes
108        /// the remaining components to produce a safe, rooted virtual view.
109        fn compute_virtual<Marker>(
110            system_path: &std::path::Path,
111            restriction: &crate::PathBoundary<Marker>,
112        ) -> std::path::PathBuf {
113            use std::path::Component;
114
115            // WHY: Windows canonicalization adds a `\\?\` verbatim prefix that breaks
116            // `strip_prefix` comparisons between system_path and jail_norm. `dunce`
117            // strips it via `std::path::Prefix` matching (no lossy UTF-8 round-trip)
118            // and declines to strip when doing so would be unsafe (reserved device
119            // names, >MAX_PATH, trailing dots). On non-Windows it is a no-op.
120            #[cfg(windows)]
121            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
122                dunce::simplified(p).to_path_buf()
123            }
124
125            #[cfg(not(windows))]
126            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
127                p.to_path_buf()
128            }
129
130            let system_norm = strip_verbatim(system_path);
131            let jail_norm = strip_verbatim(restriction.path());
132
133            // Fast path: strip the boundary prefix directly. This works when both
134            // paths share a common prefix after verbatim normalization.
135            // Raw components are stored; sanitization happens at display time in
136            // VirtualPathDisplay::fmt so that virtual_join navigation remains correct.
137            if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
138                let mut cleaned = std::path::PathBuf::new();
139                for comp in stripped.components() {
140                    if let Component::Normal(name) = comp {
141                        cleaned.push(name);
142                    }
143                }
144                return cleaned;
145            }
146
147            // Fallback: when strip_prefix fails (e.g., case differences on
148            // Windows, or UNC vs local prefix mismatch), walk components
149            // manually and skip the shared prefix with platform-aware comparison.
150            let mut strictpath_comps: Vec<_> = system_norm
151                .components()
152                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
153                .collect();
154            let mut boundary_comps: Vec<_> = jail_norm
155                .components()
156                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
157                .collect();
158
159            #[cfg(windows)]
160            fn comp_eq(a: &Component, b: &Component) -> bool {
161                match (a, b) {
162                    (Component::Normal(x), Component::Normal(y)) => {
163                        x.to_string_lossy().to_ascii_lowercase()
164                            == y.to_string_lossy().to_ascii_lowercase()
165                    }
166                    _ => false,
167                }
168            }
169
170            #[cfg(not(windows))]
171            fn comp_eq(a: &Component, b: &Component) -> bool {
172                a == b
173            }
174
175            while !strictpath_comps.is_empty()
176                && !boundary_comps.is_empty()
177                && comp_eq(&strictpath_comps[0], &boundary_comps[0])
178            {
179                strictpath_comps.remove(0);
180                boundary_comps.remove(0);
181            }
182
183            let mut vb = std::path::PathBuf::new();
184            for c in strictpath_comps {
185                if let Component::Normal(name) = c {
186                    vb.push(name);
187                }
188            }
189            vb
190        }
191
192        let virtual_path = compute_virtual(strict_path.path(), strict_path.boundary());
193
194        Self {
195            inner: strict_path,
196            virtual_path,
197        }
198    }
199
200    /// Convert this `VirtualPath` back into a system‑facing `StrictPath`.
201    #[must_use = "unvirtual() consumes self — use the returned StrictPath for system-facing I/O, or prefer .as_unvirtual() to borrow without consuming"]
202    #[inline]
203    pub fn unvirtual(self) -> StrictPath<Marker> {
204        self.inner
205    }
206
207    /// Change the compile-time marker while keeping the virtual and strict views in sync.
208    ///
209    /// WHEN TO USE:
210    /// - After authenticating/authorizing a user and granting them access to a virtual path
211    /// - When escalating or downgrading permissions (e.g., ReadOnly → ReadWrite)
212    /// - When reinterpreting a path's domain (e.g., TempStorage → UserUploads)
213    ///
214    /// WHEN NOT TO USE:
215    /// - When converting between path types - conversions preserve markers automatically
216    /// - When the current marker already matches your needs - no transformation needed
217    /// - When you haven't verified authorization - NEVER change markers without checking permissions
218    ///
219    /// SECURITY:
220    /// This method performs no permission checks. Only elevate markers after verifying real
221    /// authorization out-of-band.
222    ///
223    /// # Examples
224    ///
225    /// ```rust
226    /// # use strict_path::VirtualPath;
227    /// # struct GuestAccess;
228    /// # struct UserAccess;
229    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-example");
230    /// # std::fs::create_dir_all(&root_dir)?;
231    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir)?;
232    /// // Simulated authorization: verify user credentials before granting access
233    /// fn grant_user_access(user_token: &str, path: VirtualPath<GuestAccess>) -> Option<VirtualPath<UserAccess>> {
234    ///     if user_token == "valid-token-12345" {
235    ///         Some(path.change_marker())  // ✅ Only after token validation
236    ///     } else {
237    ///         None  // ❌ Invalid token
238    ///     }
239    /// }
240    ///
241    /// // Untrusted input from request/CLI/config/etc.
242    /// let requested_file = "docs/readme.md";
243    /// let guest_path: VirtualPath<GuestAccess> = guest_root.virtual_join(requested_file)?;
244    /// let user_path = grant_user_access("valid-token-12345", guest_path).expect("authorized");
245    /// assert_eq!(user_path.virtualpath_display().to_string(), "/docs/readme.md");
246    /// # std::fs::remove_dir_all(&root_dir)?;
247    /// # Ok::<_, Box<dyn std::error::Error>>(())
248    /// ```
249    ///
250    /// **Type Safety Guarantee:**
251    ///
252    /// The following code **fails to compile** because you cannot pass a path with one marker
253    /// type to a function expecting a different marker type. This compile-time check enforces
254    /// that permission changes are explicit and cannot be bypassed accidentally.
255    ///
256    /// ```compile_fail
257    /// # use strict_path::VirtualPath;
258    /// # struct GuestAccess;
259    /// # struct EditorAccess;
260    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-deny");
261    /// # std::fs::create_dir_all(&root_dir).unwrap();
262    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir).unwrap();
263    /// fn require_editor(_: VirtualPath<EditorAccess>) {}
264    /// let guest_file = guest_root.virtual_join("docs/manual.txt").unwrap();
265    /// // ❌ Compile error: expected `VirtualPath<EditorAccess>`, found `VirtualPath<GuestAccess>`
266    /// require_editor(guest_file);
267    /// ```
268    #[must_use = "change_marker() consumes self — the original VirtualPath is moved; use the returned VirtualPath<NewMarker>"]
269    #[inline]
270    pub fn change_marker<NewMarker>(self) -> VirtualPath<NewMarker> {
271        let VirtualPath {
272            inner,
273            virtual_path,
274        } = self;
275
276        VirtualPath {
277            inner: inner.change_marker(),
278            virtual_path,
279        }
280    }
281
282    /// Consume and return the `VirtualRoot` for its boundary (no directory creation).
283    ///
284    /// # Errors
285    ///
286    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` when the
287    ///   strict path does not exist or is not a directory.
288    #[must_use = "try_into_root() consumes self — use the returned VirtualRoot to restrict future path operations"]
289    #[inline]
290    pub fn try_into_root(self) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
291        Ok(self.inner.try_into_boundary()?.virtualize())
292    }
293
294    /// Consume and return a `VirtualRoot`, creating the underlying directory if missing.
295    ///
296    /// # Errors
297    ///
298    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` or directory
299    ///   creation failures wrapped in `InvalidRestriction`.
300    #[must_use = "try_into_root_create() consumes self — use the returned VirtualRoot to restrict future path operations"]
301    #[inline]
302    pub fn try_into_root_create(
303        self,
304    ) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
305        let strict_path = self.inner;
306        let validated_dir = strict_path.try_into_boundary_create()?;
307        Ok(validated_dir.virtualize())
308    }
309
310    /// Borrow the underlying system‑facing `StrictPath` (no allocation).
311    #[must_use = "as_unvirtual() borrows the system-facing StrictPath — use it for system I/O or pass to functions accepting &StrictPath<Marker>"]
312    #[inline]
313    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
314        &self.inner
315    }
316
317    /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
318    #[must_use = "pass interop_path() directly to third-party APIs requiring AsRef<Path> — never wrap it in Path::new() or PathBuf::from(); NEVER expose this in user-facing output (use .virtualpath_display() instead)"]
319    #[inline]
320    pub fn interop_path(&self) -> &std::ffi::OsStr {
321        self.inner.interop_path()
322    }
323
324    /// Join a virtual path segment (virtual semantics) and re‑validate within the same restriction.
325    ///
326    /// Applies virtual path clamping: absolute paths are interpreted relative to the virtual root,
327    /// and traversal attempts are clamped to prevent escaping the boundary. This method maintains
328    /// the security guarantee that all `VirtualPath` instances stay within their virtual root.
329    ///
330    /// # Examples
331    ///
332    /// ```rust
333    /// # use strict_path::VirtualRoot;
334    /// # let td = tempfile::tempdir().unwrap();
335    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
336    /// let base = vroot.virtual_join("data")?;
337    ///
338    /// // Absolute paths are clamped to virtual root
339    /// let abs = base.virtual_join("/etc/config")?;
340    /// assert_eq!(abs.virtualpath_display().to_string(), "/etc/config");
341    /// # Ok::<(), Box<dyn std::error::Error>>(())
342    /// ```
343    #[must_use = "virtual_join() validates untrusted input against the virtual root — always handle the Result to detect escape attempts"]
344    #[inline]
345    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
346        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
347        let candidate = self.virtual_path.join(path.as_ref());
348        let anchored = crate::validator::path_history::PathHistory::new(candidate)
349            .canonicalize_anchored(self.inner.boundary())?;
350        let boundary_path = clamp(self.inner.boundary(), anchored)?;
351        Ok(VirtualPath::new(boundary_path))
352    }
353
354    // No local clamping helpers; virtual flows should route through
355    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
356
357    /// Return the parent virtual path, or `None` at the virtual root.
358    #[must_use = "returns a Result<Option> — handle both the error case and the None (at virtual root) case"]
359    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
360        match self.virtual_path.parent() {
361            Some(parent_virtual_path) => {
362                let anchored = crate::validator::path_history::PathHistory::new(
363                    parent_virtual_path.to_path_buf(),
364                )
365                .canonicalize_anchored(self.inner.boundary())?;
366                let validated_path = clamp(self.inner.boundary(), anchored)?;
367                Ok(Some(VirtualPath::new(validated_path)))
368            }
369            None => Ok(None),
370        }
371    }
372
373    /// Return a new virtual path with file name changed, preserving clamping.
374    #[must_use = "returns a new validated VirtualPath with the file name replaced — the original is unchanged; handle the Result to detect boundary escapes"]
375    #[inline]
376    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
377        let candidate = self.virtual_path.with_file_name(file_name);
378        let anchored = crate::validator::path_history::PathHistory::new(candidate)
379            .canonicalize_anchored(self.inner.boundary())?;
380        let validated_path = clamp(self.inner.boundary(), anchored)?;
381        Ok(VirtualPath::new(validated_path))
382    }
383
384    /// Return a new virtual path with the extension changed, preserving clamping.
385    #[must_use = "returns a new validated VirtualPath with the extension changed — the original is unchanged; handle the Result to detect boundary escapes"]
386    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
387        if self.virtual_path.file_name().is_none() {
388            return Err(StrictPathError::path_escapes_boundary(
389                self.virtual_path.clone(),
390                self.inner.boundary().path().to_path_buf(),
391            ));
392        }
393
394        // WHY: `Path::with_extension` panics when the extension contains a
395        // path separator. Untrusted callers must get an `Err`, never a crash.
396        let candidate =
397            crate::path::with_validated_extension(&self.virtual_path, extension.as_ref())?;
398        let anchored = crate::validator::path_history::PathHistory::new(candidate)
399            .canonicalize_anchored(self.inner.boundary())?;
400        let validated_path = clamp(self.inner.boundary(), anchored)?;
401        Ok(VirtualPath::new(validated_path))
402    }
403
404    /// Return the file name component of the virtual path, if any.
405    #[must_use]
406    #[inline]
407    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
408        self.virtual_path.file_name()
409    }
410
411    /// Return the file stem of the virtual path, if any.
412    #[must_use]
413    #[inline]
414    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
415        self.virtual_path.file_stem()
416    }
417
418    /// Return the extension of the virtual path, if any.
419    #[must_use]
420    #[inline]
421    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
422        self.virtual_path.extension()
423    }
424
425    /// Return `true` if the virtual path starts with the given prefix (virtual semantics).
426    #[must_use]
427    #[inline]
428    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
429        self.virtual_path.starts_with(p)
430    }
431
432    /// Return `true` if the virtual path ends with the given suffix (virtual semantics).
433    #[must_use]
434    #[inline]
435    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
436        self.virtual_path.ends_with(p)
437    }
438
439    /// Return a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt").
440    #[must_use = "virtualpath_display() returns a safe user-facing path representation — use this (not interop_path()) in API responses, logs, and UI"]
441    #[inline]
442    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
443        VirtualPathDisplay(self)
444    }
445
446    /// Return `true` if the underlying system path exists.
447    #[must_use]
448    #[inline]
449    pub fn exists(&self) -> bool {
450        self.inner.exists()
451    }
452
453    /// Return `true` if the underlying system path is a file.
454    #[must_use]
455    #[inline]
456    pub fn is_file(&self) -> bool {
457        self.inner.is_file()
458    }
459
460    /// Return `true` if the underlying system path is a directory.
461    #[must_use]
462    #[inline]
463    pub fn is_dir(&self) -> bool {
464        self.inner.is_dir()
465    }
466
467    /// Return metadata for the underlying system path.
468    #[inline]
469    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
470        self.inner.metadata()
471    }
472
473    /// Read the file contents as `String` from the underlying system path.
474    #[inline]
475    pub fn read_to_string(&self) -> std::io::Result<String> {
476        self.inner.read_to_string()
477    }
478
479    /// Read raw bytes from the underlying system path.
480    #[inline]
481    pub fn read(&self) -> std::io::Result<Vec<u8>> {
482        self.inner.read()
483    }
484
485    /// Return metadata for the underlying system path without following symlinks.
486    #[inline]
487    pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
488        self.inner.symlink_metadata()
489    }
490
491    /// Set permissions on the file or directory at this path.
492    ///
493    #[inline]
494    pub fn set_permissions(&self, perm: std::fs::Permissions) -> std::io::Result<()> {
495        self.inner.set_permissions(perm)
496    }
497
498    /// Check if the path exists, returning an error on permission issues.
499    ///
500    /// Unlike `exists()` which returns `false` on permission errors, this method
501    /// distinguishes between "path does not exist" (`Ok(false)`) and "cannot check
502    /// due to permission error" (`Err(...)`).
503    ///
504    #[inline]
505    pub fn try_exists(&self) -> std::io::Result<bool> {
506        self.inner.try_exists()
507    }
508
509    /// Create an empty file if it doesn't exist, or update the modification time if it does.
510    ///
511    /// This is a convenience method combining file creation and mtime update.
512    /// Uses `OpenOptions` with `create(true).write(true)` which creates the file
513    /// if missing or opens it for writing if it exists, updating mtime on close.
514    ///
515    pub fn touch(&self) -> std::io::Result<()> {
516        self.inner.touch()
517    }
518
519    /// Read directory entries (discovery). Re‑join names with `virtual_join(...)` to preserve clamping.
520    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
521        self.inner.read_dir()
522    }
523
524    /// Read directory entries as validated `VirtualPath` values (auto re-joins each entry).
525    ///
526    /// Unlike `read_dir()` which returns raw `std::fs::DirEntry`, this method automatically
527    /// validates each directory entry through `virtual_join()`, returning an iterator of
528    /// `Result<VirtualPath<Marker>>`. This eliminates the need for manual re-validation loops
529    /// while preserving the virtual path semantics.
530    ///
531    /// # Errors
532    ///
533    /// - `std::io::Error`: If the directory cannot be read.
534    /// - Each yielded item may also be `Err` if validation fails for that entry.
535    ///
536    /// # Examples
537    ///
538    /// ```rust
539    /// # use strict_path::{VirtualRoot, VirtualPath};
540    /// # let temp = tempfile::tempdir()?;
541    /// # let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
542    /// # let dir = vroot.virtual_join("uploads")?;
543    /// # dir.create_dir_all()?;
544    /// # vroot.virtual_join("uploads/file1.txt")?.write("a")?;
545    /// # vroot.virtual_join("uploads/file2.txt")?.write("b")?;
546    /// // Iterate with automatic validation
547    /// for entry in dir.virtual_read_dir()? {
548    ///     let child: VirtualPath = entry?;
549    ///     let child_display = child.virtualpath_display();
550    ///     println!("{child_display}");
551    /// }
552    /// # Ok::<_, Box<dyn std::error::Error>>(())
553    /// ```
554    pub fn virtual_read_dir(&self) -> std::io::Result<VirtualReadDir<'_, Marker>> {
555        let inner = std::fs::read_dir(self.inner.path())?;
556        Ok(VirtualReadDir {
557            inner,
558            parent: self,
559        })
560    }
561}