strict_path/validator/virtual_root.rs
1//! `VirtualRoot<Marker>` — the factory for `VirtualPath` values clamped to a virtual root.
2//!
3//! A `VirtualRoot` wraps a `PathBoundary` and maps all paths into a virtual namespace
4//! rooted at `"/"`. Traversal past the root is clamped (not rejected): `virtual_join("../../x")`
5//! resolves to `"/x"` rather than escaping the real filesystem boundary. This makes
6//! `VirtualRoot` safe to expose to untrusted input even without returning errors.
7use crate::path::virtual_path::VirtualPath;
8use crate::validator::path_history::PathHistory;
9use crate::PathBoundary;
10use crate::Result;
11use std::marker::PhantomData;
12use std::path::Path;
13
14/// Provide a user‑facing virtual root that produces `VirtualPath` values clamped to a boundary.
15#[derive(Clone)]
16#[must_use = "a VirtualRoot is validated and ready to enforce virtual path restrictions — call .virtual_join() to validate untrusted input, .into_virtualpath() to get the root path, or pass to functions that accept &VirtualRoot<Marker>"]
17pub struct VirtualRoot<Marker = ()> {
18 pub(crate) root: PathBoundary<Marker>,
19 pub(crate) _marker: PhantomData<Marker>,
20}
21
22impl<Marker> VirtualRoot<Marker> {
23 // no extra constructors; use PathBoundary::virtualize() or VirtualRoot::try_new
24 /// Create a `VirtualRoot` from an existing directory.
25 ///
26 /// # Errors
27 ///
28 /// - `StrictPathError::InvalidRestriction`: Root invalid or cannot be canonicalized.
29 ///
30 /// # Examples
31 ///
32 /// ```rust
33 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
34 /// use strict_path::VirtualRoot;
35 /// let vroot = VirtualRoot::<()>::try_new("./data")?;
36 /// # Ok(())
37 /// # }
38 /// ```
39 #[must_use = "this returns a Result containing the validated VirtualRoot — handle the Result to detect invalid root directories"]
40 #[inline]
41 pub fn try_new<P: AsRef<Path>>(root_path: P) -> Result<Self> {
42 let root = PathBoundary::try_new(root_path)?;
43 Ok(Self {
44 root,
45 _marker: PhantomData,
46 })
47 }
48
49 /// Return filesystem metadata for the underlying root directory.
50 #[inline]
51 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
52 self.root.metadata()
53 }
54
55 /// Consume this virtual root and return the rooted `VirtualPath` ("/").
56 ///
57 /// # Errors
58 ///
59 /// - `StrictPathError::PathResolutionError`: Canonicalization fails (root removed or inaccessible).
60 /// - `StrictPathError::PathEscapesBoundary`: Root moved outside the boundary between checks.
61 ///
62 /// # Examples
63 ///
64 /// ```rust
65 /// # use strict_path::{VirtualPath, VirtualRoot};
66 /// # let root = std::env::temp_dir().join("into-virtualpath-example");
67 /// # std::fs::create_dir_all(&root)?;
68 /// let vroot: VirtualRoot = VirtualRoot::try_new(&root)?;
69 /// let root_virtual: VirtualPath = vroot.into_virtualpath()?;
70 /// assert_eq!(root_virtual.virtualpath_display().to_string(), "/");
71 /// # std::fs::remove_dir_all(&root)?;
72 /// # Ok::<_, Box<dyn std::error::Error>>(())
73 /// ```
74 #[must_use = "into_virtualpath() consumes the VirtualRoot — use the returned VirtualPath for virtual path operations"]
75 #[inline]
76 pub fn into_virtualpath(self) -> Result<VirtualPath<Marker>> {
77 let strict_root = self.root.into_strictpath()?;
78 Ok(strict_root.virtualize())
79 }
80
81 /// Consume this virtual root and substitute a new marker type.
82 ///
83 /// Mirrors [`crate::PathBoundary::change_marker`], [`crate::StrictPath::change_marker`], and
84 /// [`crate::VirtualPath::change_marker`]. Use this when encoding proven authorization
85 /// into the type system (e.g., after validating a user's permissions). The
86 /// consumption makes marker changes explicit during code review.
87 ///
88 /// # Examples
89 ///
90 /// ```rust
91 /// # use strict_path::VirtualRoot;
92 /// # let root_dir = std::env::temp_dir().join("vroot-change-marker-example");
93 /// # std::fs::create_dir_all(&root_dir)?;
94 /// struct UserFiles;
95 /// struct ReadOnly;
96 /// struct ReadWrite;
97 ///
98 /// let read_root: VirtualRoot<(UserFiles, ReadOnly)> = VirtualRoot::try_new(&root_dir)?;
99 ///
100 /// // After authorization check...
101 /// let write_root: VirtualRoot<(UserFiles, ReadWrite)> = read_root.change_marker();
102 /// # std::fs::remove_dir_all(&root_dir)?;
103 /// # Ok::<_, Box<dyn std::error::Error>>(())
104 /// ```
105 #[must_use = "change_marker() consumes self — the original VirtualRoot is moved; use the returned VirtualRoot<NewMarker>"]
106 #[inline]
107 pub fn change_marker<NewMarker>(self) -> VirtualRoot<NewMarker> {
108 let VirtualRoot { root, .. } = self;
109
110 VirtualRoot {
111 root: root.change_marker(),
112 _marker: PhantomData,
113 }
114 }
115
116 /// Create a symbolic link at `link_path` pointing to this root's underlying directory.
117 ///
118 /// `link_path` is interpreted in the virtual dimension and resolved via `virtual_join()`
119 /// so that absolute virtual paths ("/links/a") are clamped within this virtual root and
120 /// relative paths are resolved relative to the virtual root.
121 pub fn virtual_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
122 // Resolve the link location in virtual space first (clamps/anchors under this root)
123 let link_ref = link_path.as_ref();
124 let validated_link = self.virtual_join(link_ref).map_err(std::io::Error::other)?;
125
126 // Obtain the strict target for the root directory
127 let root = self
128 .root
129 .clone()
130 .into_strictpath()
131 .map_err(std::io::Error::other)?;
132
133 root.strict_symlink(validated_link.as_unvirtual().path())
134 }
135
136 /// Create a hard link at `link_path` pointing to this root's underlying directory.
137 ///
138 /// The link location is resolved via `virtual_join()` to clamp/anchor within this root.
139 /// Note: Most platforms forbid directory hard links; expect an error from the OS.
140 pub fn virtual_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
141 let link_ref = link_path.as_ref();
142 let validated_link = self.virtual_join(link_ref).map_err(std::io::Error::other)?;
143
144 let root = self
145 .root
146 .clone()
147 .into_strictpath()
148 .map_err(std::io::Error::other)?;
149
150 root.strict_hard_link(validated_link.as_unvirtual().path())
151 }
152
153 /// Create a Windows NTFS directory junction at `link_path` pointing to this virtual root's directory.
154 ///
155 /// - Windows-only and behind the `junctions` feature.
156 #[cfg(all(windows, feature = "junctions"))]
157 pub fn virtual_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
158 let link_ref = link_path.as_ref();
159 let validated_link = self.virtual_join(link_ref).map_err(std::io::Error::other)?;
160
161 let root = self
162 .root
163 .clone()
164 .into_strictpath()
165 .map_err(std::io::Error::other)?;
166
167 root.strict_junction(validated_link.as_unvirtual().path())
168 }
169
170 /// Read directory entries at the virtual root (discovery). Re‑join names through virtual/strict APIs before I/O.
171 #[inline]
172 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
173 self.root.read_dir()
174 }
175
176 /// Iterate directory entries at the virtual root, yielding validated `VirtualPath` values.
177 ///
178 /// Unlike `read_dir()` which returns raw `std::fs::DirEntry` values requiring manual
179 /// re-validation, this method yields `VirtualPath` entries directly. Each entry is
180 /// automatically validated through `virtual_join()` so you can use it immediately
181 /// for I/O operations without additional validation.
182 ///
183 /// # Examples
184 ///
185 /// ```rust
186 /// use strict_path::VirtualRoot;
187 ///
188 /// # let temp = tempfile::tempdir()?;
189 /// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
190 /// # vroot.virtual_join("file.txt")?.write("test")?;
191 ///
192 /// // Auto-validated iteration - no manual re-join needed!
193 /// for entry in vroot.virtual_read_dir()? {
194 /// let child = entry?;
195 /// println!("Virtual: {}", child.virtualpath_display());
196 /// }
197 /// # Ok::<_, Box<dyn std::error::Error>>(())
198 /// ```
199 #[inline]
200 pub fn virtual_read_dir(&self) -> std::io::Result<VirtualRootReadDir<'_, Marker>> {
201 Ok(VirtualRootReadDir {
202 inner: self.root.read_dir()?,
203 vroot: self,
204 })
205 }
206
207 /// Remove the underlying root directory (non‑recursive); fails if not empty.
208 #[inline]
209 pub fn remove_dir(&self) -> std::io::Result<()> {
210 self.root.remove_dir()
211 }
212
213 /// Recursively remove the underlying root directory and all its contents.
214 #[inline]
215 pub fn remove_dir_all(&self) -> std::io::Result<()> {
216 self.root.remove_dir_all()
217 }
218
219 /// Ensure the directory exists (create if missing), then return a `VirtualRoot`.
220 ///
221 /// # Examples
222 ///
223 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
224 /// ```rust
225 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
226 /// use strict_path::VirtualRoot;
227 /// let vroot = VirtualRoot::<()>::try_new_create("./data")?;
228 /// # Ok(())
229 /// # }
230 /// ```
231 #[must_use = "this returns a Result containing the validated VirtualRoot — handle the Result to detect invalid root directories"]
232 #[inline]
233 pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
234 let root = PathBoundary::try_new_create(root_path)?;
235 Ok(Self {
236 root,
237 _marker: PhantomData,
238 })
239 }
240
241 /// Join a candidate path to this virtual root, producing a clamped `VirtualPath`.
242 ///
243 /// This is the security gateway for virtual paths. Absolute paths (starting with `"/"`) are
244 /// automatically clamped to the virtual root, ensuring paths cannot escape the sandbox.
245 /// For example, `"/etc/config"` becomes `vroot/etc/config`, and traversal attempts like
246 /// `"../../../../etc/passwd"` are clamped to `vroot/etc/passwd`. This clamping behavior is
247 /// what makes the `virtual_` dimension safe for user-facing operations.
248 ///
249 /// # Errors
250 ///
251 /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
252 ///
253 /// # Examples
254 ///
255 /// ```rust
256 /// # use strict_path::VirtualRoot;
257 /// # let td = tempfile::tempdir().unwrap();
258 /// let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path())?;
259 ///
260 /// // Absolute paths are clamped to virtual root, not system root
261 /// let user_input_abs = "/etc/config"; // Untrusted input
262 /// let path1 = vroot.virtual_join(user_input_abs)?;
263 /// assert_eq!(path1.virtualpath_display().to_string(), "/etc/config");
264 ///
265 /// // Traversal attempts are also clamped
266 /// let attack_input = "../../../etc/passwd"; // Untrusted input
267 /// let path2 = vroot.virtual_join(attack_input)?;
268 /// assert_eq!(path2.virtualpath_display().to_string(), "/etc/passwd");
269 ///
270 /// // Both paths are safely within the virtual root on the actual filesystem
271 /// # Ok::<(), Box<dyn std::error::Error>>(())
272 /// ```
273 #[must_use = "virtual_join() validates untrusted input against the virtual root — always handle the Result to detect escape attempts"]
274 #[inline]
275 pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
276 // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
277 let user_candidate = candidate_path.as_ref().to_path_buf();
278 let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
279
280 // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
281 let validated = anchored.boundary_check(self.root.stated_path())?;
282
283 // 3) Construct a StrictPath directly and then virtualize
284 let jp = crate::path::strict_path::StrictPath::new(
285 std::sync::Arc::new(self.root.clone()),
286 validated,
287 );
288 Ok(jp.virtualize())
289 }
290
291 /// Returns the underlying path boundary root as a system path.
292 #[inline]
293 pub(crate) fn path(&self) -> &Path {
294 self.root.path()
295 }
296
297 /// Return the virtual root path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
298 #[must_use = "pass interop_path() directly to third-party APIs requiring AsRef<Path> — never wrap it in Path::new() or PathBuf::from() as that defeats boundary safety"]
299 #[inline]
300 pub fn interop_path(&self) -> &std::ffi::OsStr {
301 self.root.interop_path()
302 }
303
304 /// Returns true if the underlying path boundary root exists.
305 #[must_use]
306 #[inline]
307 pub fn exists(&self) -> bool {
308 self.root.exists()
309 }
310
311 /// Borrow the underlying `PathBoundary`.
312 #[must_use = "as_unvirtual() borrows the underlying PathBoundary — use it for strict operations or pass to functions accepting &PathBoundary<Marker>"]
313 #[inline]
314 pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
315 &self.root
316 }
317
318 /// Consume this `VirtualRoot` and return the underlying `PathBoundary` (symmetry with `virtualize`).
319 #[must_use = "unvirtual() consumes self — use the returned PathBoundary for strict path operations, or prefer .as_unvirtual() to borrow without consuming"]
320 #[inline]
321 pub fn unvirtual(self) -> PathBoundary<Marker> {
322 self.root
323 }
324
325 // OS Standard Directory Constructors
326 //
327 // Creates virtual roots in OS standard directories following platform conventions.
328 // Applications see clean virtual paths ("/config.toml") while the system manages
329 // the actual location (e.g., "~/.config/myapp/config.toml").
330}
331
332/// Display shows "/": The real system path must never appear in user-facing output
333/// (logs, API responses, error messages). Showing "/" reinforces that VirtualRoot
334/// represents a virtual namespace root, not a concrete filesystem location.
335impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 write!(f, "/")
338 }
339}
340
341impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 f.debug_struct("VirtualRoot")
344 .field("root", &self.path())
345 .field("marker", &std::any::type_name::<Marker>())
346 .finish()
347 }
348}
349
350impl<Marker> Eq for VirtualRoot<Marker> {}
351
352impl<M1, M2> PartialEq<VirtualRoot<M2>> for VirtualRoot<M1> {
353 #[inline]
354 fn eq(&self, other: &VirtualRoot<M2>) -> bool {
355 self.path() == other.path()
356 }
357}
358
359impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
360 #[inline]
361 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
362 self.path().hash(state);
363 }
364}
365
366impl<Marker> PartialOrd for VirtualRoot<Marker> {
367 #[inline]
368 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
369 Some(self.cmp(other))
370 }
371}
372
373impl<Marker> Ord for VirtualRoot<Marker> {
374 #[inline]
375 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
376 self.path().cmp(other.path())
377 }
378}
379
380impl<M1, M2> PartialEq<crate::PathBoundary<M2>> for VirtualRoot<M1> {
381 #[inline]
382 fn eq(&self, other: &crate::PathBoundary<M2>) -> bool {
383 self.path() == other.path()
384 }
385}
386
387/// compare against "/": VirtualRoot's public identity is the virtual namespace root.
388/// Comparing against the real system path would leak implementation details and break the
389/// abstraction — callers should never need to know the underlying directory.
390impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
391 #[inline]
392 fn eq(&self, other: &std::path::Path) -> bool {
393 // Compare as virtual root path (always "/")
394 // VirtualRoot represents the virtual "/" regardless of underlying system path
395 let other_str = other.to_string_lossy();
396
397 #[cfg(windows)]
398 let other_normalized = other_str.replace('\\', "/");
399 #[cfg(not(windows))]
400 let other_normalized = other_str.to_string();
401
402 let normalized_other = if other_normalized.starts_with('/') {
403 other_normalized
404 } else {
405 format!("/{other_normalized}")
406 };
407
408 "/" == normalized_other
409 }
410}
411
412impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
413 #[inline]
414 fn eq(&self, other: &std::path::PathBuf) -> bool {
415 self.eq(other.as_path())
416 }
417}
418
419impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
420 #[inline]
421 fn eq(&self, other: &&std::path::Path) -> bool {
422 self.eq(*other)
423 }
424}
425
426impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
427 #[inline]
428 fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
429 // Compare as virtual root path (always "/")
430 let other_str = other.to_string_lossy();
431
432 // Handle empty path specially - "/" is greater than ""
433 if other_str.is_empty() {
434 return Some(std::cmp::Ordering::Greater);
435 }
436
437 #[cfg(windows)]
438 let other_normalized = other_str.replace('\\', "/");
439 #[cfg(not(windows))]
440 let other_normalized = other_str.to_string();
441
442 let normalized_other = if other_normalized.starts_with('/') {
443 other_normalized
444 } else {
445 format!("/{other_normalized}")
446 };
447
448 Some("/".cmp(&normalized_other))
449 }
450}
451
452impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
453 #[inline]
454 fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
455 self.partial_cmp(*other)
456 }
457}
458
459impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
460 #[inline]
461 fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
462 self.partial_cmp(other.as_path())
463 }
464}
465
466impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
467 type Err = crate::StrictPathError;
468
469 /// Parse a VirtualRoot from a string path for universal ergonomics.
470 ///
471 /// Creates the directory if it doesn't exist, enabling seamless integration
472 /// with any string-parsing context (clap, config files, environment variables, etc.):
473 /// ```rust
474 /// # use strict_path::VirtualRoot;
475 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
476 /// let temp_dir = tempfile::tempdir()?;
477 /// let virtual_path = temp_dir.path().join("virtual_dir");
478 /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
479 /// assert!(virtual_path.exists());
480 /// # Ok(())
481 /// # }
482 /// ```
483 #[inline]
484 fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
485 Self::try_new_create(path)
486 }
487}
488
489// ============================================================
490// VirtualRootReadDir — Iterator for validated virtual directory entries
491// ============================================================
492
493/// Iterator over directory entries that yields validated `VirtualPath` values.
494///
495/// Created by `VirtualRoot::virtual_read_dir()`. Each iteration automatically validates
496/// the directory entry through `virtual_join()`, so you get `VirtualPath` values directly
497/// instead of raw `std::fs::DirEntry` that would require manual re-validation.
498///
499/// # Examples
500///
501/// ```rust
502/// # use strict_path::VirtualRoot;
503/// # let temp = tempfile::tempdir()?;
504/// let vroot: VirtualRoot = VirtualRoot::try_new(temp.path())?;
505/// # vroot.virtual_join("readme.md")?.write("# Docs")?;
506/// for entry in vroot.virtual_read_dir()? {
507/// let child = entry?;
508/// if child.is_file() {
509/// println!("Virtual: {}", child.virtualpath_display());
510/// }
511/// }
512/// # Ok::<_, Box<dyn std::error::Error>>(())
513/// ```
514pub struct VirtualRootReadDir<'a, Marker> {
515 inner: std::fs::ReadDir,
516 vroot: &'a VirtualRoot<Marker>,
517}
518
519impl<Marker> std::fmt::Debug for VirtualRootReadDir<'_, Marker> {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 f.debug_struct("VirtualRootReadDir")
522 .field("vroot", &"/")
523 .finish_non_exhaustive()
524 }
525}
526
527impl<Marker: Clone> Iterator for VirtualRootReadDir<'_, Marker> {
528 type Item = std::io::Result<crate::path::virtual_path::VirtualPath<Marker>>;
529
530 fn next(&mut self) -> Option<Self::Item> {
531 match self.inner.next()? {
532 Ok(entry) => {
533 let file_name = entry.file_name();
534 match self.vroot.virtual_join(file_name) {
535 Ok(virtual_path) => Some(Ok(virtual_path)),
536 Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
537 }
538 }
539 Err(e) => Some(Err(e)),
540 }
541 }
542}