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/// SUMMARY:
13/// Hold a user‑facing path clamped to a virtual root (`"/"`) over a `PathBoundary`.
14///
15/// DETAILS:
16/// `virtualpath_display()` shows rooted, forward‑slashed paths (e.g., `"/a/b.txt"`).
17/// Use virtual manipulation methods to compose paths while preserving clamping, then convert to
18/// `StrictPath` with `unvirtual()` for system‑facing I/O.
19#[derive(Clone)]
20pub struct VirtualPath<Marker = ()> {
21    inner: StrictPath<Marker>,
22    virtual_path: PathBuf,
23}
24
25#[inline]
26fn clamp<Marker, H>(
27    restriction: &PathBoundary<Marker>,
28    anchored: PathHistory<(H, Canonicalized)>,
29) -> crate::Result<crate::path::strict_path::StrictPath<Marker>> {
30    restriction.strict_join(anchored.into_inner())
31}
32
33impl<Marker> VirtualPath<Marker> {
34    /// SUMMARY:
35    /// Create the virtual root (`"/"`) for the given filesystem root.
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    /// SUMMARY:
42    /// Create the virtual root, creating the filesystem root if missing.
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(strict_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(strict_path.path(), strict_path.boundary());
141
142        Self {
143            inner: strict_path,
144            virtual_path,
145        }
146    }
147
148    /// SUMMARY:
149    /// Convert this `VirtualPath` back into a system‑facing `StrictPath`.
150    #[inline]
151    pub fn unvirtual(self) -> StrictPath<Marker> {
152        self.inner
153    }
154
155    /// SUMMARY:
156    /// Consume and return the `VirtualRoot` for its boundary (no directory creation).
157    #[inline]
158    pub fn try_into_root(self) -> crate::validator::virtual_root::VirtualRoot<Marker> {
159        self.inner.try_into_boundary().virtualize()
160    }
161
162    /// SUMMARY:
163    /// Consume and return a `VirtualRoot`, creating the underlying directory if missing.
164    #[inline]
165    pub fn try_into_root_create(self) -> crate::validator::virtual_root::VirtualRoot<Marker> {
166        let boundary = self.inner.try_into_boundary();
167        if !boundary.exists() {
168            // Best-effort create; ignore error and let later operations surface it
169            let _ = std::fs::create_dir_all(boundary.as_ref());
170        }
171        boundary.virtualize()
172    }
173
174    /// SUMMARY:
175    /// Borrow the underlying system‑facing `StrictPath` (no allocation).
176    #[inline]
177    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
178        &self.inner
179    }
180
181    /// SUMMARY:
182    /// Return the underlying system path as `&OsStr` for `AsRef<Path>` interop.
183    #[inline]
184    pub fn interop_path(&self) -> &OsStr {
185        self.inner.interop_path()
186    }
187
188    /// SUMMARY:
189    /// Join a virtual path segment (virtual semantics) and re‑validate within the same restriction.
190    #[inline]
191    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
192        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
193        let candidate = self.virtual_path.join(path.as_ref());
194        let anchored = crate::validator::path_history::PathHistory::new(candidate)
195            .canonicalize_anchored(self.inner.boundary())?;
196        let boundary_path = clamp(self.inner.boundary(), anchored)?;
197        Ok(VirtualPath::new(boundary_path))
198    }
199
200    // No local clamping helpers; virtual flows should route through
201    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
202
203    /// SUMMARY:
204    /// Return the parent virtual path, or `None` at the virtual root.
205    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
206        match self.virtual_path.parent() {
207            Some(parent_virtual_path) => {
208                let anchored = crate::validator::path_history::PathHistory::new(
209                    parent_virtual_path.to_path_buf(),
210                )
211                .canonicalize_anchored(self.inner.boundary())?;
212                let validated_path = clamp(self.inner.boundary(), anchored)?;
213                Ok(Some(VirtualPath::new(validated_path)))
214            }
215            None => Ok(None),
216        }
217    }
218
219    /// SUMMARY:
220    /// Return a new virtual path with file name changed, preserving clamping.
221    #[inline]
222    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
223        let candidate = self.virtual_path.with_file_name(file_name);
224        let anchored = crate::validator::path_history::PathHistory::new(candidate)
225            .canonicalize_anchored(self.inner.boundary())?;
226        let validated_path = clamp(self.inner.boundary(), anchored)?;
227        Ok(VirtualPath::new(validated_path))
228    }
229
230    /// SUMMARY:
231    /// Return a new virtual path with the extension changed, preserving clamping.
232    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
233        if self.virtual_path.file_name().is_none() {
234            return Err(StrictPathError::path_escapes_boundary(
235                self.virtual_path.clone(),
236                self.inner.boundary().path().to_path_buf(),
237            ));
238        }
239
240        let candidate = self.virtual_path.with_extension(extension);
241        let anchored = crate::validator::path_history::PathHistory::new(candidate)
242            .canonicalize_anchored(self.inner.boundary())?;
243        let validated_path = clamp(self.inner.boundary(), anchored)?;
244        Ok(VirtualPath::new(validated_path))
245    }
246
247    /// SUMMARY:
248    /// Return 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    /// SUMMARY:
255    /// Return the file stem of the virtual path, if any.
256    #[inline]
257    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
258        self.virtual_path.file_stem()
259    }
260
261    /// SUMMARY:
262    /// Return the extension of the virtual path, if any.
263    #[inline]
264    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
265        self.virtual_path.extension()
266    }
267
268    /// SUMMARY:
269    /// Return `true` if the virtual path starts with the given prefix (virtual semantics).
270    #[inline]
271    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
272        self.virtual_path.starts_with(p)
273    }
274
275    /// SUMMARY:
276    /// Return `true` if the virtual path ends with the given suffix (virtual semantics).
277    #[inline]
278    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
279        self.virtual_path.ends_with(p)
280    }
281
282    /// SUMMARY:
283    /// Return a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt").
284    #[inline]
285    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
286        VirtualPathDisplay(self)
287    }
288
289    /// SUMMARY:
290    /// Return `true` if the underlying system path exists.
291    #[inline]
292    pub fn exists(&self) -> bool {
293        self.inner.exists()
294    }
295
296    /// SUMMARY:
297    /// Return `true` if the underlying system path is a file.
298    #[inline]
299    pub fn is_file(&self) -> bool {
300        self.inner.is_file()
301    }
302
303    /// SUMMARY:
304    /// Return `true` if the underlying system path is a directory.
305    #[inline]
306    pub fn is_dir(&self) -> bool {
307        self.inner.is_dir()
308    }
309
310    /// SUMMARY:
311    /// Return metadata for the underlying system path.
312    #[inline]
313    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
314        self.inner.metadata()
315    }
316
317    /// SUMMARY:
318    /// Read the file contents as `String` from the underlying system path.
319    #[inline]
320    pub fn read_to_string(&self) -> std::io::Result<String> {
321        self.inner.read_to_string()
322    }
323
324    /// Reads the file contents as raw bytes from the underlying system path.
325    #[deprecated(since = "0.1.0-alpha.5", note = "Use read() instead")]
326    #[inline]
327    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
328        self.inner.read()
329    }
330
331    /// Writes raw bytes to the underlying system path.
332    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
333    #[inline]
334    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
335        self.inner.write(data)
336    }
337
338    /// Writes a UTF-8 string to the underlying system path.
339    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
340    #[inline]
341    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
342        self.inner.write(data)
343    }
344
345    /// SUMMARY:
346    /// Read raw bytes from the underlying system path (replacement for `read_bytes`).
347    #[inline]
348    pub fn read(&self) -> std::io::Result<Vec<u8>> {
349        self.inner.read()
350    }
351
352    /// SUMMARY:
353    /// Read directory entries (discovery). Re‑join names with `virtual_join(...)` to preserve clamping.
354    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
355        self.inner.read_dir()
356    }
357
358    /// SUMMARY:
359    /// Write bytes to the underlying system path. Accepts `&str`, `String`, `&[u8]`, `Vec<u8]`, etc.
360    #[inline]
361    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
362        self.inner.write(contents)
363    }
364
365    /// SUMMARY:
366    /// Create all directories in the underlying system path if missing.
367    #[inline]
368    pub fn create_dir_all(&self) -> std::io::Result<()> {
369        self.inner.create_dir_all()
370    }
371
372    /// SUMMARY:
373    /// Create the directory at this virtual location (non‑recursive). Fails if parent missing.
374    #[inline]
375    pub fn create_dir(&self) -> std::io::Result<()> {
376        self.inner.create_dir()
377    }
378
379    /// SUMMARY:
380    /// Create only the immediate parent of this virtual path (non‑recursive). `Ok(())` at virtual root.
381    #[inline]
382    pub fn create_parent_dir(&self) -> std::io::Result<()> {
383        match self.virtualpath_parent() {
384            Ok(Some(parent)) => parent.create_dir(),
385            Ok(None) => Ok(()),
386            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
387            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
388        }
389    }
390
391    /// SUMMARY:
392    /// Recursively create all missing directories up to the immediate parent. `Ok(())` at virtual root.
393    #[inline]
394    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
395        match self.virtualpath_parent() {
396            Ok(Some(parent)) => parent.create_dir_all(),
397            Ok(None) => Ok(()),
398            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
399            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
400        }
401    }
402
403    /// SUMMARY:
404    /// Remove the file at the underlying system path.
405    #[inline]
406    pub fn remove_file(&self) -> std::io::Result<()> {
407        self.inner.remove_file()
408    }
409
410    /// SUMMARY:
411    /// Remove the directory at the underlying system path.
412    #[inline]
413    pub fn remove_dir(&self) -> std::io::Result<()> {
414        self.inner.remove_dir()
415    }
416
417    /// SUMMARY:
418    /// Recursively remove the directory and its contents at the underlying system path.
419    #[inline]
420    pub fn remove_dir_all(&self) -> std::io::Result<()> {
421        self.inner.remove_dir_all()
422    }
423
424    /// SUMMARY:
425    /// Create a symlink at this virtual location pointing to `target` (same virtual root required).
426    pub fn virtual_symlink(&self, link_path: &Self) -> std::io::Result<()> {
427        if self.inner.boundary().path() != link_path.inner.boundary().path() {
428            let err = StrictPathError::path_escapes_boundary(
429                link_path.inner.path().to_path_buf(),
430                self.inner.boundary().path().to_path_buf(),
431            );
432            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
433        }
434
435        self.inner.strict_symlink(&link_path.inner)
436    }
437
438    /// SUMMARY:
439    /// Create a hard link at this virtual location pointing to `target` (same virtual root).
440    pub fn virtual_hard_link(&self, link_path: &Self) -> std::io::Result<()> {
441        if self.inner.boundary().path() != link_path.inner.boundary().path() {
442            let err = StrictPathError::path_escapes_boundary(
443                link_path.inner.path().to_path_buf(),
444                self.inner.boundary().path().to_path_buf(),
445            );
446            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
447        }
448
449        self.inner.strict_hard_link(&link_path.inner)
450    }
451
452    /// SUMMARY:
453    /// Rename/move within the same virtual root. Relative destinations are siblings; absolute are from root.
454    /// Parents are not created automatically.
455    pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
456        let dest_ref = dest.as_ref();
457        let dest_v = if dest_ref.is_absolute() {
458            match self.virtual_join(dest_ref) {
459                Ok(p) => p,
460                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
461            }
462        } else {
463            // Resolve as sibling under the current virtual parent (or root if at "/")
464            let parent = match self.virtualpath_parent() {
465                Ok(Some(p)) => p,
466                Ok(None) => match self.virtual_join("") {
467                    Ok(root) => root,
468                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
469                },
470                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
471            };
472            match parent.virtual_join(dest_ref) {
473                Ok(p) => p,
474                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
475            }
476        };
477
478        // Perform the actual rename via StrictPath
479        self.inner.strict_rename(dest_v.inner.path())
480    }
481
482    /// SUMMARY:
483    /// Copy within the same virtual root. Relative destinations are siblings; absolute are from root.
484    /// Parents are not created automatically. Returns bytes copied.
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}