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.into_virtualpath()
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.into_virtualpath()
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    /// Change the compile-time marker while keeping the virtual and strict views in sync.
157    ///
158    /// WHEN TO USE:
159    /// - After authenticating/authorizing a user and granting them access to a virtual path
160    /// - When escalating or downgrading permissions (e.g., ReadOnly → ReadWrite)
161    /// - When reinterpreting a path's domain (e.g., TempStorage → UserUploads)
162    ///
163    /// WHEN NOT TO USE:
164    /// - When converting between path types - conversions preserve markers automatically
165    /// - When the current marker already matches your needs - no transformation needed
166    /// - When you haven't verified authorization - NEVER change markers without checking permissions
167    ///
168    /// PARAMETERS:
169    /// - `_none_`
170    ///
171    /// RETURNS:
172    /// - `VirtualPath<NewMarker>`: Same clamped path encoded with the new marker.
173    ///
174    /// ERRORS:
175    /// - `_none_`
176    ///
177    /// SECURITY:
178    /// This method performs no permission checks. Only elevate markers after verifying real
179    /// authorization out-of-band.
180    ///
181    /// EXAMPLE:
182    /// ```rust
183    /// # use strict_path::VirtualPath;
184    /// # struct GuestAccess;
185    /// # struct UserAccess;
186    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-example");
187    /// # std::fs::create_dir_all(&root_dir)?;
188    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir)?;
189    /// // Simulated authorization: verify user credentials before granting access
190    /// fn grant_user_access(user_token: &str, path: VirtualPath<GuestAccess>) -> Option<VirtualPath<UserAccess>> {
191    ///     if user_token == "valid-token-12345" {
192    ///         Some(path.change_marker())  // ✅ Only after token validation
193    ///     } else {
194    ///         None  // ❌ Invalid token
195    ///     }
196    /// }
197    ///
198    /// let guest_path: VirtualPath<GuestAccess> = guest_root.virtual_join("docs/readme.md")?;
199    /// let user_path = grant_user_access("valid-token-12345", guest_path).expect("authorized");
200    /// assert_eq!(user_path.virtualpath_display().to_string(), "/docs/readme.md");
201    /// # std::fs::remove_dir_all(&root_dir)?;
202    /// # Ok::<_, Box<dyn std::error::Error>>(())
203    /// ```
204    ///
205    /// **Type Safety Guarantee:**
206    ///
207    /// The following code **fails to compile** because you cannot pass a path with one marker
208    /// type to a function expecting a different marker type. This compile-time check enforces
209    /// that permission changes are explicit and cannot be bypassed accidentally.
210    ///
211    /// ```compile_fail
212    /// # use strict_path::VirtualPath;
213    /// # struct GuestAccess;
214    /// # struct EditorAccess;
215    /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-deny");
216    /// # std::fs::create_dir_all(&root_dir).unwrap();
217    /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir).unwrap();
218    /// fn require_editor(_: VirtualPath<EditorAccess>) {}
219    /// let guest_file = guest_root.virtual_join("docs/manual.txt").unwrap();
220    /// // ❌ Compile error: expected `VirtualPath<EditorAccess>`, found `VirtualPath<GuestAccess>`
221    /// require_editor(guest_file);
222    /// ```
223    #[inline]
224    pub fn change_marker<NewMarker>(self) -> VirtualPath<NewMarker> {
225        let VirtualPath {
226            inner,
227            virtual_path,
228        } = self;
229
230        VirtualPath {
231            inner: inner.change_marker(),
232            virtual_path,
233        }
234    }
235
236    /// SUMMARY:
237    /// Consume and return the `VirtualRoot` for its boundary (no directory creation).
238    ///
239    /// RETURNS:
240    /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory.
241    ///
242    /// ERRORS:
243    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` when the
244    ///   strict path does not exist or is not a directory.
245    #[inline]
246    pub fn try_into_root(self) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
247        Ok(self.inner.try_into_boundary()?.virtualize())
248    }
249
250    /// SUMMARY:
251    /// Consume and return a `VirtualRoot`, creating the underlying directory if missing.
252    ///
253    /// RETURNS:
254    /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory
255    ///   (created if necessary).
256    ///
257    /// ERRORS:
258    /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` or directory
259    ///   creation failures wrapped in `InvalidRestriction`.
260    #[inline]
261    pub fn try_into_root_create(
262        self,
263    ) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
264        let strict_path = self.inner;
265        let boundary = strict_path.try_into_boundary_create()?;
266        Ok(boundary.virtualize())
267    }
268
269    /// SUMMARY:
270    /// Borrow the underlying system‑facing `StrictPath` (no allocation).
271    #[inline]
272    pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
273        &self.inner
274    }
275
276    /// SUMMARY:
277    /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
278    #[inline]
279    pub fn interop_path(&self) -> &OsStr {
280        self.inner.interop_path()
281    }
282
283    /// SUMMARY:
284    /// Join a virtual path segment (virtual semantics) and re‑validate within the same restriction.
285    ///
286    /// DETAILS:
287    /// Applies virtual path clamping: absolute paths are interpreted relative to the virtual root,
288    /// and traversal attempts are clamped to prevent escaping the boundary. This method maintains
289    /// the security guarantee that all `VirtualPath` instances stay within their virtual root.
290    ///
291    /// PARAMETERS:
292    /// - `path` (`impl AsRef<Path>`): Path segment to join. Absolute paths are clamped to virtual root.
293    ///
294    /// RETURNS:
295    /// - `Result<VirtualPath<Marker>>`: New virtual path within the same restriction.
296    ///
297    /// EXAMPLE:
298    /// ```rust
299    /// # use strict_path::VirtualRoot;
300    /// # let td = tempfile::tempdir().unwrap();
301    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
302    /// let base = vroot.virtual_join("data")?;
303    ///
304    /// // Absolute paths are clamped to virtual root
305    /// let abs = base.virtual_join("/etc/config")?;
306    /// assert_eq!(abs.virtualpath_display().to_string(), "/etc/config");
307    /// # Ok::<(), Box<dyn std::error::Error>>(())
308    /// ```
309    #[inline]
310    pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
311        // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
312        let candidate = self.virtual_path.join(path.as_ref());
313        let anchored = crate::validator::path_history::PathHistory::new(candidate)
314            .canonicalize_anchored(self.inner.boundary())?;
315        let boundary_path = clamp(self.inner.boundary(), anchored)?;
316        Ok(VirtualPath::new(boundary_path))
317    }
318
319    // No local clamping helpers; virtual flows should route through
320    // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
321
322    /// SUMMARY:
323    /// Return the parent virtual path, or `None` at the virtual root.
324    pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
325        match self.virtual_path.parent() {
326            Some(parent_virtual_path) => {
327                let anchored = crate::validator::path_history::PathHistory::new(
328                    parent_virtual_path.to_path_buf(),
329                )
330                .canonicalize_anchored(self.inner.boundary())?;
331                let validated_path = clamp(self.inner.boundary(), anchored)?;
332                Ok(Some(VirtualPath::new(validated_path)))
333            }
334            None => Ok(None),
335        }
336    }
337
338    /// SUMMARY:
339    /// Return a new virtual path with file name changed, preserving clamping.
340    #[inline]
341    pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
342        let candidate = self.virtual_path.with_file_name(file_name);
343        let anchored = crate::validator::path_history::PathHistory::new(candidate)
344            .canonicalize_anchored(self.inner.boundary())?;
345        let validated_path = clamp(self.inner.boundary(), anchored)?;
346        Ok(VirtualPath::new(validated_path))
347    }
348
349    /// SUMMARY:
350    /// Return a new virtual path with the extension changed, preserving clamping.
351    pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
352        if self.virtual_path.file_name().is_none() {
353            return Err(StrictPathError::path_escapes_boundary(
354                self.virtual_path.clone(),
355                self.inner.boundary().path().to_path_buf(),
356            ));
357        }
358
359        let candidate = self.virtual_path.with_extension(extension);
360        let anchored = crate::validator::path_history::PathHistory::new(candidate)
361            .canonicalize_anchored(self.inner.boundary())?;
362        let validated_path = clamp(self.inner.boundary(), anchored)?;
363        Ok(VirtualPath::new(validated_path))
364    }
365
366    /// SUMMARY:
367    /// Return the file name component of the virtual path, if any.
368    #[inline]
369    pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
370        self.virtual_path.file_name()
371    }
372
373    /// SUMMARY:
374    /// Return the file stem of the virtual path, if any.
375    #[inline]
376    pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
377        self.virtual_path.file_stem()
378    }
379
380    /// SUMMARY:
381    /// Return the extension of the virtual path, if any.
382    #[inline]
383    pub fn virtualpath_extension(&self) -> Option<&OsStr> {
384        self.virtual_path.extension()
385    }
386
387    /// SUMMARY:
388    /// Return `true` if the virtual path starts with the given prefix (virtual semantics).
389    #[inline]
390    pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
391        self.virtual_path.starts_with(p)
392    }
393
394    /// SUMMARY:
395    /// Return `true` if the virtual path ends with the given suffix (virtual semantics).
396    #[inline]
397    pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
398        self.virtual_path.ends_with(p)
399    }
400
401    /// SUMMARY:
402    /// Return a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt").
403    #[inline]
404    pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
405        VirtualPathDisplay(self)
406    }
407
408    /// SUMMARY:
409    /// Return `true` if the underlying system path exists.
410    #[inline]
411    pub fn exists(&self) -> bool {
412        self.inner.exists()
413    }
414
415    /// SUMMARY:
416    /// Return `true` if the underlying system path is a file.
417    #[inline]
418    pub fn is_file(&self) -> bool {
419        self.inner.is_file()
420    }
421
422    /// SUMMARY:
423    /// Return `true` if the underlying system path is a directory.
424    #[inline]
425    pub fn is_dir(&self) -> bool {
426        self.inner.is_dir()
427    }
428
429    /// SUMMARY:
430    /// Return metadata for the underlying system path.
431    #[inline]
432    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
433        self.inner.metadata()
434    }
435
436    /// SUMMARY:
437    /// Read the file contents as `String` from the underlying system path.
438    #[inline]
439    pub fn read_to_string(&self) -> std::io::Result<String> {
440        self.inner.read_to_string()
441    }
442
443    /// SUMMARY:
444    /// Read raw bytes from the underlying system path.
445    #[inline]
446    pub fn read(&self) -> std::io::Result<Vec<u8>> {
447        self.inner.read()
448    }
449
450    /// SUMMARY:
451    /// Read directory entries (discovery). Re‑join names with `virtual_join(...)` to preserve clamping.
452    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
453        self.inner.read_dir()
454    }
455
456    /// SUMMARY:
457    /// Write bytes to the underlying system path. Accepts `&str`, `String`, `&[u8]`, `Vec<u8]`, etc.
458    #[inline]
459    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
460        self.inner.write(contents)
461    }
462
463    /// SUMMARY:
464    /// Create or truncate the file at this virtual path and return a writable handle.
465    ///
466    /// PARAMETERS:
467    /// - _none_
468    ///
469    /// RETURNS:
470    /// - `std::fs::File`: Writable handle scoped to the same virtual root restriction.
471    ///
472    /// ERRORS:
473    /// - `std::io::Error`: Propagates operating-system errors when the parent directory is missing or file creation fails.
474    ///
475    /// EXAMPLE:
476    /// ```rust
477    /// # use strict_path::VirtualRoot;
478    /// # use std::io::Write;
479    /// # let root = std::env::temp_dir().join("strict-path-virtual-create-file");
480    /// # std::fs::create_dir_all(&root)?;
481    /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
482    /// let report = vroot.virtual_join("reports/summary.txt")?;
483    /// report.create_parent_dir_all()?;
484    /// let mut file = report.create_file()?;
485    /// file.write_all(b"summary")?;
486    /// # std::fs::remove_dir_all(&root)?;
487    /// # Ok::<_, Box<dyn std::error::Error>>(())
488    /// ```
489    #[inline]
490    pub fn create_file(&self) -> std::io::Result<std::fs::File> {
491        self.inner.create_file()
492    }
493
494    /// SUMMARY:
495    /// Open the file at this virtual path in read-only mode.
496    ///
497    /// PARAMETERS:
498    /// - _none_
499    ///
500    /// RETURNS:
501    /// - `std::fs::File`: Read-only handle scoped to the same virtual root restriction.
502    ///
503    /// ERRORS:
504    /// - `std::io::Error`: Propagates operating-system errors when the file is missing or inaccessible.
505    ///
506    /// EXAMPLE:
507    /// ```rust
508    /// # use strict_path::VirtualRoot;
509    /// # use std::io::{Read, Write};
510    /// # let root = std::env::temp_dir().join("strict-path-virtual-open-file");
511    /// # std::fs::create_dir_all(&root)?;
512    /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
513    /// let report = vroot.virtual_join("reports/summary.txt")?;
514    /// report.create_parent_dir_all()?;
515    /// report.write("summary")?;
516    /// let mut file = report.open_file()?;
517    /// let mut contents = String::new();
518    /// file.read_to_string(&mut contents)?;
519    /// assert_eq!(contents, "summary");
520    /// # std::fs::remove_dir_all(&root)?;
521    /// # Ok::<_, Box<dyn std::error::Error>>(())
522    /// ```
523    #[inline]
524    pub fn open_file(&self) -> std::io::Result<std::fs::File> {
525        self.inner.open_file()
526    }
527
528    /// SUMMARY:
529    /// Create all directories in the underlying system path if missing.
530    #[inline]
531    pub fn create_dir_all(&self) -> std::io::Result<()> {
532        self.inner.create_dir_all()
533    }
534
535    /// SUMMARY:
536    /// Create the directory at this virtual location (non‑recursive). Fails if parent missing.
537    #[inline]
538    pub fn create_dir(&self) -> std::io::Result<()> {
539        self.inner.create_dir()
540    }
541
542    /// SUMMARY:
543    /// Create only the immediate parent of this virtual path (non‑recursive). `Ok(())` at virtual root.
544    #[inline]
545    pub fn create_parent_dir(&self) -> std::io::Result<()> {
546        match self.virtualpath_parent() {
547            Ok(Some(parent)) => parent.create_dir(),
548            Ok(None) => Ok(()),
549            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
550            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
551        }
552    }
553
554    /// SUMMARY:
555    /// Recursively create all missing directories up to the immediate parent. `Ok(())` at virtual root.
556    #[inline]
557    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
558        match self.virtualpath_parent() {
559            Ok(Some(parent)) => parent.create_dir_all(),
560            Ok(None) => Ok(()),
561            Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
562            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
563        }
564    }
565
566    /// SUMMARY:
567    /// Remove the file at the underlying system path.
568    #[inline]
569    pub fn remove_file(&self) -> std::io::Result<()> {
570        self.inner.remove_file()
571    }
572
573    /// SUMMARY:
574    /// Remove the directory at the underlying system path.
575    #[inline]
576    pub fn remove_dir(&self) -> std::io::Result<()> {
577        self.inner.remove_dir()
578    }
579
580    /// SUMMARY:
581    /// Recursively remove the directory and its contents at the underlying system path.
582    #[inline]
583    pub fn remove_dir_all(&self) -> std::io::Result<()> {
584        self.inner.remove_dir_all()
585    }
586
587    /// SUMMARY:
588    /// Create a symlink at `link_path` pointing to this virtual path (same virtual root required).
589    ///
590    /// DETAILS:
591    /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
592    /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/config"`
593    /// passed to `virtual_join()` are automatically clamped to `vroot/etc/config`, ensuring symlinks
594    /// cannot escape the virtual root boundary.
595    ///
596    /// EXAMPLE:
597    /// ```rust
598    /// # use strict_path::VirtualRoot;
599    /// # let td = tempfile::tempdir().unwrap();
600    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
601    ///
602    /// // Create target file
603    /// let target = vroot.virtual_join("/etc/config/app.conf")?;
604    /// target.create_parent_dir_all()?;
605    /// target.write(b"config data")?;
606    ///
607    /// // Ensure link parent directory exists (Windows requires this for symlink creation)
608    /// let link = vroot.virtual_join("/links/config.link")?;
609    /// link.create_parent_dir_all()?;
610    ///
611    /// // Create symlink - may fail on Windows without Developer Mode/admin privileges
612    /// if let Err(e) = target.virtual_symlink("/links/config.link") {
613    ///     // Skip test if we don't have symlink privileges (Windows ERROR_PRIVILEGE_NOT_HELD = 1314)
614    ///     #[cfg(windows)]
615    ///     if e.raw_os_error() == Some(1314) { return Ok(()); }
616    ///     return Err(e.into());
617    /// }
618    ///
619    /// assert_eq!(link.read_to_string()?, "config data");
620    /// # Ok::<(), Box<dyn std::error::Error>>(())
621    /// ```
622    pub fn virtual_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
623        let link_ref = link_path.as_ref();
624        let validated_link = if link_ref.is_absolute() {
625            match self.virtual_join(link_ref) {
626                Ok(p) => p,
627                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
628            }
629        } else {
630            // Resolve as sibling
631            let parent = match self.virtualpath_parent() {
632                Ok(Some(p)) => p,
633                Ok(None) => match self
634                    .inner
635                    .boundary()
636                    .clone()
637                    .virtualize()
638                    .into_virtualpath()
639                {
640                    Ok(root) => root,
641                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
642                },
643                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
644            };
645            match parent.virtual_join(link_ref) {
646                Ok(p) => p,
647                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
648            }
649        };
650
651        self.inner.strict_symlink(validated_link.inner.path())
652    }
653
654    /// SUMMARY:
655    /// Create a hard link at `link_path` pointing to this virtual path (same virtual root required).
656    ///
657    /// DETAILS:
658    /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
659    /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/data"`
660    /// passed to `virtual_join()` are automatically clamped to `vroot/etc/data`, ensuring hard links
661    /// cannot escape the virtual root boundary.
662    ///
663    /// EXAMPLE:
664    /// ```rust
665    /// # use strict_path::VirtualRoot;
666    /// # let td = tempfile::tempdir().unwrap();
667    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
668    ///
669    /// // Create target file
670    /// let target = vroot.virtual_join("/shared/data.dat")?;
671    /// target.create_parent_dir_all()?;
672    /// target.write(b"shared data")?;
673    ///
674    /// // Ensure link parent directory exists (Windows requires this for hard link creation)
675    /// let link = vroot.virtual_join("/backup/data.dat")?;
676    /// link.create_parent_dir_all()?;
677    ///
678    /// // Create hard link
679    /// target.virtual_hard_link("/backup/data.dat")?;
680    ///
681    /// // Read through link path, verify through target (hard link behavior)
682    /// link.write(b"modified")?;
683    /// assert_eq!(target.read_to_string()?, "modified");
684    /// # Ok::<(), Box<dyn std::error::Error>>(())
685    /// ```
686    pub fn virtual_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
687        let link_ref = link_path.as_ref();
688        let validated_link = if link_ref.is_absolute() {
689            match self.virtual_join(link_ref) {
690                Ok(p) => p,
691                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
692            }
693        } else {
694            // Resolve as sibling
695            let parent = match self.virtualpath_parent() {
696                Ok(Some(p)) => p,
697                Ok(None) => match self
698                    .inner
699                    .boundary()
700                    .clone()
701                    .virtualize()
702                    .into_virtualpath()
703                {
704                    Ok(root) => root,
705                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
706                },
707                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
708            };
709            match parent.virtual_join(link_ref) {
710                Ok(p) => p,
711                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
712            }
713        };
714
715        self.inner.strict_hard_link(validated_link.inner.path())
716    }
717
718    /// SUMMARY:
719    /// Create a Windows NTFS directory junction at `link_path` pointing to this virtual path.
720    ///
721    /// DETAILS:
722    /// - Windows-only and behind the `junctions` feature.
723    /// - Directory-only semantics; both paths must share the same virtual root.
724    #[cfg(all(windows, feature = "junctions"))]
725    pub fn virtual_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
726        // Mirror virtual semantics used by symlink/hard-link helpers:
727        // - Absolute paths are interpreted in the VIRTUAL namespace and clamped to this root
728        // - Relative paths are resolved as siblings (or from the virtual root when at root)
729        let link_ref = link_path.as_ref();
730        let validated_link = if link_ref.is_absolute() {
731            match self.virtual_join(link_ref) {
732                Ok(p) => p,
733                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
734            }
735        } else {
736            let parent = match self.virtualpath_parent() {
737                Ok(Some(p)) => p,
738                Ok(None) => match self
739                    .inner
740                    .boundary()
741                    .clone()
742                    .virtualize()
743                    .into_virtualpath()
744                {
745                    Ok(root) => root,
746                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
747                },
748                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
749            };
750            match parent.virtual_join(link_ref) {
751                Ok(p) => p,
752                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
753            }
754        };
755
756        // Delegate to strict helper after validating link location in virtual space
757        self.inner.strict_junction(validated_link.inner.path())
758    }
759
760    /// SUMMARY:
761    /// Rename/move within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
762    ///
763    /// DETAILS:
764    /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
765    /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
766    /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
767    /// Parent directories are not created automatically.
768    ///
769    /// PARAMETERS:
770    /// - `dest` (`impl AsRef<Path>`): Destination path. Absolute paths like `"/archive/file.txt"`
771    ///   are clamped to `vroot/archive/file.txt`.
772    ///
773    /// EXAMPLE:
774    /// ```rust
775    /// # use strict_path::VirtualRoot;
776    /// # let td = tempfile::tempdir().unwrap();
777    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
778    ///
779    /// let source = vroot.virtual_join("temp/file.txt")?;
780    /// source.create_parent_dir_all()?;
781    /// source.write(b"content")?;
782    ///
783    /// // Absolute destination path is clamped to virtual root
784    /// let dest_dir = vroot.virtual_join("/archive")?;
785    /// dest_dir.create_dir_all()?;
786    /// source.virtual_rename("/archive/file.txt")?;
787    ///
788    /// let renamed = vroot.virtual_join("/archive/file.txt")?;
789    /// assert_eq!(renamed.read_to_string()?, "content");
790    /// # Ok::<(), Box<dyn std::error::Error>>(())
791    /// ```
792    pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
793        let dest_ref = dest.as_ref();
794        let dest_v = if dest_ref.is_absolute() {
795            match self.virtual_join(dest_ref) {
796                Ok(p) => p,
797                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
798            }
799        } else {
800            // Resolve as sibling under the current virtual parent (or root if at "/")
801            let parent = match self.virtualpath_parent() {
802                Ok(Some(p)) => p,
803                Ok(None) => match self
804                    .inner
805                    .boundary()
806                    .clone()
807                    .virtualize()
808                    .into_virtualpath()
809                {
810                    Ok(root) => root,
811                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
812                },
813                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
814            };
815            match parent.virtual_join(dest_ref) {
816                Ok(p) => p,
817                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
818            }
819        };
820
821        // Perform the actual rename via StrictPath
822        self.inner.strict_rename(dest_v.inner.path())
823    }
824
825    /// SUMMARY:
826    /// Copy within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
827    ///
828    /// DETAILS:
829    /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
830    /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
831    /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
832    /// Parent directories are not created automatically. Returns the number of bytes copied.
833    ///
834    /// PARAMETERS:
835    /// - `dest` (`impl AsRef<Path>`): Destination path. Absolute paths like `"/backup/file.txt"`
836    ///   are clamped to `vroot/backup/file.txt`.
837    ///
838    /// RETURNS:
839    /// - `u64`: Number of bytes copied.
840    ///
841    /// EXAMPLE:
842    /// ```rust
843    /// # use strict_path::VirtualRoot;
844    /// # let td = tempfile::tempdir().unwrap();
845    /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
846    ///
847    /// let source = vroot.virtual_join("data/source.txt")?;
848    /// source.create_parent_dir_all()?;
849    /// source.write(b"data to copy")?;
850    ///
851    /// // Absolute destination path is clamped to virtual root
852    /// let dest_dir = vroot.virtual_join("/backup")?;
853    /// dest_dir.create_dir_all()?;
854    /// let bytes = source.virtual_copy("/backup/copy.txt")?;
855    ///
856    /// let copied = vroot.virtual_join("/backup/copy.txt")?;
857    /// assert_eq!(copied.read_to_string()?, "data to copy");
858    /// assert_eq!(bytes, 12);
859    /// # Ok::<(), Box<dyn std::error::Error>>(())
860    /// ```
861    pub fn virtual_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
862        let dest_ref = dest.as_ref();
863        let dest_v = if dest_ref.is_absolute() {
864            match self.virtual_join(dest_ref) {
865                Ok(p) => p,
866                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
867            }
868        } else {
869            // Resolve as sibling under the current virtual parent (or root if at "/")
870            let parent = match self.virtualpath_parent() {
871                Ok(Some(p)) => p,
872                Ok(None) => match self
873                    .inner
874                    .boundary()
875                    .clone()
876                    .virtualize()
877                    .into_virtualpath()
878                {
879                    Ok(root) => root,
880                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
881                },
882                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
883            };
884            match parent.virtual_join(dest_ref) {
885                Ok(p) => p,
886                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
887            }
888        };
889
890        // Perform the actual copy via StrictPath
891        std::fs::copy(self.inner.path(), dest_v.inner.path())
892    }
893}
894
895pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
896
897impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
898    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
899        // Ensure leading slash and normalize to forward slashes for display
900        let s_lossy = self.0.virtual_path.to_string_lossy();
901        let s_norm: std::borrow::Cow<'_, str> = {
902            #[cfg(windows)]
903            {
904                std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
905            }
906            #[cfg(not(windows))]
907            {
908                std::borrow::Cow::Borrowed(&s_lossy)
909            }
910        };
911        if s_norm.starts_with('/') {
912            write!(f, "{s_norm}")
913        } else {
914            write!(f, "/{s_norm}")
915        }
916    }
917}
918
919impl<Marker> fmt::Debug for VirtualPath<Marker> {
920    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
921        f.debug_struct("VirtualPath")
922            .field("system_path", &self.inner.path())
923            .field("virtual", &format!("{}", self.virtualpath_display()))
924            .field("boundary", &self.inner.boundary().path())
925            .field("marker", &std::any::type_name::<Marker>())
926            .finish()
927    }
928}
929
930impl<Marker> PartialEq for VirtualPath<Marker> {
931    #[inline]
932    fn eq(&self, other: &Self) -> bool {
933        self.inner.path() == other.inner.path()
934    }
935}
936
937impl<Marker> Eq for VirtualPath<Marker> {}
938
939impl<Marker> Hash for VirtualPath<Marker> {
940    #[inline]
941    fn hash<H: Hasher>(&self, state: &mut H) {
942        self.inner.path().hash(state);
943    }
944}
945
946impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
947    #[inline]
948    fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
949        self.inner.path() == other.path()
950    }
951}
952
953impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
954    #[inline]
955    fn eq(&self, other: &T) -> bool {
956        // Compare virtual paths - the user-facing representation
957        // If you want system path comparison, use as_unvirtual()
958        let virtual_str = format!("{}", self.virtualpath_display());
959        let other_str = other.as_ref().to_string_lossy();
960
961        // Normalize both to forward slashes and ensure leading slash
962        let normalized_virtual = virtual_str.as_str();
963
964        #[cfg(windows)]
965        let other_normalized = other_str.replace('\\', "/");
966        #[cfg(not(windows))]
967        let other_normalized = other_str.to_string();
968
969        let normalized_other = if other_normalized.starts_with('/') {
970            other_normalized
971        } else {
972            format!("/{}", other_normalized)
973        };
974
975        normalized_virtual == normalized_other
976    }
977}
978
979impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
980    #[inline]
981    fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
982        // Compare virtual paths - the user-facing representation
983        let virtual_str = format!("{}", self.virtualpath_display());
984        let other_str = other.as_ref().to_string_lossy();
985
986        // Normalize both to forward slashes and ensure leading slash
987        let normalized_virtual = virtual_str.as_str();
988
989        #[cfg(windows)]
990        let other_normalized = other_str.replace('\\', "/");
991        #[cfg(not(windows))]
992        let other_normalized = other_str.to_string();
993
994        let normalized_other = if other_normalized.starts_with('/') {
995            other_normalized
996        } else {
997            format!("/{}", other_normalized)
998        };
999
1000        Some(normalized_virtual.cmp(&normalized_other))
1001    }
1002}
1003
1004impl<Marker> PartialOrd for VirtualPath<Marker> {
1005    #[inline]
1006    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1007        Some(self.cmp(other))
1008    }
1009}
1010
1011impl<Marker> Ord for VirtualPath<Marker> {
1012    #[inline]
1013    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1014        self.inner.path().cmp(other.inner.path())
1015    }
1016}