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/// A user-facing path clamped to the virtual root path.
13///
14/// `virtualpath_display()` shows a rooted, forward-slashed path (e.g., `"/a/b.txt"`).
15/// Use virtual manipulation methods to compose paths while preserving clamping,
16/// and convert to `StrictPath` with `unvirtual()` for system-facing I/O.
17#[derive(Clone)]
18pub struct VirtualPath<Marker = ()> {
19    inner: StrictPath<Marker>,
20    virtual_path: PathBuf,
21}
22
23#[inline]
24fn clamp<Marker, H>(
25    restriction: &PathBoundary<Marker>,
26    anchored: PathHistory<(H, Canonicalized)>,
27) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
28    restriction.strict_join(anchored.into_inner())
29}
30
31impl<Marker> VirtualPath<Marker> {
32    /// Create the virtual root (`"/"`) for the given filesystem root.
33    ///
34    /// Prefer this ergonomic constructor when you want to start from the
35    /// virtual root for a given filesystem directory.
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.virtual_join("")
39    }
40
41    /// Create the virtual root (`"/"`), creating the filesystem root if missing.
42    ///
43    /// Creates the backing directory if needed.
44    pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
45        let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
46        vroot.virtual_join("")
47    }
48    #[inline]
49    pub(crate) fn new(restricted_path: StrictPath<Marker>) -> Self {
50        fn compute_virtual<Marker>(
51            system_path: &std::path::Path,
52            restriction: &crate::PathBoundary<Marker>,
53        ) -> std::path::PathBuf {
54            use std::ffi::OsString;
55            use std::path::Component;
56
57            #[cfg(windows)]
58            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
59                let s = p.as_os_str().to_string_lossy();
60                if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
61                    return std::path::PathBuf::from(trimmed);
62                }
63                if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
64                    return std::path::PathBuf::from(trimmed);
65                }
66                std::path::PathBuf::from(s.to_string())
67            }
68
69            #[cfg(not(windows))]
70            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
71                p.to_path_buf()
72            }
73
74            let system_norm = strip_verbatim(system_path);
75            let jail_norm = strip_verbatim(restriction.path());
76
77            if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
78                let mut cleaned = std::path::PathBuf::new();
79                for comp in stripped.components() {
80                    if let Component::Normal(name) = comp {
81                        let s = name.to_string_lossy();
82                        let cleaned_s = s.replace(['\n', ';'], "_");
83                        if cleaned_s == s {
84                            cleaned.push(name);
85                        } else {
86                            cleaned.push(OsString::from(cleaned_s));
87                        }
88                    }
89                }
90                return cleaned;
91            }
92
93            let mut strictpath_comps: Vec<_> = system_norm
94                .components()
95                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
96                .collect();
97            let mut boundary_comps: Vec<_> = jail_norm
98                .components()
99                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
100                .collect();
101
102            #[cfg(windows)]
103            fn comp_eq(a: &Component, b: &Component) -> bool {
104                match (a, b) {
105                    (Component::Normal(x), Component::Normal(y)) => {
106                        x.to_string_lossy().to_ascii_lowercase()
107                            == y.to_string_lossy().to_ascii_lowercase()
108                    }
109                    _ => false,
110                }
111            }
112
113            #[cfg(not(windows))]
114            fn comp_eq(a: &Component, b: &Component) -> bool {
115                a == b
116            }
117
118            while !strictpath_comps.is_empty()
119                && !boundary_comps.is_empty()
120                && comp_eq(&strictpath_comps[0], &boundary_comps[0])
121            {
122                strictpath_comps.remove(0);
123                boundary_comps.remove(0);
124            }
125
126            let mut vb = std::path::PathBuf::new();
127            for c in strictpath_comps {
128                if let Component::Normal(name) = c {
129                    let s = name.to_string_lossy();
130                    let cleaned = s.replace(['\n', ';'], "_");
131                    if cleaned == s {
132                        vb.push(name);
133                    } else {
134                        vb.push(OsString::from(cleaned));
135                    }
136                }
137            }
138            vb
139        }
140
141        let virtual_path = compute_virtual(restricted_path.path(), restricted_path.boundary());
142
143        Self {
144            inner: restricted_path,
145            virtual_path,
146        }
147    }
148
149    /// Converts this `VirtualPath` back into a system-facing `StrictPath`.
150    #[inline]
151    pub fn unvirtual(self) -> StrictPath<Marker> {
152        self.inner
153    }
154
155    /// Consumes this `VirtualPath` and returns the `VirtualRoot` for its boundary.
156    ///
157    /// Equivalent to `self.unvirtual().try_into_boundary().virtualize()` but without
158    /// requiring intermediate variables. Does not create any directories.
159    #[inline]
160    pub fn try_into_root(self) -> crate::validator::virtual_root::VirtualRoot<Marker> {
161        self.inner.try_into_boundary().virtualize()
162    }
163
164    /// Consumes this `VirtualPath` and returns a `VirtualRoot`, creating the
165    /// underlying directory if it does not exist.
166    ///
167    /// If the boundary directory is somehow missing, it will be created before
168    /// producing the `VirtualRoot`.
169    #[inline]
170    pub fn try_into_root_create(self) -> crate::validator::virtual_root::VirtualRoot<Marker> {
171        let boundary = self.inner.try_into_boundary();
172        if !boundary.exists() {
173            // Best-effort create; ignore error and let later operations surface it
174            let _ = std::fs::create_dir_all(boundary.as_ref());
175        }
176        boundary.virtualize()
177    }
178
179    /// Borrows the underlying system-facing `StrictPath` (no allocation).
180    ///
181    /// Use this to pass a `&VirtualPath` to APIs that accept `&StrictPath`.
182    #[inline]
183    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
184        &self.inner
185    }
186
187    /// Returns the underlying system path as `&OsStr` for `AsRef<Path>` interop.
188    #[inline]
189    pub fn interop_path(&self) -> &OsStr {
190        self.inner.interop_path()
191    }
192
193    /// Safely joins a virtual path segment (virtual semantics) and re-validates.
194    #[inline]
195    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
196        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
197        let candidate = self.virtual_path.join(path.as_ref());
198        let anchored = crate::validator::path_history::PathHistory::new(candidate)
199            .canonicalize_anchored(self.inner.boundary())?;
200        let boundary_path = clamp(self.inner.boundary(), anchored)?;
201        Ok(VirtualPath::new(boundary_path))
202    }
203
204    // No local clamping helpers; virtual flows should route through
205    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
206
207    /// Returns the parent virtual path, or `None` if at the virtual root.
208    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
209        match self.virtual_path.parent() {
210            Some(parent_virtual_path) => {
211                let anchored = crate::validator::path_history::PathHistory::new(
212                    parent_virtual_path.to_path_buf(),
213                )
214                .canonicalize_anchored(self.inner.boundary())?;
215                let restricted_path = clamp(self.inner.boundary(), anchored)?;
216                Ok(Some(VirtualPath::new(restricted_path)))
217            }
218            None => Ok(None),
219        }
220    }
221
222    /// Returns a new `VirtualPath` with the file name changed, preserving clamping.
223    #[inline]
224    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
225        let candidate = self.virtual_path.with_file_name(file_name);
226        let anchored = crate::validator::path_history::PathHistory::new(candidate)
227            .canonicalize_anchored(self.inner.boundary())?;
228        let restricted_path = clamp(self.inner.boundary(), anchored)?;
229        Ok(VirtualPath::new(restricted_path))
230    }
231
232    /// Returns a new `VirtualPath` with the extension changed, preserving clamping.
233    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
234        if self.virtual_path.file_name().is_none() {
235            return Err(StrictPathError::path_escapes_boundary(
236                self.virtual_path.clone(),
237                self.inner.boundary().path().to_path_buf(),
238            ));
239        }
240
241        let candidate = self.virtual_path.with_extension(extension);
242        let anchored = crate::validator::path_history::PathHistory::new(candidate)
243            .canonicalize_anchored(self.inner.boundary())?;
244        let restricted_path = clamp(self.inner.boundary(), anchored)?;
245        Ok(VirtualPath::new(restricted_path))
246    }
247
248    /// Returns the file name component of the virtual path, if any.
249    #[inline]
250    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
251        self.virtual_path.file_name()
252    }
253
254    /// Returns the file stem of the virtual path, if any.
255    #[inline]
256    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
257        self.virtual_path.file_stem()
258    }
259
260    /// Returns the extension of the virtual path, if any.
261    #[inline]
262    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
263        self.virtual_path.extension()
264    }
265
266    /// Returns `true` if the virtual path starts with the given prefix (virtual semantics).
267    #[inline]
268    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
269        self.virtual_path.starts_with(p)
270    }
271
272    /// Returns `true` if the virtual path ends with the given suffix (virtual semantics).
273    #[inline]
274    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
275        self.virtual_path.ends_with(p)
276    }
277
278    /// Returns a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt"`).
279    #[inline]
280    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
281        VirtualPathDisplay(self)
282    }
283
284    /// Returns `true` if the underlying system path exists.
285    #[inline]
286    pub fn exists(&self) -> bool {
287        self.inner.exists()
288    }
289
290    /// Returns `true` if the underlying system path is a file.
291    #[inline]
292    pub fn is_file(&self) -> bool {
293        self.inner.is_file()
294    }
295
296    /// Returns `true` if the underlying system path is a directory.
297    #[inline]
298    pub fn is_dir(&self) -> bool {
299        self.inner.is_dir()
300    }
301
302    /// Returns metadata for the underlying system path.
303    #[inline]
304    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
305        self.inner.metadata()
306    }
307
308    /// Reads the file contents as `String` from the underlying system path.
309    #[inline]
310    pub fn read_to_string(&self) -> std::io::Result<String> {
311        self.inner.read_to_string()
312    }
313
314    /// Reads the file contents as raw bytes from the underlying system path.
315    #[deprecated(since = "0.1.0-alpha.5", note = "Use read() instead")]
316    #[inline]
317    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
318        self.inner.read()
319    }
320
321    /// Writes raw bytes to the underlying system path.
322    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
323    #[inline]
324    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
325        self.inner.write(data)
326    }
327
328    /// Writes a UTF-8 string to the underlying system path.
329    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
330    #[inline]
331    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
332        self.inner.write(data)
333    }
334
335    /// Reads the file contents as raw bytes (replacement for `read_bytes`).
336    #[inline]
337    pub fn read(&self) -> std::io::Result<Vec<u8>> {
338        self.inner.read()
339    }
340
341    /// Reads the directory entries at the underlying system path (like `std::fs::read_dir`).
342    ///
343    /// This is intended for discovery in the virtual space. Collect each entry's file name via
344    /// `entry.file_name()` and re‑join with `virtual_join(...)` to preserve clamping before I/O.
345    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
346        self.inner.read_dir()
347    }
348
349    /// Writes data to the underlying system path. Accepts `&str`, `String`, `&[u8]`, `Vec<u8]`, etc.
350    #[inline]
351    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
352        self.inner.write(contents)
353    }
354
355    /// Creates all directories in the underlying system path if missing.
356    #[inline]
357    pub fn create_dir_all(&self) -> std::io::Result<()> {
358        self.inner.create_dir_all()
359    }
360
361    /// Creates the directory at this virtual location (non-recursive).
362    ///
363    /// Mirrors `std::fs::create_dir` and fails if the parent does not exist.
364    #[inline]
365    pub fn create_dir(&self) -> std::io::Result<()> {
366        self.inner.create_dir()
367    }
368
369    /// Creates only the immediate parent directory of this virtual path (non-recursive).
370    ///
371    /// Acts in the virtual dimension: the parent is derived via `virtualpath_parent()`
372    /// and then created on the underlying system path. Returns `Ok(())` at virtual root.
373    #[inline]
374    pub fn create_parent_dir(&self) -> std::io::Result<()> {
375        match self.virtualpath_parent() {
376            Ok(Some(parent)) => parent.create_dir(),
377            Ok(None) => Ok(()),
378            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
379            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
380        }
381    }
382
383    /// Recursively creates all missing directories up to the immediate parent of this virtual path.
384    ///
385    /// Acts in the virtual dimension; returns `Ok(())` at virtual root.
386    #[inline]
387    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
388        match self.virtualpath_parent() {
389            Ok(Some(parent)) => parent.create_dir_all(),
390            Ok(None) => Ok(()),
391            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
392            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
393        }
394    }
395
396    /// Removes the file at the underlying system path.
397    #[inline]
398    pub fn remove_file(&self) -> std::io::Result<()> {
399        self.inner.remove_file()
400    }
401
402    /// Removes the directory at the underlying system path.
403    #[inline]
404    pub fn remove_dir(&self) -> std::io::Result<()> {
405        self.inner.remove_dir()
406    }
407
408    /// Recursively removes the directory and its contents at the underlying system path.
409    #[inline]
410    pub fn remove_dir_all(&self) -> std::io::Result<()> {
411        self.inner.remove_dir_all()
412    }
413
414    /// Creates a symbolic link at this virtual location pointing to `target`, ensuring both remain
415    /// within the same virtual root restriction.
416    pub fn virtual_symlink(&self, link_path: &Self) -> std::io::Result<()> {
417        if self.inner.boundary().path() != link_path.inner.boundary().path() {
418            let err = StrictPathError::path_escapes_boundary(
419                link_path.inner.path().to_path_buf(),
420                self.inner.boundary().path().to_path_buf(),
421            );
422            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
423        }
424
425        self.inner.strict_symlink(&link_path.inner)
426    }
427
428    /// Creates a hard link at this virtual location pointing to `target`.
429    pub fn virtual_hard_link(&self, link_path: &Self) -> std::io::Result<()> {
430        if self.inner.boundary().path() != link_path.inner.boundary().path() {
431            let err = StrictPathError::path_escapes_boundary(
432                link_path.inner.path().to_path_buf(),
433                self.inner.boundary().path().to_path_buf(),
434            );
435            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
436        }
437
438        self.inner.strict_hard_link(&link_path.inner)
439    }
440
441    /// Renames or moves this virtual path to a new location within the same virtual root.
442    ///
443    /// Destination paths are resolved in virtual space:
444    /// - Relative inputs are interpreted as siblings (resolved against the virtual parent).
445    /// - Absolute virtual inputs are treated as requests from the virtual root.
446    ///
447    /// Clamping and boundary checks are enforced by virtual joins. No parent directories are
448    /// created implicitly; call `create_parent_dir_all()` beforehand if needed.
449    pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
450        let dest_ref = dest.as_ref();
451        let dest_v = if dest_ref.is_absolute() {
452            match self.virtual_join(dest_ref) {
453                Ok(p) => p,
454                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
455            }
456        } else {
457            // Resolve as sibling under the current virtual parent (or root if at "/")
458            let parent = match self.virtualpath_parent() {
459                Ok(Some(p)) => p,
460                Ok(None) => match self.virtual_join("") {
461                    Ok(root) => root,
462                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
463                },
464                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
465            };
466            match parent.virtual_join(dest_ref) {
467                Ok(p) => p,
468                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
469            }
470        };
471
472        // Perform the actual rename via StrictPath
473        self.inner.strict_rename(dest_v.inner.path())
474    }
475
476    /// Copies this virtual path to a new location within the same virtual root.
477    ///
478    /// Destination paths are resolved in virtual space:
479    /// - Relative inputs are interpreted as siblings (resolved against the virtual parent).
480    /// - Absolute virtual inputs are treated as requests from the virtual root.
481    ///
482    /// Clamping and boundary checks are enforced by virtual joins. No parent directories are
483    /// created implicitly; call `create_parent_dir_all()` beforehand if needed. Returns the
484    /// number of bytes copied, mirroring `std::fs::copy`.
485    pub fn virtual_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
486        let dest_ref = dest.as_ref();
487        let dest_v = if dest_ref.is_absolute() {
488            match self.virtual_join(dest_ref) {
489                Ok(p) => p,
490                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
491            }
492        } else {
493            // Resolve as sibling under the current virtual parent (or root if at "/")
494            let parent = match self.virtualpath_parent() {
495                Ok(Some(p)) => p,
496                Ok(None) => match self.virtual_join("") {
497                    Ok(root) => root,
498                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
499                },
500                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
501            };
502            match parent.virtual_join(dest_ref) {
503                Ok(p) => p,
504                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
505            }
506        };
507
508        // Perform the actual copy via StrictPath
509        std::fs::copy(self.inner.path(), dest_v.inner.path())
510    }
511}
512
513pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
514
515impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517        // Ensure leading slash and normalize to forward slashes for display
518        let s_lossy = self.0.virtual_path.to_string_lossy();
519        let s_norm: std::borrow::Cow<'_, str> = {
520            #[cfg(windows)]
521            {
522                std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
523            }
524            #[cfg(not(windows))]
525            {
526                std::borrow::Cow::Borrowed(&s_lossy)
527            }
528        };
529        if s_norm.starts_with('/') {
530            write!(f, "{s_norm}")
531        } else {
532            write!(f, "/{s_norm}")
533        }
534    }
535}
536
537impl<Marker> fmt::Debug for VirtualPath<Marker> {
538    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539        f.debug_struct("VirtualPath")
540            .field("system_path", &self.inner.path())
541            .field("virtual", &format!("{}", self.virtualpath_display()))
542            .field("boundary", &self.inner.boundary().path())
543            .field("marker", &std::any::type_name::<Marker>())
544            .finish()
545    }
546}
547
548impl<Marker> PartialEq for VirtualPath<Marker> {
549    #[inline]
550    fn eq(&self, other: &Self) -> bool {
551        self.inner.path() == other.inner.path()
552    }
553}
554
555impl<Marker> Eq for VirtualPath<Marker> {}
556
557impl<Marker> Hash for VirtualPath<Marker> {
558    #[inline]
559    fn hash<H: Hasher>(&self, state: &mut H) {
560        self.inner.path().hash(state);
561    }
562}
563
564impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
565    #[inline]
566    fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
567        self.inner.path() == other.path()
568    }
569}
570
571impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
572    #[inline]
573    fn eq(&self, other: &T) -> bool {
574        // Compare virtual paths - the user-facing representation
575        // If you want system path comparison, use as_unvirtual()
576        let virtual_str = format!("{}", self.virtualpath_display());
577        let other_str = other.as_ref().to_string_lossy();
578
579        // Normalize both to forward slashes and ensure leading slash
580        let normalized_virtual = virtual_str.as_str();
581
582        #[cfg(windows)]
583        let other_normalized = other_str.replace('\\', "/");
584        #[cfg(not(windows))]
585        let other_normalized = other_str.to_string();
586
587        let normalized_other = if other_normalized.starts_with('/') {
588            other_normalized
589        } else {
590            format!("/{}", other_normalized)
591        };
592
593        normalized_virtual == normalized_other
594    }
595}
596
597impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
598    #[inline]
599    fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
600        // Compare virtual paths - the user-facing representation
601        let virtual_str = format!("{}", self.virtualpath_display());
602        let other_str = other.as_ref().to_string_lossy();
603
604        // Normalize both to forward slashes and ensure leading slash
605        let normalized_virtual = virtual_str.as_str();
606
607        #[cfg(windows)]
608        let other_normalized = other_str.replace('\\', "/");
609        #[cfg(not(windows))]
610        let other_normalized = other_str.to_string();
611
612        let normalized_other = if other_normalized.starts_with('/') {
613            other_normalized
614        } else {
615            format!("/{}", other_normalized)
616        };
617
618        Some(normalized_virtual.cmp(&normalized_other))
619    }
620}
621
622impl<Marker> PartialOrd for VirtualPath<Marker> {
623    #[inline]
624    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
625        Some(self.cmp(other))
626    }
627}
628
629impl<Marker> Ord for VirtualPath<Marker> {
630    #[inline]
631    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
632        self.inner.path().cmp(other.inner.path())
633    }
634}
635
636#[cfg(feature = "serde")]
637impl<Marker> serde::Serialize for VirtualPath<Marker> {
638    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
639    where
640        S: serde::Serializer,
641    {
642        serializer.serialize_str(&format!("{}", self.virtualpath_display()))
643    }
644}