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    /// Sugar for `VirtualRoot::try_new(root)?.virtual_join("")`.
35    pub fn with_root<P: AsRef<Path>>(root: P) -> Result<Self> {
36        let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
37        vroot.virtual_join("")
38    }
39
40    /// Create the virtual root (`"/"`), creating the filesystem root if missing.
41    ///
42    /// Sugar for `VirtualRoot::try_new_create(root)?.virtual_join("")`.
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.virtual_join("")
46    }
47    #[inline]
48    pub(crate) fn new(restricted_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(restricted_path.path(), restricted_path.boundary());
141
142        Self {
143            inner: restricted_path,
144            virtual_path,
145        }
146    }
147
148    /// Converts this `VirtualPath` back into a system-facing `StrictPath`.
149    #[inline]
150    pub fn unvirtual(self) -> StrictPath<Marker> {
151        self.inner
152    }
153
154    /// Borrows the underlying system-facing `StrictPath` (no allocation).
155    ///
156    /// Use this to pass a `&VirtualPath` to APIs that accept `&StrictPath`.
157    #[inline]
158    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
159        &self.inner
160    }
161
162    /// Returns the underlying system path as `&OsStr` for `AsRef<Path>` interop.
163    #[inline]
164    pub fn interop_path(&self) -> &OsStr {
165        self.inner.interop_path()
166    }
167
168    /// Safely joins a virtual path segment (virtual semantics) and re-validates.
169    #[inline]
170    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
171        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
172        let candidate = self.virtual_path.join(path.as_ref());
173        let anchored = crate::validator::path_history::PathHistory::new(candidate)
174            .canonicalize_anchored(self.inner.boundary())?;
175        let boundary_path = clamp(self.inner.boundary(), anchored)?;
176        Ok(VirtualPath::new(boundary_path))
177    }
178
179    // No local clamping helpers; virtual flows should route through
180    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
181
182    /// Returns the parent virtual path, or `None` if at the virtual root.
183    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
184        match self.virtual_path.parent() {
185            Some(parent_virtual_path) => {
186                let anchored = crate::validator::path_history::PathHistory::new(
187                    parent_virtual_path.to_path_buf(),
188                )
189                .canonicalize_anchored(self.inner.boundary())?;
190                let restricted_path = clamp(self.inner.boundary(), anchored)?;
191                Ok(Some(VirtualPath::new(restricted_path)))
192            }
193            None => Ok(None),
194        }
195    }
196
197    /// Returns a new `VirtualPath` with the file name changed, preserving clamping.
198    #[inline]
199    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
200        let candidate = self.virtual_path.with_file_name(file_name);
201        let anchored = crate::validator::path_history::PathHistory::new(candidate)
202            .canonicalize_anchored(self.inner.boundary())?;
203        let restricted_path = clamp(self.inner.boundary(), anchored)?;
204        Ok(VirtualPath::new(restricted_path))
205    }
206
207    /// Returns a new `VirtualPath` with the extension changed, preserving clamping.
208    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
209        if self.virtual_path.file_name().is_none() {
210            return Err(StrictPathError::path_escapes_boundary(
211                self.virtual_path.clone(),
212                self.inner.boundary().path().to_path_buf(),
213            ));
214        }
215
216        let candidate = self.virtual_path.with_extension(extension);
217        let anchored = crate::validator::path_history::PathHistory::new(candidate)
218            .canonicalize_anchored(self.inner.boundary())?;
219        let restricted_path = clamp(self.inner.boundary(), anchored)?;
220        Ok(VirtualPath::new(restricted_path))
221    }
222
223    /// Returns the file name component of the virtual path, if any.
224    #[inline]
225    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
226        self.virtual_path.file_name()
227    }
228
229    /// Returns the file stem of the virtual path, if any.
230    #[inline]
231    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
232        self.virtual_path.file_stem()
233    }
234
235    /// Returns the extension of the virtual path, if any.
236    #[inline]
237    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
238        self.virtual_path.extension()
239    }
240
241    /// Returns `true` if the virtual path starts with the given prefix (virtual semantics).
242    #[inline]
243    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
244        self.virtual_path.starts_with(p)
245    }
246
247    /// Returns `true` if the virtual path ends with the given suffix (virtual semantics).
248    #[inline]
249    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
250        self.virtual_path.ends_with(p)
251    }
252
253    /// Returns a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt"`).
254    #[inline]
255    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
256        VirtualPathDisplay(self)
257    }
258
259    /// Returns `true` if the underlying system path exists.
260    #[inline]
261    pub fn exists(&self) -> bool {
262        self.inner.exists()
263    }
264
265    /// Returns `true` if the underlying system path is a file.
266    #[inline]
267    pub fn is_file(&self) -> bool {
268        self.inner.is_file()
269    }
270
271    /// Returns `true` if the underlying system path is a directory.
272    #[inline]
273    pub fn is_dir(&self) -> bool {
274        self.inner.is_dir()
275    }
276
277    /// Returns metadata for the underlying system path.
278    #[inline]
279    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
280        self.inner.metadata()
281    }
282
283    /// Reads the file contents as `String` from the underlying system path.
284    #[inline]
285    pub fn read_to_string(&self) -> std::io::Result<String> {
286        self.inner.read_to_string()
287    }
288
289    /// Reads the file contents as raw bytes from the underlying system path.
290    #[inline]
291    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
292        self.inner.read_bytes()
293    }
294
295    /// Writes raw bytes to the underlying system path.
296    #[inline]
297    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
298        self.inner.write_bytes(data)
299    }
300
301    /// Writes a UTF-8 string to the underlying system path.
302    #[inline]
303    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
304        self.inner.write_string(data)
305    }
306
307    /// Creates all directories in the underlying system path if missing.
308    #[inline]
309    pub fn create_dir_all(&self) -> std::io::Result<()> {
310        self.inner.create_dir_all()
311    }
312
313    /// Creates the directory at this virtual location (non-recursive).
314    ///
315    /// Mirrors `std::fs::create_dir` and fails if the parent does not exist.
316    #[inline]
317    pub fn create_dir(&self) -> std::io::Result<()> {
318        self.inner.create_dir()
319    }
320
321    /// Creates only the immediate parent directory of this virtual path (non-recursive).
322    ///
323    /// Acts in the virtual dimension: the parent is derived via `virtualpath_parent()`
324    /// and then created on the underlying system path. Returns `Ok(())` at virtual root.
325    #[inline]
326    pub fn create_parent_dir(&self) -> std::io::Result<()> {
327        match self.virtualpath_parent() {
328            Ok(Some(parent)) => parent.create_dir(),
329            Ok(None) => Ok(()),
330            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
331            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
332        }
333    }
334
335    /// Recursively creates all missing directories up to the immediate parent of this virtual path.
336    ///
337    /// Acts in the virtual dimension; returns `Ok(())` at virtual root.
338    #[inline]
339    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
340        match self.virtualpath_parent() {
341            Ok(Some(parent)) => parent.create_dir_all(),
342            Ok(None) => Ok(()),
343            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
344            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
345        }
346    }
347
348    /// Removes the file at the underlying system path.
349    #[inline]
350    pub fn remove_file(&self) -> std::io::Result<()> {
351        self.inner.remove_file()
352    }
353
354    /// Removes the directory at the underlying system path.
355    #[inline]
356    pub fn remove_dir(&self) -> std::io::Result<()> {
357        self.inner.remove_dir()
358    }
359
360    /// Recursively removes the directory and its contents at the underlying system path.
361    #[inline]
362    pub fn remove_dir_all(&self) -> std::io::Result<()> {
363        self.inner.remove_dir_all()
364    }
365
366    /// Renames or moves this virtual path to a new location within the same virtual root.
367    ///
368    /// Destination paths are resolved in virtual space:
369    /// - Relative inputs are interpreted as siblings (resolved against the virtual parent).
370    /// - Absolute virtual inputs are treated as requests from the virtual root.
371    ///
372    /// Clamping and boundary checks are enforced by virtual joins. No parent directories are
373    /// created implicitly; call `create_parent_dir_all()` beforehand if needed.
374    pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
375        let dest_ref = dest.as_ref();
376        let dest_v = if dest_ref.is_absolute() {
377            match self.virtual_join(dest_ref) {
378                Ok(p) => p,
379                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
380            }
381        } else {
382            // Resolve as sibling under the current virtual parent (or root if at "/")
383            let parent = match self.virtualpath_parent() {
384                Ok(Some(p)) => p,
385                Ok(None) => match self.virtual_join("") {
386                    Ok(root) => root,
387                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
388                },
389                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
390            };
391            match parent.virtual_join(dest_ref) {
392                Ok(p) => p,
393                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
394            }
395        };
396
397        // Perform the actual rename via StrictPath
398        let moved_strict = self.inner.strict_rename(dest_v.inner.path())?;
399        Ok(moved_strict.virtualize())
400    }
401}
402
403pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
404
405impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        // Ensure leading slash and normalize to forward slashes for display
408        let s_lossy = self.0.virtual_path.to_string_lossy();
409        let s_norm: std::borrow::Cow<'_, str> = {
410            #[cfg(windows)]
411            {
412                std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
413            }
414            #[cfg(not(windows))]
415            {
416                std::borrow::Cow::Borrowed(&s_lossy)
417            }
418        };
419        if s_norm.starts_with('/') {
420            write!(f, "{s_norm}")
421        } else {
422            write!(f, "/{s_norm}")
423        }
424    }
425}
426
427impl<Marker> fmt::Debug for VirtualPath<Marker> {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        f.debug_struct("VirtualPath")
430            .field("system_path", &self.inner.path())
431            .field("virtual", &format!("{}", self.virtualpath_display()))
432            .field("boundary", &self.inner.boundary().path())
433            .field("marker", &std::any::type_name::<Marker>())
434            .finish()
435    }
436}
437
438impl<Marker> PartialEq for VirtualPath<Marker> {
439    #[inline]
440    fn eq(&self, other: &Self) -> bool {
441        self.inner.path() == other.inner.path()
442    }
443}
444
445impl<Marker> Eq for VirtualPath<Marker> {}
446
447impl<Marker> Hash for VirtualPath<Marker> {
448    #[inline]
449    fn hash<H: Hasher>(&self, state: &mut H) {
450        self.inner.path().hash(state);
451    }
452}
453
454impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
455    #[inline]
456    fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
457        self.inner.path() == other.path()
458    }
459}
460
461impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
462    #[inline]
463    fn eq(&self, other: &T) -> bool {
464        // Compare virtual paths - the user-facing representation
465        // If you want system path comparison, use as_unvirtual()
466        let virtual_str = format!("{}", self.virtualpath_display());
467        let other_str = other.as_ref().to_string_lossy();
468
469        // Normalize both to forward slashes and ensure leading slash
470        let normalized_virtual = virtual_str.as_str();
471
472        #[cfg(windows)]
473        let other_normalized = other_str.replace('\\', "/");
474        #[cfg(not(windows))]
475        let other_normalized = other_str.to_string();
476
477        let normalized_other = if other_normalized.starts_with('/') {
478            other_normalized
479        } else {
480            format!("/{}", other_normalized)
481        };
482
483        normalized_virtual == normalized_other
484    }
485}
486
487impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
488    #[inline]
489    fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
490        // Compare virtual paths - the user-facing representation
491        let virtual_str = format!("{}", self.virtualpath_display());
492        let other_str = other.as_ref().to_string_lossy();
493
494        // Normalize both to forward slashes and ensure leading slash
495        let normalized_virtual = virtual_str.as_str();
496
497        #[cfg(windows)]
498        let other_normalized = other_str.replace('\\', "/");
499        #[cfg(not(windows))]
500        let other_normalized = other_str.to_string();
501
502        let normalized_other = if other_normalized.starts_with('/') {
503            other_normalized
504        } else {
505            format!("/{}", other_normalized)
506        };
507
508        Some(normalized_virtual.cmp(&normalized_other))
509    }
510}
511
512impl<Marker> PartialOrd for VirtualPath<Marker> {
513    #[inline]
514    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
515        Some(self.cmp(other))
516    }
517}
518
519impl<Marker> Ord for VirtualPath<Marker> {
520    #[inline]
521    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
522        self.inner.path().cmp(other.inner.path())
523    }
524}
525
526#[cfg(feature = "serde")]
527impl<Marker> serde::Serialize for VirtualPath<Marker> {
528    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
529    where
530        S: serde::Serializer,
531    {
532        serializer.serialize_str(&format!("{}", self.virtualpath_display()))
533    }
534}