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    #[inline]
33    pub(crate) fn new(restricted_path: StrictPath<Marker>) -> Self {
34        fn compute_virtual<Marker>(
35            system_path: &std::path::Path,
36            restriction: &crate::PathBoundary<Marker>,
37        ) -> std::path::PathBuf {
38            use std::ffi::OsString;
39            use std::path::Component;
40
41            #[cfg(windows)]
42            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
43                let s = p.as_os_str().to_string_lossy();
44                if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
45                    return std::path::PathBuf::from(trimmed);
46                }
47                if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
48                    return std::path::PathBuf::from(trimmed);
49                }
50                std::path::PathBuf::from(s.to_string())
51            }
52
53            #[cfg(not(windows))]
54            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
55                p.to_path_buf()
56            }
57
58            let system_norm = strip_verbatim(system_path);
59            let jail_norm = strip_verbatim(restriction.path());
60
61            if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
62                let mut cleaned = std::path::PathBuf::new();
63                for comp in stripped.components() {
64                    if let Component::Normal(name) = comp {
65                        let s = name.to_string_lossy();
66                        let cleaned_s = s.replace(['\n', ';'], "_");
67                        if cleaned_s == s {
68                            cleaned.push(name);
69                        } else {
70                            cleaned.push(OsString::from(cleaned_s));
71                        }
72                    }
73                }
74                return cleaned;
75            }
76
77            let mut strictpath_comps: Vec<_> = system_norm
78                .components()
79                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
80                .collect();
81            let mut boundary_comps: Vec<_> = jail_norm
82                .components()
83                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
84                .collect();
85
86            #[cfg(windows)]
87            fn comp_eq(a: &Component, b: &Component) -> bool {
88                match (a, b) {
89                    (Component::Normal(x), Component::Normal(y)) => {
90                        x.to_string_lossy().to_ascii_lowercase()
91                            == y.to_string_lossy().to_ascii_lowercase()
92                    }
93                    _ => false,
94                }
95            }
96
97            #[cfg(not(windows))]
98            fn comp_eq(a: &Component, b: &Component) -> bool {
99                a == b
100            }
101
102            while !strictpath_comps.is_empty()
103                && !boundary_comps.is_empty()
104                && comp_eq(&strictpath_comps[0], &boundary_comps[0])
105            {
106                strictpath_comps.remove(0);
107                boundary_comps.remove(0);
108            }
109
110            let mut vb = std::path::PathBuf::new();
111            for c in strictpath_comps {
112                if let Component::Normal(name) = c {
113                    let s = name.to_string_lossy();
114                    let cleaned = s.replace(['\n', ';'], "_");
115                    if cleaned == s {
116                        vb.push(name);
117                    } else {
118                        vb.push(OsString::from(cleaned));
119                    }
120                }
121            }
122            vb
123        }
124
125        let virtual_path = compute_virtual(restricted_path.path(), restricted_path.restriction());
126
127        Self {
128            inner: restricted_path,
129            virtual_path,
130        }
131    }
132
133    /// Converts this `VirtualPath` back into a system-facing `StrictPath`.
134    #[inline]
135    pub fn unvirtual(self) -> StrictPath<Marker> {
136        self.inner
137    }
138
139    /// Borrows the underlying system-facing `StrictPath` (no allocation).
140    ///
141    /// Use this to pass a `&VirtualPath` to APIs that accept `&StrictPath`.
142    #[inline]
143    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
144        &self.inner
145    }
146
147    /// Returns the underlying system path as `&OsStr` for `AsRef<Path>` interop.
148    #[inline]
149    pub fn interop_path(&self) -> &OsStr {
150        self.inner.interop_path()
151    }
152
153    /// Safely joins a virtual path segment (virtual semantics) and re-validates.
154    #[inline]
155    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
156        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
157        let candidate = self.virtual_path.join(path.as_ref());
158        let anchored = crate::validator::path_history::PathHistory::new(candidate)
159            .canonicalize_anchored(self.inner.restriction())?;
160        let restricted_path = clamp(self.inner.restriction(), anchored)?;
161        Ok(VirtualPath::new(restricted_path))
162    }
163
164    // No local clamping helpers; virtual flows should route through
165    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
166
167    /// Returns the parent virtual path, or `None` if at the virtual root.
168    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
169        match self.virtual_path.parent() {
170            Some(parent_virtual_path) => {
171                let anchored = crate::validator::path_history::PathHistory::new(
172                    parent_virtual_path.to_path_buf(),
173                )
174                .canonicalize_anchored(self.inner.restriction())?;
175                let restricted_path = clamp(self.inner.restriction(), anchored)?;
176                Ok(Some(VirtualPath::new(restricted_path)))
177            }
178            None => Ok(None),
179        }
180    }
181
182    /// Returns a new `VirtualPath` with the file name changed, preserving clamping.
183    #[inline]
184    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
185        let candidate = self.virtual_path.with_file_name(file_name);
186        let anchored = crate::validator::path_history::PathHistory::new(candidate)
187            .canonicalize_anchored(self.inner.restriction())?;
188        let restricted_path = clamp(self.inner.restriction(), anchored)?;
189        Ok(VirtualPath::new(restricted_path))
190    }
191
192    /// Returns a new `VirtualPath` with the extension changed, preserving clamping.
193    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
194        if self.virtual_path.file_name().is_none() {
195            return Err(StrictPathError::path_escapes_boundary(
196                self.virtual_path.clone(),
197                self.inner.restriction().path().to_path_buf(),
198            ));
199        }
200
201        let candidate = self.virtual_path.with_extension(extension);
202        let anchored = crate::validator::path_history::PathHistory::new(candidate)
203            .canonicalize_anchored(self.inner.restriction())?;
204        let restricted_path = clamp(self.inner.restriction(), anchored)?;
205        Ok(VirtualPath::new(restricted_path))
206    }
207
208    /// Returns the file name component of the virtual path, if any.
209    #[inline]
210    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
211        self.virtual_path.file_name()
212    }
213
214    /// Returns the file stem of the virtual path, if any.
215    #[inline]
216    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
217        self.virtual_path.file_stem()
218    }
219
220    /// Returns the extension of the virtual path, if any.
221    #[inline]
222    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
223        self.virtual_path.extension()
224    }
225
226    /// Returns `true` if the virtual path starts with the given prefix (virtual semantics).
227    #[inline]
228    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
229        self.virtual_path.starts_with(p)
230    }
231
232    /// Returns `true` if the virtual path ends with the given suffix (virtual semantics).
233    #[inline]
234    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
235        self.virtual_path.ends_with(p)
236    }
237
238    /// Returns a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt"`).
239    #[inline]
240    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
241        VirtualPathDisplay(self)
242    }
243
244    /// Returns `true` if the underlying system path exists.
245    #[inline]
246    pub fn exists(&self) -> bool {
247        self.inner.exists()
248    }
249
250    /// Returns `true` if the underlying system path is a file.
251    #[inline]
252    pub fn is_file(&self) -> bool {
253        self.inner.is_file()
254    }
255
256    /// Returns `true` if the underlying system path is a directory.
257    #[inline]
258    pub fn is_dir(&self) -> bool {
259        self.inner.is_dir()
260    }
261
262    /// Returns metadata for the underlying system path.
263    #[inline]
264    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
265        self.inner.metadata()
266    }
267
268    /// Reads the file contents as `String` from the underlying system path.
269    #[inline]
270    pub fn read_to_string(&self) -> std::io::Result<String> {
271        self.inner.read_to_string()
272    }
273
274    /// Reads the file contents as raw bytes from the underlying system path.
275    #[inline]
276    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
277        self.inner.read_bytes()
278    }
279
280    /// Writes raw bytes to the underlying system path.
281    #[inline]
282    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
283        self.inner.write_bytes(data)
284    }
285
286    /// Writes a UTF-8 string to the underlying system path.
287    #[inline]
288    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
289        self.inner.write_string(data)
290    }
291
292    /// Creates all directories in the underlying system path if missing.
293    #[inline]
294    pub fn create_dir_all(&self) -> std::io::Result<()> {
295        self.inner.create_dir_all()
296    }
297
298    /// Creates the directory at this virtual location (non-recursive).
299    ///
300    /// Mirrors `std::fs::create_dir` and fails if the parent does not exist.
301    #[inline]
302    pub fn create_dir(&self) -> std::io::Result<()> {
303        self.inner.create_dir()
304    }
305
306    /// Creates only the immediate parent directory of this virtual path (non-recursive).
307    ///
308    /// Acts in the virtual dimension: the parent is derived via `virtualpath_parent()`
309    /// and then created on the underlying system path. Returns `Ok(())` at virtual root.
310    #[inline]
311    pub fn create_parent_dir(&self) -> std::io::Result<()> {
312        match self.virtualpath_parent() {
313            Ok(Some(parent)) => parent.create_dir(),
314            Ok(None) => Ok(()),
315            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
316            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
317        }
318    }
319
320    /// Recursively creates all missing directories up to the immediate parent of this virtual path.
321    ///
322    /// Acts in the virtual dimension; returns `Ok(())` at virtual root.
323    #[inline]
324    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
325        match self.virtualpath_parent() {
326            Ok(Some(parent)) => parent.create_dir_all(),
327            Ok(None) => Ok(()),
328            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
329            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
330        }
331    }
332
333    /// Removes the file at the underlying system path.
334    #[inline]
335    pub fn remove_file(&self) -> std::io::Result<()> {
336        self.inner.remove_file()
337    }
338
339    /// Removes the directory at the underlying system path.
340    #[inline]
341    pub fn remove_dir(&self) -> std::io::Result<()> {
342        self.inner.remove_dir()
343    }
344
345    /// Recursively removes the directory and its contents at the underlying system path.
346    #[inline]
347    pub fn remove_dir_all(&self) -> std::io::Result<()> {
348        self.inner.remove_dir_all()
349    }
350}
351
352pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
353
354impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        // Ensure leading slash and normalize to forward slashes for display
357        let s_lossy = self.0.virtual_path.to_string_lossy();
358        let s_norm: std::borrow::Cow<'_, str> = {
359            #[cfg(windows)]
360            {
361                std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
362            }
363            #[cfg(not(windows))]
364            {
365                std::borrow::Cow::Borrowed(&s_lossy)
366            }
367        };
368        if s_norm.starts_with('/') {
369            write!(f, "{s_norm}")
370        } else {
371            write!(f, "/{s_norm}")
372        }
373    }
374}
375
376impl<Marker> fmt::Debug for VirtualPath<Marker> {
377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378        f.debug_struct("VirtualPath")
379            .field("system_path", &self.inner.path())
380            .field("virtual", &format!("{}", self.virtualpath_display()))
381            .field("restriction", &self.inner.restriction().path())
382            .field("marker", &std::any::type_name::<Marker>())
383            .finish()
384    }
385}
386
387impl<Marker> PartialEq for VirtualPath<Marker> {
388    #[inline]
389    fn eq(&self, other: &Self) -> bool {
390        self.inner.path() == other.inner.path()
391    }
392}
393
394impl<Marker> Eq for VirtualPath<Marker> {}
395
396impl<Marker> Hash for VirtualPath<Marker> {
397    #[inline]
398    fn hash<H: Hasher>(&self, state: &mut H) {
399        self.inner.path().hash(state);
400    }
401}
402
403impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
404    #[inline]
405    fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
406        self.inner.path() == other.path()
407    }
408}
409
410impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
411    #[inline]
412    fn eq(&self, other: &T) -> bool {
413        // Compare virtual paths - the user-facing representation
414        // If you want system path comparison, use as_unvirtual()
415        let virtual_str = format!("{}", self.virtualpath_display());
416        let other_str = other.as_ref().to_string_lossy();
417
418        // Normalize both to forward slashes and ensure leading slash
419        let normalized_virtual = virtual_str.as_str();
420
421        #[cfg(windows)]
422        let other_normalized = other_str.replace('\\', "/");
423        #[cfg(not(windows))]
424        let other_normalized = other_str.to_string();
425
426        let normalized_other = if other_normalized.starts_with('/') {
427            other_normalized
428        } else {
429            format!("/{}", other_normalized)
430        };
431
432        normalized_virtual == normalized_other
433    }
434}
435
436impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
437    #[inline]
438    fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
439        // Compare virtual paths - the user-facing representation
440        let virtual_str = format!("{}", self.virtualpath_display());
441        let other_str = other.as_ref().to_string_lossy();
442
443        // Normalize both to forward slashes and ensure leading slash
444        let normalized_virtual = virtual_str.as_str();
445
446        #[cfg(windows)]
447        let other_normalized = other_str.replace('\\', "/");
448        #[cfg(not(windows))]
449        let other_normalized = other_str.to_string();
450
451        let normalized_other = if other_normalized.starts_with('/') {
452            other_normalized
453        } else {
454            format!("/{}", other_normalized)
455        };
456
457        Some(normalized_virtual.cmp(&normalized_other))
458    }
459}
460
461impl<Marker> PartialOrd for VirtualPath<Marker> {
462    #[inline]
463    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
464        Some(self.cmp(other))
465    }
466}
467
468impl<Marker> Ord for VirtualPath<Marker> {
469    #[inline]
470    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
471        self.inner.path().cmp(other.inner.path())
472    }
473}
474
475#[cfg(feature = "serde")]
476impl<Marker> serde::Serialize for VirtualPath<Marker> {
477    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
478    where
479        S: serde::Serializer,
480    {
481        serializer.serialize_str(&format!("{}", self.virtualpath_display()))
482    }
483}