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