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 /// // Untrusted input from request/CLI/config/etc.
199 /// let requested_file = "docs/readme.md";
200 /// let guest_path: VirtualPath<GuestAccess> = guest_root.virtual_join(requested_file)?;
201 /// let user_path = grant_user_access("valid-token-12345", guest_path).expect("authorized");
202 /// assert_eq!(user_path.virtualpath_display().to_string(), "/docs/readme.md");
203 /// # std::fs::remove_dir_all(&root_dir)?;
204 /// # Ok::<_, Box<dyn std::error::Error>>(())
205 /// ```
206 ///
207 /// **Type Safety Guarantee:**
208 ///
209 /// The following code **fails to compile** because you cannot pass a path with one marker
210 /// type to a function expecting a different marker type. This compile-time check enforces
211 /// that permission changes are explicit and cannot be bypassed accidentally.
212 ///
213 /// ```compile_fail
214 /// # use strict_path::VirtualPath;
215 /// # struct GuestAccess;
216 /// # struct EditorAccess;
217 /// # let root_dir = std::env::temp_dir().join("virtual-change-marker-deny");
218 /// # std::fs::create_dir_all(&root_dir).unwrap();
219 /// # let guest_root: VirtualPath<GuestAccess> = VirtualPath::with_root(&root_dir).unwrap();
220 /// fn require_editor(_: VirtualPath<EditorAccess>) {}
221 /// let guest_file = guest_root.virtual_join("docs/manual.txt").unwrap();
222 /// // ❌ Compile error: expected `VirtualPath<EditorAccess>`, found `VirtualPath<GuestAccess>`
223 /// require_editor(guest_file);
224 /// ```
225 #[inline]
226 pub fn change_marker<NewMarker>(self) -> VirtualPath<NewMarker> {
227 let VirtualPath {
228 inner,
229 virtual_path,
230 } = self;
231
232 VirtualPath {
233 inner: inner.change_marker(),
234 virtual_path,
235 }
236 }
237
238 /// SUMMARY:
239 /// Consume and return the `VirtualRoot` for its boundary (no directory creation).
240 ///
241 /// RETURNS:
242 /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory.
243 ///
244 /// ERRORS:
245 /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` when the
246 /// strict path does not exist or is not a directory.
247 #[inline]
248 pub fn try_into_root(self) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
249 Ok(self.inner.try_into_boundary()?.virtualize())
250 }
251
252 /// SUMMARY:
253 /// Consume and return a `VirtualRoot`, creating the underlying directory if missing.
254 ///
255 /// RETURNS:
256 /// - `Result<VirtualRoot<Marker>>`: Virtual root anchored at the strict path's directory
257 /// (created if necessary).
258 ///
259 /// ERRORS:
260 /// - `StrictPathError::InvalidRestriction`: Propagated from `try_into_boundary` or directory
261 /// creation failures wrapped in `InvalidRestriction`.
262 #[inline]
263 pub fn try_into_root_create(
264 self,
265 ) -> Result<crate::validator::virtual_root::VirtualRoot<Marker>> {
266 let strict_path = self.inner;
267 let boundary = strict_path.try_into_boundary_create()?;
268 Ok(boundary.virtualize())
269 }
270
271 /// SUMMARY:
272 /// Borrow the underlying system‑facing `StrictPath` (no allocation).
273 #[inline]
274 pub fn as_unvirtual(&self) -> &StrictPath<Marker> {
275 &self.inner
276 }
277
278 /// SUMMARY:
279 /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
280 #[inline]
281 pub fn interop_path(&self) -> &OsStr {
282 self.inner.interop_path()
283 }
284
285 /// SUMMARY:
286 /// Join a virtual path segment (virtual semantics) and re‑validate within the same restriction.
287 ///
288 /// DETAILS:
289 /// Applies virtual path clamping: absolute paths are interpreted relative to the virtual root,
290 /// and traversal attempts are clamped to prevent escaping the boundary. This method maintains
291 /// the security guarantee that all `VirtualPath` instances stay within their virtual root.
292 ///
293 /// PARAMETERS:
294 /// - `path` (`impl AsRef<Path>`): Path segment to join. Absolute paths are clamped to virtual root.
295 ///
296 /// RETURNS:
297 /// - `Result<VirtualPath<Marker>>`: New virtual path within the same restriction.
298 ///
299 /// EXAMPLE:
300 /// ```rust
301 /// # use strict_path::VirtualRoot;
302 /// # let td = tempfile::tempdir().unwrap();
303 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
304 /// let base = vroot.virtual_join("data")?;
305 ///
306 /// // Absolute paths are clamped to virtual root
307 /// let abs = base.virtual_join("/etc/config")?;
308 /// assert_eq!(abs.virtualpath_display().to_string(), "/etc/config");
309 /// # Ok::<(), Box<dyn std::error::Error>>(())
310 /// ```
311 #[inline]
312 pub fn virtual_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
313 // Compose candidate in virtual space (do not pre-normalize lexically to preserve symlink semantics)
314 let candidate = self.virtual_path.join(path.as_ref());
315 let anchored = crate::validator::path_history::PathHistory::new(candidate)
316 .canonicalize_anchored(self.inner.boundary())?;
317 let boundary_path = clamp(self.inner.boundary(), anchored)?;
318 Ok(VirtualPath::new(boundary_path))
319 }
320
321 // No local clamping helpers; virtual flows should route through
322 // PathHistory::virtualize_to_jail + PathBoundary::strict_join to avoid drift.
323
324 /// SUMMARY:
325 /// Return the parent virtual path, or `None` at the virtual root.
326 pub fn virtualpath_parent(&self) -> Result<Option<Self>> {
327 match self.virtual_path.parent() {
328 Some(parent_virtual_path) => {
329 let anchored = crate::validator::path_history::PathHistory::new(
330 parent_virtual_path.to_path_buf(),
331 )
332 .canonicalize_anchored(self.inner.boundary())?;
333 let validated_path = clamp(self.inner.boundary(), anchored)?;
334 Ok(Some(VirtualPath::new(validated_path)))
335 }
336 None => Ok(None),
337 }
338 }
339
340 /// SUMMARY:
341 /// Return a new virtual path with file name changed, preserving clamping.
342 #[inline]
343 pub fn virtualpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
344 let candidate = self.virtual_path.with_file_name(file_name);
345 let anchored = crate::validator::path_history::PathHistory::new(candidate)
346 .canonicalize_anchored(self.inner.boundary())?;
347 let validated_path = clamp(self.inner.boundary(), anchored)?;
348 Ok(VirtualPath::new(validated_path))
349 }
350
351 /// SUMMARY:
352 /// Return a new virtual path with the extension changed, preserving clamping.
353 pub fn virtualpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
354 if self.virtual_path.file_name().is_none() {
355 return Err(StrictPathError::path_escapes_boundary(
356 self.virtual_path.clone(),
357 self.inner.boundary().path().to_path_buf(),
358 ));
359 }
360
361 let candidate = self.virtual_path.with_extension(extension);
362 let anchored = crate::validator::path_history::PathHistory::new(candidate)
363 .canonicalize_anchored(self.inner.boundary())?;
364 let validated_path = clamp(self.inner.boundary(), anchored)?;
365 Ok(VirtualPath::new(validated_path))
366 }
367
368 /// SUMMARY:
369 /// Return the file name component of the virtual path, if any.
370 #[inline]
371 pub fn virtualpath_file_name(&self) -> Option<&OsStr> {
372 self.virtual_path.file_name()
373 }
374
375 /// SUMMARY:
376 /// Return the file stem of the virtual path, if any.
377 #[inline]
378 pub fn virtualpath_file_stem(&self) -> Option<&OsStr> {
379 self.virtual_path.file_stem()
380 }
381
382 /// SUMMARY:
383 /// Return the extension of the virtual path, if any.
384 #[inline]
385 pub fn virtualpath_extension(&self) -> Option<&OsStr> {
386 self.virtual_path.extension()
387 }
388
389 /// SUMMARY:
390 /// Return `true` if the virtual path starts with the given prefix (virtual semantics).
391 #[inline]
392 pub fn virtualpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
393 self.virtual_path.starts_with(p)
394 }
395
396 /// SUMMARY:
397 /// Return `true` if the virtual path ends with the given suffix (virtual semantics).
398 #[inline]
399 pub fn virtualpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
400 self.virtual_path.ends_with(p)
401 }
402
403 /// SUMMARY:
404 /// Return a Display wrapper that shows a rooted virtual path (e.g., `"/a/b.txt").
405 #[inline]
406 pub fn virtualpath_display(&self) -> VirtualPathDisplay<'_, Marker> {
407 VirtualPathDisplay(self)
408 }
409
410 /// SUMMARY:
411 /// Return `true` if the underlying system path exists.
412 #[inline]
413 pub fn exists(&self) -> bool {
414 self.inner.exists()
415 }
416
417 /// SUMMARY:
418 /// Return `true` if the underlying system path is a file.
419 #[inline]
420 pub fn is_file(&self) -> bool {
421 self.inner.is_file()
422 }
423
424 /// SUMMARY:
425 /// Return `true` if the underlying system path is a directory.
426 #[inline]
427 pub fn is_dir(&self) -> bool {
428 self.inner.is_dir()
429 }
430
431 /// SUMMARY:
432 /// Return metadata for the underlying system path.
433 #[inline]
434 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
435 self.inner.metadata()
436 }
437
438 /// SUMMARY:
439 /// Read the file contents as `String` from the underlying system path.
440 #[inline]
441 pub fn read_to_string(&self) -> std::io::Result<String> {
442 self.inner.read_to_string()
443 }
444
445 /// SUMMARY:
446 /// Read raw bytes from the underlying system path.
447 #[inline]
448 pub fn read(&self) -> std::io::Result<Vec<u8>> {
449 self.inner.read()
450 }
451
452 /// SUMMARY:
453 /// Return metadata for the underlying system path without following symlinks.
454 #[inline]
455 pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
456 self.inner.symlink_metadata()
457 }
458
459 /// SUMMARY:
460 /// Set permissions on the file or directory at this path.
461 ///
462 /// PARAMETERS:
463 /// - `perm` (`std::fs::Permissions`): The permissions to set.
464 ///
465 /// RETURNS:
466 /// - `io::Result<()>`: Success or I/O error.
467 #[inline]
468 pub fn set_permissions(&self, perm: std::fs::Permissions) -> std::io::Result<()> {
469 self.inner.set_permissions(perm)
470 }
471
472 /// SUMMARY:
473 /// Check if the path exists, returning an error on permission issues.
474 ///
475 /// DETAILS:
476 /// Unlike `exists()` which returns `false` on permission errors, this method
477 /// distinguishes between "path does not exist" (`Ok(false)`) and "cannot check
478 /// due to permission error" (`Err(...)`).
479 ///
480 /// RETURNS:
481 /// - `Ok(true)`: Path exists
482 /// - `Ok(false)`: Path does not exist
483 /// - `Err(...)`: Permission or other I/O error prevented the check
484 #[inline]
485 pub fn try_exists(&self) -> std::io::Result<bool> {
486 self.inner.try_exists()
487 }
488
489 /// SUMMARY:
490 /// Create an empty file if it doesn't exist, or update the modification time if it does.
491 ///
492 /// DETAILS:
493 /// This is a convenience method combining file creation and mtime update.
494 /// Uses `OpenOptions` with `create(true).write(true)` which creates the file
495 /// if missing or opens it for writing if it exists, updating mtime on close.
496 ///
497 /// RETURNS:
498 /// - `io::Result<()>`: Success or I/O error.
499 pub fn touch(&self) -> std::io::Result<()> {
500 self.inner.touch()
501 }
502
503 /// SUMMARY:
504 /// Read directory entries (discovery). Re‑join names with `virtual_join(...)` to preserve clamping.
505 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
506 self.inner.read_dir()
507 }
508
509 /// SUMMARY:
510 /// Read directory entries as validated `VirtualPath` values (auto re-joins each entry).
511 ///
512 /// DETAILS:
513 /// Unlike `read_dir()` which returns raw `std::fs::DirEntry`, this method automatically
514 /// validates each directory entry through `virtual_join()`, returning an iterator of
515 /// `Result<VirtualPath<Marker>>`. This eliminates the need for manual re-validation loops
516 /// while preserving the virtual path semantics.
517 ///
518 /// PARAMETERS:
519 /// - _none_
520 ///
521 /// RETURNS:
522 /// - `io::Result<VirtualReadDir<Marker>>`: Iterator yielding validated `VirtualPath` entries.
523 ///
524 /// ERRORS:
525 /// - `std::io::Error`: If the directory cannot be read.
526 /// - Each yielded item may also be `Err` if validation fails for that entry.
527 ///
528 /// EXAMPLE:
529 /// ```rust
530 /// # use strict_path::{VirtualRoot, VirtualPath};
531 /// # let temp = tempfile::tempdir()?;
532 /// # let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
533 /// # let dir = vroot.virtual_join("uploads")?;
534 /// # dir.create_dir_all()?;
535 /// # vroot.virtual_join("uploads/file1.txt")?.write("a")?;
536 /// # vroot.virtual_join("uploads/file2.txt")?.write("b")?;
537 /// // Iterate with automatic validation
538 /// for entry in dir.virtual_read_dir()? {
539 /// let child: VirtualPath = entry?;
540 /// println!("{}", child.virtualpath_display());
541 /// }
542 /// # Ok::<_, Box<dyn std::error::Error>>(())
543 /// ```
544 pub fn virtual_read_dir(&self) -> std::io::Result<VirtualReadDir<'_, Marker>> {
545 let inner = std::fs::read_dir(self.inner.path())?;
546 Ok(VirtualReadDir {
547 inner,
548 parent: self,
549 })
550 }
551
552 /// SUMMARY:
553 /// Write bytes to the underlying system path. Accepts `&str`, `String`, `&[u8]`, `Vec<u8]`, etc.
554 #[inline]
555 pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
556 self.inner.write(contents)
557 }
558
559 /// SUMMARY:
560 /// Append bytes to the underlying system path (create if missing). Accepts `&str`, `&[u8]`, etc.
561 ///
562 /// PARAMETERS:
563 /// - `data` (`AsRef<[u8]>`): Bytes to append to the file.
564 ///
565 /// RETURNS:
566 /// - `()`: Returns nothing on success.
567 ///
568 /// ERRORS:
569 /// - `std::io::Error`: Propagates OS errors when the file cannot be opened or written.
570 ///
571 /// EXAMPLE:
572 /// ```rust
573 /// # use strict_path::VirtualRoot;
574 /// # let root = std::env::temp_dir().join("strict-path-vpath-append");
575 /// # std::fs::create_dir_all(&root)?;
576 /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
577 /// // Untrusted input from request/CLI/config/etc.
578 /// let log_file = "logs/activity.log";
579 /// let vpath = vroot.virtual_join(log_file)?;
580 /// vpath.create_parent_dir_all()?;
581 /// vpath.append("[2025-01-01] Operation A\n")?;
582 /// vpath.append("[2025-01-01] Operation B\n")?;
583 /// let contents = vpath.read_to_string()?;
584 /// assert!(contents.contains("Operation A"));
585 /// assert!(contents.contains("Operation B"));
586 /// # std::fs::remove_dir_all(&root)?;
587 /// # Ok::<_, Box<dyn std::error::Error>>(())
588 /// ```
589 #[inline]
590 pub fn append<C: AsRef<[u8]>>(&self, data: C) -> std::io::Result<()> {
591 self.inner.append(data)
592 }
593
594 /// SUMMARY:
595 /// Create or truncate the file at this virtual path and return a writable handle.
596 ///
597 /// PARAMETERS:
598 /// - _none_
599 ///
600 /// RETURNS:
601 /// - `std::fs::File`: Writable handle scoped to the same virtual root restriction.
602 ///
603 /// ERRORS:
604 /// - `std::io::Error`: Propagates operating-system errors when the parent directory is missing or file creation fails.
605 ///
606 /// EXAMPLE:
607 /// ```rust
608 /// # use strict_path::VirtualRoot;
609 /// # use std::io::Write;
610 /// # let root = std::env::temp_dir().join("strict-path-virtual-create-file");
611 /// # std::fs::create_dir_all(&root)?;
612 /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
613 /// let report = vroot.virtual_join("reports/summary.txt")?;
614 /// report.create_parent_dir_all()?;
615 /// let mut file = report.create_file()?;
616 /// file.write_all(b"summary")?;
617 /// # std::fs::remove_dir_all(&root)?;
618 /// # Ok::<_, Box<dyn std::error::Error>>(())
619 /// ```
620 #[inline]
621 pub fn create_file(&self) -> std::io::Result<std::fs::File> {
622 self.inner.create_file()
623 }
624
625 /// SUMMARY:
626 /// Open the file at this virtual path in read-only mode.
627 ///
628 /// PARAMETERS:
629 /// - _none_
630 ///
631 /// RETURNS:
632 /// - `std::fs::File`: Read-only handle scoped to the same virtual root restriction.
633 ///
634 /// ERRORS:
635 /// - `std::io::Error`: Propagates operating-system errors when the file is missing or inaccessible.
636 ///
637 /// EXAMPLE:
638 /// ```rust
639 /// # use strict_path::VirtualRoot;
640 /// # use std::io::{Read, Write};
641 /// # let root = std::env::temp_dir().join("strict-path-virtual-open-file");
642 /// # std::fs::create_dir_all(&root)?;
643 /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
644 /// let report = vroot.virtual_join("reports/summary.txt")?;
645 /// report.create_parent_dir_all()?;
646 /// report.write("summary")?;
647 /// let mut file = report.open_file()?;
648 /// let mut contents = String::new();
649 /// file.read_to_string(&mut contents)?;
650 /// assert_eq!(contents, "summary");
651 /// # std::fs::remove_dir_all(&root)?;
652 /// # Ok::<_, Box<dyn std::error::Error>>(())
653 /// ```
654 #[inline]
655 pub fn open_file(&self) -> std::io::Result<std::fs::File> {
656 self.inner.open_file()
657 }
658
659 /// SUMMARY:
660 /// Return an options builder for advanced file opening (read+write, append, exclusive create, etc.).
661 ///
662 /// PARAMETERS:
663 /// - _none_
664 ///
665 /// RETURNS:
666 /// - `StrictOpenOptions<Marker>`: Builder to configure file opening options.
667 ///
668 /// EXAMPLE:
669 /// ```rust
670 /// # use strict_path::VirtualRoot;
671 /// # use std::io::{Read, Write, Seek, SeekFrom};
672 /// # let root = std::env::temp_dir().join("vpath-open-with-example");
673 /// # std::fs::create_dir_all(&root)?;
674 /// # let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
675 /// // Untrusted input from request/CLI/config/etc.
676 /// let data_file = "cache/state.bin";
677 /// let cache_path = vroot.virtual_join(data_file)?;
678 /// cache_path.create_parent_dir_all()?;
679 ///
680 /// // Open with read+write access, create if missing
681 /// let mut file = cache_path.open_with()
682 /// .read(true)
683 /// .write(true)
684 /// .create(true)
685 /// .open()?;
686 /// file.write_all(b"state")?;
687 /// file.seek(SeekFrom::Start(0))?;
688 /// let mut buf = [0u8; 5];
689 /// file.read_exact(&mut buf)?;
690 /// assert_eq!(&buf, b"state");
691 /// # std::fs::remove_dir_all(&root)?;
692 /// # Ok::<_, Box<dyn std::error::Error>>(())
693 /// ```
694 #[inline]
695 pub fn open_with(&self) -> crate::path::strict_path::StrictOpenOptions<'_, Marker> {
696 self.inner.open_with()
697 }
698
699 /// SUMMARY:
700 /// Create all directories in the underlying system path if missing.
701 #[inline]
702 pub fn create_dir_all(&self) -> std::io::Result<()> {
703 self.inner.create_dir_all()
704 }
705
706 /// SUMMARY:
707 /// Create the directory at this virtual location (non‑recursive). Fails if parent missing.
708 #[inline]
709 pub fn create_dir(&self) -> std::io::Result<()> {
710 self.inner.create_dir()
711 }
712
713 /// SUMMARY:
714 /// Create only the immediate parent of this virtual path (non‑recursive). `Ok(())` at virtual root.
715 #[inline]
716 pub fn create_parent_dir(&self) -> std::io::Result<()> {
717 match self.virtualpath_parent() {
718 Ok(Some(parent)) => parent.create_dir(),
719 Ok(None) => Ok(()),
720 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
721 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
722 }
723 }
724
725 /// SUMMARY:
726 /// Recursively create all missing directories up to the immediate parent. `Ok(())` at virtual root.
727 #[inline]
728 pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
729 match self.virtualpath_parent() {
730 Ok(Some(parent)) => parent.create_dir_all(),
731 Ok(None) => Ok(()),
732 Err(crate::StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
733 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
734 }
735 }
736
737 /// SUMMARY:
738 /// Remove the file at the underlying system path.
739 #[inline]
740 pub fn remove_file(&self) -> std::io::Result<()> {
741 self.inner.remove_file()
742 }
743
744 /// SUMMARY:
745 /// Remove the directory at the underlying system path.
746 #[inline]
747 pub fn remove_dir(&self) -> std::io::Result<()> {
748 self.inner.remove_dir()
749 }
750
751 /// SUMMARY:
752 /// Recursively remove the directory and its contents at the underlying system path.
753 #[inline]
754 pub fn remove_dir_all(&self) -> std::io::Result<()> {
755 self.inner.remove_dir_all()
756 }
757
758 /// SUMMARY:
759 /// Create a symlink at `link_path` pointing to this virtual path (same virtual root required).
760 ///
761 /// DETAILS:
762 /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
763 /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/config"`
764 /// passed to `virtual_join()` are automatically clamped to `vroot/etc/config`, ensuring symlinks
765 /// cannot escape the virtual root boundary.
766 ///
767 /// EXAMPLE:
768 /// ```rust
769 /// # use strict_path::VirtualRoot;
770 /// # let td = tempfile::tempdir().unwrap();
771 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
772 ///
773 /// // Create target file
774 /// let target = vroot.virtual_join("/etc/config/app.conf")?;
775 /// target.create_parent_dir_all()?;
776 /// target.write(b"config data")?;
777 ///
778 /// // Ensure link parent directory exists (Windows requires this for symlink creation)
779 /// let link = vroot.virtual_join("/links/config.link")?;
780 /// link.create_parent_dir_all()?;
781 ///
782 /// // Create symlink - may fail on Windows without Developer Mode/admin privileges
783 /// if let Err(e) = target.virtual_symlink("/links/config.link") {
784 /// // Skip test if we don't have symlink privileges (Windows ERROR_PRIVILEGE_NOT_HELD = 1314)
785 /// #[cfg(windows)]
786 /// if e.raw_os_error() == Some(1314) { return Ok(()); }
787 /// return Err(e.into());
788 /// }
789 ///
790 /// assert_eq!(link.read_to_string()?, "config data");
791 /// # Ok::<(), Box<dyn std::error::Error>>(())
792 /// ```
793 pub fn virtual_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
794 let link_ref = link_path.as_ref();
795 let validated_link = if link_ref.is_absolute() {
796 match self.virtual_join(link_ref) {
797 Ok(p) => p,
798 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
799 }
800 } else {
801 // Resolve as sibling
802 let parent = match self.virtualpath_parent() {
803 Ok(Some(p)) => p,
804 Ok(None) => match self
805 .inner
806 .boundary()
807 .clone()
808 .virtualize()
809 .into_virtualpath()
810 {
811 Ok(root) => root,
812 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
813 },
814 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
815 };
816 match parent.virtual_join(link_ref) {
817 Ok(p) => p,
818 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
819 }
820 };
821
822 self.inner.strict_symlink(validated_link.inner.path())
823 }
824
825 /// SUMMARY:
826 /// Read the target of a symbolic link and return it as a validated `VirtualPath`.
827 ///
828 /// DESIGN NOTE:
829 /// This method has limited practical use because `virtual_join` resolves symlinks
830 /// during canonicalization. A `VirtualPath` obtained via `virtual_join("/link")` already
831 /// points to the symlink's target, not the symlink itself.
832 ///
833 /// To read a symlink target before validation, use `std::fs::read_link` on the raw
834 /// path, then validate the target with `virtual_join`:
835 ///
836 /// EXAMPLE:
837 /// ```rust
838 /// use strict_path::VirtualRoot;
839 ///
840 /// let temp = tempfile::tempdir()?;
841 /// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
842 ///
843 /// // Create a target file
844 /// let target = vroot.virtual_join("/data/target.txt")?;
845 /// target.create_parent_dir_all()?;
846 /// target.write("secret")?;
847 ///
848 /// // Create symlink (may fail on Windows without Developer Mode)
849 /// if target.virtual_symlink("/data/link.txt").is_ok() {
850 /// // virtual_join resolves symlinks: link.txt -> target.txt
851 /// let resolved = vroot.virtual_join("/data/link.txt")?;
852 /// assert_eq!(resolved.virtualpath_display().to_string(), "/data/target.txt");
853 /// // The resolved path reads the target file's content
854 /// assert_eq!(resolved.read_to_string()?, "secret");
855 /// }
856 /// # Ok::<(), Box<dyn std::error::Error>>(())
857 /// ```
858 pub fn virtual_read_link(&self) -> std::io::Result<Self> {
859 // Read the raw symlink target
860 let raw_target = std::fs::read_link(self.inner.path())?;
861
862 // If the target is relative, resolve it relative to the symlink's parent
863 let resolved_target = if raw_target.is_relative() {
864 match self.inner.path().parent() {
865 Some(parent) => parent.join(&raw_target),
866 None => raw_target,
867 }
868 } else {
869 raw_target
870 };
871
872 // Validate through virtual_join which clamps escapes
873 // We need to compute the relative path from the virtual root
874 let vroot = self.inner.boundary().clone().virtualize();
875 vroot
876 .virtual_join(resolved_target)
877 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
878 }
879
880 /// SUMMARY:
881 /// Create a hard link at `link_path` pointing to this virtual path (same virtual root required).
882 ///
883 /// DETAILS:
884 /// Both `self` (target) and `link_path` must be `VirtualPath` instances created via `virtual_join()`,
885 /// which ensures all paths are clamped to the virtual root. Absolute paths like `"/etc/data"`
886 /// passed to `virtual_join()` are automatically clamped to `vroot/etc/data`, ensuring hard links
887 /// cannot escape the virtual root boundary.
888 ///
889 /// EXAMPLE:
890 /// ```rust
891 /// # use strict_path::VirtualRoot;
892 /// # let td = tempfile::tempdir().unwrap();
893 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
894 ///
895 /// // Create target file
896 /// let target = vroot.virtual_join("/shared/data.dat")?;
897 /// target.create_parent_dir_all()?;
898 /// target.write(b"shared data")?;
899 ///
900 /// // Ensure link parent directory exists (Windows requires this for hard link creation)
901 /// let link = vroot.virtual_join("/backup/data.dat")?;
902 /// link.create_parent_dir_all()?;
903 ///
904 /// // Create hard link
905 /// target.virtual_hard_link("/backup/data.dat")?;
906 ///
907 /// // Read through link path, verify through target (hard link behavior)
908 /// link.write(b"modified")?;
909 /// assert_eq!(target.read_to_string()?, "modified");
910 /// # Ok::<(), Box<dyn std::error::Error>>(())
911 /// ```
912 pub fn virtual_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
913 let link_ref = link_path.as_ref();
914 let validated_link = if link_ref.is_absolute() {
915 match self.virtual_join(link_ref) {
916 Ok(p) => p,
917 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
918 }
919 } else {
920 // Resolve as sibling
921 let parent = match self.virtualpath_parent() {
922 Ok(Some(p)) => p,
923 Ok(None) => match self
924 .inner
925 .boundary()
926 .clone()
927 .virtualize()
928 .into_virtualpath()
929 {
930 Ok(root) => root,
931 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
932 },
933 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
934 };
935 match parent.virtual_join(link_ref) {
936 Ok(p) => p,
937 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
938 }
939 };
940
941 self.inner.strict_hard_link(validated_link.inner.path())
942 }
943
944 /// SUMMARY:
945 /// Create a Windows NTFS directory junction at `link_path` pointing to this virtual path.
946 ///
947 /// DETAILS:
948 /// - Windows-only and behind the `junctions` feature.
949 /// - Directory-only semantics; both paths must share the same virtual root.
950 #[cfg(all(windows, feature = "junctions"))]
951 pub fn virtual_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
952 // Mirror virtual semantics used by symlink/hard-link helpers:
953 // - Absolute paths are interpreted in the VIRTUAL namespace and clamped to this root
954 // - Relative paths are resolved as siblings (or from the virtual root when at root)
955 let link_ref = link_path.as_ref();
956 let validated_link = if link_ref.is_absolute() {
957 match self.virtual_join(link_ref) {
958 Ok(p) => p,
959 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
960 }
961 } else {
962 let parent = match self.virtualpath_parent() {
963 Ok(Some(p)) => p,
964 Ok(None) => match self
965 .inner
966 .boundary()
967 .clone()
968 .virtualize()
969 .into_virtualpath()
970 {
971 Ok(root) => root,
972 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
973 },
974 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
975 };
976 match parent.virtual_join(link_ref) {
977 Ok(p) => p,
978 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
979 }
980 };
981
982 // Delegate to strict helper after validating link location in virtual space
983 self.inner.strict_junction(validated_link.inner.path())
984 }
985
986 /// SUMMARY:
987 /// Rename/move within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
988 ///
989 /// DETAILS:
990 /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
991 /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
992 /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
993 /// Parent directories are not created automatically.
994 ///
995 /// PARAMETERS:
996 /// - `dest` (`impl AsRef<Path>`): Destination path. Absolute paths like `"/archive/file.txt"`
997 /// are clamped to `vroot/archive/file.txt`.
998 ///
999 /// EXAMPLE:
1000 /// ```rust
1001 /// # use strict_path::VirtualRoot;
1002 /// # let td = tempfile::tempdir().unwrap();
1003 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
1004 ///
1005 /// let source = vroot.virtual_join("temp/file.txt")?;
1006 /// source.create_parent_dir_all()?;
1007 /// source.write(b"content")?;
1008 ///
1009 /// // Absolute destination path is clamped to virtual root
1010 /// let dest_dir = vroot.virtual_join("/archive")?;
1011 /// dest_dir.create_dir_all()?;
1012 /// source.virtual_rename("/archive/file.txt")?;
1013 ///
1014 /// let renamed = vroot.virtual_join("/archive/file.txt")?;
1015 /// assert_eq!(renamed.read_to_string()?, "content");
1016 /// # Ok::<(), Box<dyn std::error::Error>>(())
1017 /// ```
1018 pub fn virtual_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
1019 let dest_ref = dest.as_ref();
1020 let dest_v = if dest_ref.is_absolute() {
1021 match self.virtual_join(dest_ref) {
1022 Ok(p) => p,
1023 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1024 }
1025 } else {
1026 // Resolve as sibling under the current virtual parent (or root if at "/")
1027 let parent = match self.virtualpath_parent() {
1028 Ok(Some(p)) => p,
1029 Ok(None) => match self
1030 .inner
1031 .boundary()
1032 .clone()
1033 .virtualize()
1034 .into_virtualpath()
1035 {
1036 Ok(root) => root,
1037 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1038 },
1039 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1040 };
1041 match parent.virtual_join(dest_ref) {
1042 Ok(p) => p,
1043 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1044 }
1045 };
1046
1047 // Perform the actual rename via StrictPath
1048 self.inner.strict_rename(dest_v.inner.path())
1049 }
1050
1051 /// SUMMARY:
1052 /// Copy within the same virtual root. Relative destinations are siblings; absolute are clamped to root.
1053 ///
1054 /// DETAILS:
1055 /// Accepts `impl AsRef<Path>` for the destination. Absolute paths (starting with `"/"`) are
1056 /// automatically clamped to the virtual root via internal `virtual_join()` call, ensuring the
1057 /// destination cannot escape the virtual boundary. Relative paths are resolved as siblings.
1058 /// Parent directories are not created automatically. Returns the number of bytes copied.
1059 ///
1060 /// PARAMETERS:
1061 /// - `dest` (`impl AsRef<Path>`): Destination path. Absolute paths like `"/backup/file.txt"`
1062 /// are clamped to `vroot/backup/file.txt`.
1063 ///
1064 /// RETURNS:
1065 /// - `u64`: Number of bytes copied.
1066 ///
1067 /// EXAMPLE:
1068 /// ```rust
1069 /// # use strict_path::VirtualRoot;
1070 /// # let td = tempfile::tempdir().unwrap();
1071 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
1072 ///
1073 /// let source = vroot.virtual_join("data/source.txt")?;
1074 /// source.create_parent_dir_all()?;
1075 /// source.write(b"data to copy")?;
1076 ///
1077 /// // Absolute destination path is clamped to virtual root
1078 /// let dest_dir = vroot.virtual_join("/backup")?;
1079 /// dest_dir.create_dir_all()?;
1080 /// let bytes = source.virtual_copy("/backup/copy.txt")?;
1081 ///
1082 /// let copied = vroot.virtual_join("/backup/copy.txt")?;
1083 /// assert_eq!(copied.read_to_string()?, "data to copy");
1084 /// assert_eq!(bytes, 12);
1085 /// # Ok::<(), Box<dyn std::error::Error>>(())
1086 /// ```
1087 pub fn virtual_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
1088 let dest_ref = dest.as_ref();
1089 let dest_v = if dest_ref.is_absolute() {
1090 match self.virtual_join(dest_ref) {
1091 Ok(p) => p,
1092 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1093 }
1094 } else {
1095 // Resolve as sibling under the current virtual parent (or root if at "/")
1096 let parent = match self.virtualpath_parent() {
1097 Ok(Some(p)) => p,
1098 Ok(None) => match self
1099 .inner
1100 .boundary()
1101 .clone()
1102 .virtualize()
1103 .into_virtualpath()
1104 {
1105 Ok(root) => root,
1106 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1107 },
1108 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1109 };
1110 match parent.virtual_join(dest_ref) {
1111 Ok(p) => p,
1112 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
1113 }
1114 };
1115
1116 // Perform the actual copy via StrictPath
1117 std::fs::copy(self.inner.path(), dest_v.inner.path())
1118 }
1119}
1120
1121pub struct VirtualPathDisplay<'a, Marker>(&'a VirtualPath<Marker>);
1122
1123impl<'a, Marker> fmt::Display for VirtualPathDisplay<'a, Marker> {
1124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1125 // Ensure leading slash and normalize to forward slashes for display
1126 let s_lossy = self.0.virtual_path.to_string_lossy();
1127 let s_norm: std::borrow::Cow<'_, str> = {
1128 #[cfg(windows)]
1129 {
1130 std::borrow::Cow::Owned(s_lossy.replace('\\', "/"))
1131 }
1132 #[cfg(not(windows))]
1133 {
1134 std::borrow::Cow::Borrowed(&s_lossy)
1135 }
1136 };
1137 if s_norm.starts_with('/') {
1138 write!(f, "{s_norm}")
1139 } else {
1140 write!(f, "/{s_norm}")
1141 }
1142 }
1143}
1144
1145impl<Marker> fmt::Debug for VirtualPath<Marker> {
1146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1147 f.debug_struct("VirtualPath")
1148 .field("system_path", &self.inner.path())
1149 .field("virtual", &format!("{}", self.virtualpath_display()))
1150 .field("boundary", &self.inner.boundary().path())
1151 .field("marker", &std::any::type_name::<Marker>())
1152 .finish()
1153 }
1154}
1155
1156impl<Marker> PartialEq for VirtualPath<Marker> {
1157 #[inline]
1158 fn eq(&self, other: &Self) -> bool {
1159 self.inner.path() == other.inner.path()
1160 }
1161}
1162
1163impl<Marker> Eq for VirtualPath<Marker> {}
1164
1165impl<Marker> Hash for VirtualPath<Marker> {
1166 #[inline]
1167 fn hash<H: Hasher>(&self, state: &mut H) {
1168 self.inner.path().hash(state);
1169 }
1170}
1171
1172impl<Marker> PartialEq<crate::path::strict_path::StrictPath<Marker>> for VirtualPath<Marker> {
1173 #[inline]
1174 fn eq(&self, other: &crate::path::strict_path::StrictPath<Marker>) -> bool {
1175 self.inner.path() == other.path()
1176 }
1177}
1178
1179impl<T: AsRef<Path>, Marker> PartialEq<T> for VirtualPath<Marker> {
1180 #[inline]
1181 fn eq(&self, other: &T) -> bool {
1182 // Compare virtual paths - the user-facing representation
1183 // If you want system path comparison, use as_unvirtual()
1184 let virtual_str = format!("{}", self.virtualpath_display());
1185 let other_str = other.as_ref().to_string_lossy();
1186
1187 // Normalize both to forward slashes and ensure leading slash
1188 let normalized_virtual = virtual_str.as_str();
1189
1190 #[cfg(windows)]
1191 let other_normalized = other_str.replace('\\', "/");
1192 #[cfg(not(windows))]
1193 let other_normalized = other_str.to_string();
1194
1195 let normalized_other = if other_normalized.starts_with('/') {
1196 other_normalized
1197 } else {
1198 format!("/{}", other_normalized)
1199 };
1200
1201 normalized_virtual == normalized_other
1202 }
1203}
1204
1205impl<T: AsRef<Path>, Marker> PartialOrd<T> for VirtualPath<Marker> {
1206 #[inline]
1207 fn partial_cmp(&self, other: &T) -> Option<std::cmp::Ordering> {
1208 // Compare virtual paths - the user-facing representation
1209 let virtual_str = format!("{}", self.virtualpath_display());
1210 let other_str = other.as_ref().to_string_lossy();
1211
1212 // Normalize both to forward slashes and ensure leading slash
1213 let normalized_virtual = virtual_str.as_str();
1214
1215 #[cfg(windows)]
1216 let other_normalized = other_str.replace('\\', "/");
1217 #[cfg(not(windows))]
1218 let other_normalized = other_str.to_string();
1219
1220 let normalized_other = if other_normalized.starts_with('/') {
1221 other_normalized
1222 } else {
1223 format!("/{}", other_normalized)
1224 };
1225
1226 Some(normalized_virtual.cmp(&normalized_other))
1227 }
1228}
1229
1230impl<Marker> PartialOrd for VirtualPath<Marker> {
1231 #[inline]
1232 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1233 Some(self.cmp(other))
1234 }
1235}
1236
1237impl<Marker> Ord for VirtualPath<Marker> {
1238 #[inline]
1239 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1240 self.inner.path().cmp(other.inner.path())
1241 }
1242}
1243
1244// ============================================================
1245// VirtualReadDir — Iterator for validated virtual directory entries
1246// ============================================================
1247
1248/// SUMMARY:
1249/// Iterator over directory entries that yields validated `VirtualPath` values.
1250///
1251/// DETAILS:
1252/// Created by `VirtualPath::virtual_read_dir()`. Each iteration automatically validates
1253/// the directory entry through `virtual_join()`, so you get `VirtualPath` values directly
1254/// instead of raw `std::fs::DirEntry` that would require manual re-validation.
1255///
1256/// EXAMPLE:
1257/// ```rust
1258/// # use strict_path::{VirtualRoot, VirtualPath};
1259/// # let temp = tempfile::tempdir()?;
1260/// # let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
1261/// # let dir = vroot.virtual_join("assets")?;
1262/// # dir.create_dir_all()?;
1263/// # vroot.virtual_join("assets/logo.png")?.write(b"PNG")?;
1264/// for entry in dir.virtual_read_dir()? {
1265/// let child: VirtualPath = entry?;
1266/// if child.is_file() {
1267/// println!("File: {}", child.virtualpath_display());
1268/// }
1269/// }
1270/// # Ok::<_, Box<dyn std::error::Error>>(())
1271/// ```
1272pub struct VirtualReadDir<'a, Marker> {
1273 inner: std::fs::ReadDir,
1274 parent: &'a VirtualPath<Marker>,
1275}
1276
1277impl<Marker> std::fmt::Debug for VirtualReadDir<'_, Marker> {
1278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1279 f.debug_struct("VirtualReadDir")
1280 .field("parent", &self.parent.virtualpath_display().to_string())
1281 .finish_non_exhaustive()
1282 }
1283}
1284
1285impl<Marker: Clone> Iterator for VirtualReadDir<'_, Marker> {
1286 type Item = std::io::Result<VirtualPath<Marker>>;
1287
1288 fn next(&mut self) -> Option<Self::Item> {
1289 match self.inner.next()? {
1290 Ok(entry) => {
1291 let file_name = entry.file_name();
1292 match self.parent.virtual_join(file_name) {
1293 Ok(virtual_path) => Some(Ok(virtual_path)),
1294 Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
1295 }
1296 }
1297 Err(e) => Some(Err(e)),
1298 }
1299 }
1300}