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