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}