Skip to main content

strict_path/path/virtual_path/
mod.rs

1mod display;
2mod fs;
3mod iter;
4mod links;
5mod traits;
6
7pub use display::VirtualPathDisplay;
8pub use iter::VirtualReadDir;
9
10// Content copied from original src/path/virtual_path.rs
11use crate::error::StrictPathError;
12use crate::path::strict_path::StrictPath;
13use crate::validator::path_history::{Canonicalized, PathHistory};
14use crate::PathBoundary;
15use crate::Result;
16use std::ffi::OsStr;
17use std::path::{Path, PathBuf};
18
19/// SUMMARY:
20/// Hold a user‑facing path clamped to a virtual root (`"/"`) over a `PathBoundary`.
21///
22/// DETAILS:
23/// `virtualpath_display()` shows rooted, forward‑slashed paths (e.g., `"/a/b.txt"`).
24/// Use virtual manipulation methods to compose paths while preserving clamping, then convert to
25/// `StrictPath` with `unvirtual()` for system‑facing I/O.
26#[derive(Clone)]
27pub struct VirtualPath<Marker = ()> {
28    pub(crate) inner: StrictPath<Marker>,
29    pub(crate) virtual_path: PathBuf,
30}
31
32#[inline]
33fn clamp<Marker, H>(
34    restriction: &PathBoundary<Marker>,
35    anchored: PathHistory<(H, Canonicalized)>,
36) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
37    restriction.strict_join(anchored.into_inner())
38}
39
40impl<Marker> VirtualPath<Marker> {
41    /// SUMMARY:
42    /// Create the virtual root (`"/"`) for the given filesystem root.
43    pub fn with_root<P: AsRef<Path>>(root: P) -> Result<Self> {
44        let vroot = crate::validator::virtual_root::VirtualRoot::try_new(root)?;
45        vroot.into_virtualpath()
46    }
47
48    /// SUMMARY:
49    /// Create the virtual root, creating the filesystem root if missing.
50    pub fn with_root_create<P: AsRef<Path>>(root: P) -> Result<Self> {
51        let vroot = crate::validator::virtual_root::VirtualRoot::try_new_create(root)?;
52        vroot.into_virtualpath()
53    }
54
55    #[inline]
56    pub(crate) fn new(strict_path: StrictPath<Marker>) -> Self {
57        fn compute_virtual<Marker>(
58            system_path: &std::path::Path,
59            restriction: &crate::PathBoundary<Marker>,
60        ) -> std::path::PathBuf {
61            use std::ffi::OsString;
62            use std::path::Component;
63
64            #[cfg(windows)]
65            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
66                let s = p.as_os_str().to_string_lossy();
67                if let Some(trimmed) = s.strip_prefix("\\\\?\\") {
68                    return std::path::PathBuf::from(trimmed);
69                }
70                if let Some(trimmed) = s.strip_prefix("\\\\.\\") {
71                    return std::path::PathBuf::from(trimmed);
72                }
73                std::path::PathBuf::from(s.to_string())
74            }
75
76            #[cfg(not(windows))]
77            fn strip_verbatim(p: &std::path::Path) -> std::path::PathBuf {
78                p.to_path_buf()
79            }
80
81            let system_norm = strip_verbatim(system_path);
82            let jail_norm = strip_verbatim(restriction.path());
83
84            if let Ok(stripped) = system_norm.strip_prefix(&jail_norm) {
85                let mut cleaned = std::path::PathBuf::new();
86                for comp in stripped.components() {
87                    if let Component::Normal(name) = comp {
88                        let s = name.to_string_lossy();
89                        let cleaned_s = s.replace(['\n', ';'], "_");
90                        if cleaned_s == s {
91                            cleaned.push(name);
92                        } else {
93                            cleaned.push(OsString::from(cleaned_s));
94                        }
95                    }
96                }
97                return cleaned;
98            }
99
100            let mut strictpath_comps: Vec<_> = system_norm
101                .components()
102                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
103                .collect();
104            let mut boundary_comps: Vec<_> = jail_norm
105                .components()
106                .filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir))
107                .collect();
108
109            #[cfg(windows)]
110            fn comp_eq(a: &Component, b: &Component) -> bool {
111                match (a, b) {
112                    (Component::Normal(x), Component::Normal(y)) => {
113                        x.to_string_lossy().to_ascii_lowercase()
114                            == y.to_string_lossy().to_ascii_lowercase()
115                    }
116                    _ => false,
117                }
118            }
119
120            #[cfg(not(windows))]
121            fn comp_eq(a: &Component, b: &Component) -> bool {
122                a == b
123            }
124
125            while !strictpath_comps.is_empty()
126                && !boundary_comps.is_empty()
127                && comp_eq(&strictpath_comps[0], &boundary_comps[0])
128            {
129                strictpath_comps.remove(0);
130                boundary_comps.remove(0);
131            }
132
133            let mut vb = std::path::PathBuf::new();
134            for c in strictpath_comps {
135                if let Component::Normal(name) = c {
136                    let s = name.to_string_lossy();
137                    let cleaned = s.replace(['\n', ';'], "_");
138                    if cleaned == s {
139                        vb.push(name);
140                    } else {
141                        vb.push(OsString::from(cleaned));
142                    }
143                }
144            }
145            vb
146        }
147
148        let virtual_path = compute_virtual(strict_path.path(), strict_path.boundary());
149
150        Self {
151            inner: strict_path,
152            virtual_path,
153        }
154    }
155
156    /// SUMMARY:
157    /// Convert this `VirtualPath` back into a system‑facing `StrictPath`.
158    #[inline]
159    pub fn unvirtual(self) -> StrictPath<Marker> {
160        self.inner
161    }
162
163    /// SUMMARY:
164    /// Change the compile-time marker while keeping the virtual and strict views in sync.
165    ///
166    /// WHEN TO USE:
167    /// - After authenticating/authorizing a user and granting them access to a virtual path
168    /// - When escalating or downgrading permissions (e.g., ReadOnly → ReadWrite)
169    /// - When reinterpreting a path's domain (e.g., TempStorage → UserUploads)
170    ///
171    /// WHEN NOT TO USE:
172    /// - When converting between path types - conversions preserve markers automatically
173    /// - When the current marker already matches your needs - no transformation needed
174    /// - When you haven't verified authorization - NEVER change markers without checking permissions
175    ///
176    /// PARAMETERS:
177    /// - `_none_`
178    ///
179    /// RETURNS:
180    /// - `VirtualPath<NewMarker>`: Same clamped path encoded with the new marker.
181    ///
182    /// ERRORS:
183    /// - `_none_`
184    ///
185    /// SECURITY:
186    /// This method performs no permission checks. Only elevate markers after verifying real
187    /// authorization out-of-band.
188    ///
189    /// EXAMPLE:
190    /// ```rust
191    /// # use strict_path::VirtualPath;
192    /// # struct GuestAccess;
193    /// # struct UserAccess;
194    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-example");
195    /// # std::fs::create_dir_all(&root_dir)?;
196    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir)?;
197    /// // Simulated authorization: verify user credentials before granting access
198    /// fn grant_user_access(user_token: &str, path: VirtualPath<GuestAccess>) -> Option<VirtualPath<UserAccess>> {
199    ///     if user_token == "valid-token-12345" {
200    ///         Some(path.change_marker())  // ✅ Only after token validation
201    ///     } else {
202    ///         None  // ❌ Invalid token
203    ///     }
204    /// }
205    ///
206    /// // Untrusted input from request/CLI/config/etc.
207    /// let requested_file = "docs/readme.md";
208    /// let guest_path: VirtualPath<GuestAccess> = guest_root.virtual_join(requested_file)?;
209    /// let user_path = grant_user_access("valid-token-12345", guest_path).expect("authorized");
210    /// assert_eq!(user_path.virtualpath_display().to_string(), "/docs/readme.md");
211    /// # std::fs::remove_dir_all(&root_dir)?;
212    /// # Ok::<_, Box<dyn std::error::Error>>(())
213    /// ```
214    ///
215    /// **Type Safety Guarantee:**
216    ///
217    /// The following code **fails to compile** because you cannot pass a path with one marker
218    /// type to a function expecting a different marker type. This compile-time check enforces
219    /// that permission changes are explicit and cannot be bypassed accidentally.
220    ///
221    /// ```compile_fail
222    /// # use strict_path::VirtualPath;
223    /// # struct GuestAccess;
224    /// # struct EditorAccess;
225    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-deny");
226    /// # std::fs::create_dir_all(&root_dir).unwrap();
227    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir).unwrap();
228    /// fn require_editor(_: VirtualPath<EditorAccess>) {}
229    /// let guest_file = guest_root.virtual_join("docs/manual.txt").unwrap();
230    /// // ❌ Compile error: expected `VirtualPath<EditorAccess>`, found `VirtualPath<GuestAccess>`
231    /// require_editor(guest_file);
232    /// ```
233    #[inline]
234    pub fn change_marker<NewMarker>(self) -> VirtualPath<NewMarker> {
235        let VirtualPath {
236            inner,
237            virtual_path,
238        } = self;
239
240        VirtualPath {
241            inner: inner.change_marker(),
242            virtual_path,
243        }
244    }
245
246    /// SUMMARY:
247    /// Consume and return the `VirtualRoot` for its boundary (no directory creation).
248    ///
249    /// RETURNS:
250    /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory.
251    ///
252    /// ERRORS:
253    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` when the
254    ///   strict path does not exist or is not a directory.
255    #[inline]
256    pub fn try_into_root(self) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
257        Ok(self.inner.try_into_boundary()?.virtualize())
258    }
259
260    /// SUMMARY:
261    /// Consume and return a `VirtualRoot`, creating the underlying directory if missing.
262    ///
263    /// RETURNS:
264    /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory
265    ///   (created if necessary).
266    ///
267    /// ERRORS:
268    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` or directory
269    ///   creation failures wrapped in `InvalidRestriction`.
270    #[inline]
271    pub fn try_into_root_create(
272        self,
273    ) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
274        let strict_path = self.inner;
275        let validated_dir = strict_path.try_into_boundary_create()?;
276        Ok(validated_dir.virtualize())
277    }
278
279    /// SUMMARY:
280    /// Borrow the underlying system‑facing `StrictPath` (no allocation).
281    #[inline]
282    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
283        &self.inner
284    }
285
286    /// SUMMARY:
287    /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
288    #[inline]
289    pub fn interop_path(&self) -> &OsStr {
290        self.inner.interop_path()
291    }
292
293    /// SUMMARY:
294    /// Join a virtual path segment (virtual semantics) and re‑validate within the same restriction.
295    ///
296    /// DETAILS:
297    /// Applies virtual path clamping: absolute paths are interpreted relative to the virtual root,
298    /// and traversal attempts are clamped to prevent escaping the boundary. This method maintains
299    /// the security guarantee that all `VirtualPath` instances stay within their virtual root.
300    ///
301    /// PARAMETERS:
302    /// - `path` (`impl AsRef<Path>`): Path segment to join. Absolute paths are clamped to virtual root.
303    ///
304    /// RETURNS:
305    /// - `Result<VirtualPath<Marker>>`: New virtual path within the same restriction.
306    ///
307    /// EXAMPLE:
308    /// ```rust
309    /// # use strict_path::VirtualRoot;
310    /// # let td = tempfile::tempdir().unwrap();
311    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
312    /// let base = vroot.virtual_join("data")?;
313    ///
314    /// // Absolute paths are clamped to virtual root
315    /// let abs = base.virtual_join("/etc/config")?;
316    /// assert_eq!(abs.virtualpath_display().to_string(), "/etc/config");
317    /// # Ok::<(), Box<dyn std::error::Error>>(())
318    /// ```
319    #[inline]
320    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
321        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
322        let candidate = self.virtual_path.join(path.as_ref());
323        let anchored = crate::validator::path_history::PathHistory::new(candidate)
324            .canonicalize_anchored(self.inner.boundary())?;
325        let boundary_path = clamp(self.inner.boundary(), anchored)?;
326        Ok(VirtualPath::new(boundary_path))
327    }
328
329    // No local clamping helpers; virtual flows should route through
330    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
331
332    /// SUMMARY:
333    /// Return the parent virtual path, or `None` at the virtual root.
334    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
335        match self.virtual_path.parent() {
336            Some(parent_virtual_path) => {
337                let anchored = crate::validator::path_history::PathHistory::new(
338                    parent_virtual_path.to_path_buf(),
339                )
340                .canonicalize_anchored(self.inner.boundary())?;
341                let validated_path = clamp(self.inner.boundary(), anchored)?;
342                Ok(Some(VirtualPath::new(validated_path)))
343            }
344            None => Ok(None),
345        }
346    }
347
348    /// SUMMARY:
349    /// Return a new virtual path with file name changed, preserving clamping.
350    #[inline]
351    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
352        let candidate = self.virtual_path.with_file_name(file_name);
353        let anchored = crate::validator::path_history::PathHistory::new(candidate)
354            .canonicalize_anchored(self.inner.boundary())?;
355        let validated_path = clamp(self.inner.boundary(), anchored)?;
356        Ok(VirtualPath::new(validated_path))
357    }
358
359    /// SUMMARY:
360    /// Return a new virtual path with the extension changed, preserving clamping.
361    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
362        if self.virtual_path.file_name().is_none() {
363            return Err(StrictPathError::path_escapes_boundary(
364                self.virtual_path.clone(),
365                self.inner.boundary().path().to_path_buf(),
366            ));
367        }
368
369        let candidate = self.virtual_path.with_extension(extension);
370        let anchored = crate::validator::path_history::PathHistory::new(candidate)
371            .canonicalize_anchored(self.inner.boundary())?;
372        let validated_path = clamp(self.inner.boundary(), anchored)?;
373        Ok(VirtualPath::new(validated_path))
374    }
375
376    /// SUMMARY:
377    /// Return the file name component of the virtual path, if any.
378    #[inline]
379    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
380        self.virtual_path.file_name()
381    }
382
383    /// SUMMARY:
384    /// Return the file stem of the virtual path, if any.
385    #[inline]
386    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
387        self.virtual_path.file_stem()
388    }
389
390    /// SUMMARY:
391    /// Return the extension of the virtual path, if any.
392    #[inline]
393    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
394        self.virtual_path.extension()
395    }
396
397    /// SUMMARY:
398    /// Return `true` if the virtual path starts with the given prefix (virtual semantics).
399    #[inline]
400    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
401        self.virtual_path.starts_with(p)
402    }
403
404    /// SUMMARY:
405    /// Return `true` if the virtual path ends with the given suffix (virtual semantics).
406    #[inline]
407    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
408        self.virtual_path.ends_with(p)
409    }
410
411    /// SUMMARY:
412    /// Return a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt").
413    #[inline]
414    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
415        VirtualPathDisplay(self)
416    }
417
418    /// SUMMARY:
419    /// Return `true` if the underlying system path exists.
420    #[inline]
421    pub fn exists(&self) -> bool {
422        self.inner.exists()
423    }
424
425    /// SUMMARY:
426    /// Return `true` if the underlying system path is a file.
427    #[inline]
428    pub fn is_file(&self) -> bool {
429        self.inner.is_file()
430    }
431
432    /// SUMMARY:
433    /// Return `true` if the underlying system path is a directory.
434    #[inline]
435    pub fn is_dir(&self) -> bool {
436        self.inner.is_dir()
437    }
438
439    /// SUMMARY:
440    /// Return metadata for the underlying system path.
441    #[inline]
442    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
443        self.inner.metadata()
444    }
445
446    /// SUMMARY:
447    /// Read the file contents as `String` from the underlying system path.
448    #[inline]
449    pub fn read_to_string(&self) -> std::io::Result<String> {
450        self.inner.read_to_string()
451    }
452
453    /// SUMMARY:
454    /// Read raw bytes from the underlying system path.
455    #[inline]
456    pub fn read(&self) -> std::io::Result<Vec<u8>> {
457        self.inner.read()
458    }
459
460    /// SUMMARY:
461    /// Return metadata for the underlying system path without following symlinks.
462    #[inline]
463    pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
464        self.inner.symlink_metadata()
465    }
466
467    /// SUMMARY:
468    /// Set permissions on the file or directory at this path.
469    ///
470    /// PARAMETERS:
471    /// - `perm` (`std::fs::Permissions`): The permissions to set.
472    ///
473    /// RETURNS:
474    /// - `io::Result<()>`: Success or I/O error.
475    #[inline]
476    pub fn set_permissions(&self, perm: std::fs::Permissions) -> std::io::Result<()> {
477        self.inner.set_permissions(perm)
478    }
479
480    /// SUMMARY:
481    /// Check if the path exists, returning an error on permission issues.
482    ///
483    /// DETAILS:
484    /// Unlike `exists()` which returns `false` on permission errors, this method
485    /// distinguishes between "path does not exist" (`Ok(false)`) and "cannot check
486    /// due to permission error" (`Err(...)`).
487    ///
488    /// RETURNS:
489    /// - `Ok(true)`: Path exists
490    /// - `Ok(false)`: Path does not exist
491    /// - `Err(...)`: Permission or other I/O error prevented the check
492    #[inline]
493    pub fn try_exists(&self) -> std::io::Result<bool> {
494        self.inner.try_exists()
495    }
496
497    /// SUMMARY:
498    /// Create an empty file if it doesn't exist, or update the modification time if it does.
499    ///
500    /// DETAILS:
501    /// This is a convenience method combining file creation and mtime update.
502    /// Uses `OpenOptions` with `create(true).write(true)` which creates the file
503    /// if missing or opens it for writing if it exists, updating mtime on close.
504    ///
505    /// RETURNS:
506    /// - `io::Result<()>`: Success or I/O error.
507    pub fn touch(&self) -> std::io::Result<()> {
508        self.inner.touch()
509    }
510
511    /// SUMMARY:
512    /// Read directory entries (discovery). Re‑join names with `virtual_join(...)` to preserve clamping.
513    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
514        self.inner.read_dir()
515    }
516
517    /// SUMMARY:
518    /// Read directory entries as validated `VirtualPath` values (auto re-joins each entry).
519    ///
520    /// DETAILS:
521    /// Unlike `read_dir()` which returns raw `std::fs::DirEntry`, this method automatically
522    /// validates each directory entry through `virtual_join()`, returning an iterator of
523    /// `Result<VirtualPath<Marker>>`. This eliminates the need for manual re-validation loops
524    /// while preserving the virtual path semantics.
525    ///
526    /// PARAMETERS:
527    /// - _none_
528    ///
529    /// RETURNS:
530    /// - `io::Result<VirtualReadDir<Marker>>`: Iterator yielding validated `VirtualPath` entries.
531    ///
532    /// ERRORS:
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    /// EXAMPLE:
537    /// ```rust
538    /// # use strict_path::{VirtualRoot, VirtualPath};
539    /// # let temp = tempfile::tempdir()?;
540    /// # let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
541    /// # let dir = vroot.virtual_join("uploads")?;
542    /// # dir.create_dir_all()?;
543    /// # vroot.virtual_join("uploads/file1.txt")?.write("a")?;
544    /// # vroot.virtual_join("uploads/file2.txt")?.write("b")?;
545    /// // Iterate with automatic validation
546    /// for entry in dir.virtual_read_dir()? {
547    ///     let child: VirtualPath = entry?;
548    ///     println!("{}", child.virtualpath_display());
549    /// }
550    /// # Ok::<_, Box<dyn std::error::Error>>(())
551    /// ```
552    pub fn virtual_read_dir(&self) -> std::io::Result<VirtualReadDir<'_, Marker>> {
553        let inner = std::fs::read_dir(self.inner.path())?;
554        Ok(VirtualReadDir {
555            inner,
556            parent: self,
557        })
558    }
559}