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#[cfg(feature = "tempfile")]
9use std::sync::Arc;
10
11// keep feature-gated TempDir RAII field using Arc from std::sync
12#[cfg(feature = "tempfile")]
13use tempfile::TempDir;
14
15/// A user-facing virtual root that produces `VirtualPath` values.
16#[derive(Clone)]
17pub struct VirtualRoot<Marker = ()> {
18    pub(crate) root: PathBoundary<Marker>,
19    // Held only to tie RAII of temp directories to the VirtualRoot lifetime
20    #[cfg(feature = "tempfile")]
21    pub(crate) _temp_dir: Option<Arc<TempDir>>, // mirrors RAII when constructed from temp
22    pub(crate) _marker: PhantomData<Marker>,
23}
24
25impl<Marker> VirtualRoot<Marker> {
26    // no extra constructors; use PathBoundary::virtualize() or VirtualRoot::try_new
27    /// Creates a `VirtualRoot` from an existing directory.
28    ///
29    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
30    /// ```rust
31    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
32    /// use strict_path::VirtualRoot;
33    /// let tmp_dir = tempfile::tempdir()?;
34    /// let tmp_dir = VirtualRoot::<()>::try_new(tmp_dir)?; // Clean variable shadowing
35    /// # Ok(())
36    /// # }
37    /// ```
38    #[inline]
39    pub fn try_new<P: AsRef<Path>>(root_path: P) -> Result<Self> {
40        let root = PathBoundary::try_new(root_path)?;
41        Ok(Self {
42            root,
43            #[cfg(feature = "tempfile")]
44            _temp_dir: None,
45            _marker: PhantomData,
46        })
47    }
48
49    /// Creates a VirtualRoot backed by a unique temporary directory with RAII cleanup.
50    ///
51    /// Returns a virtual root whose backing directory is automatically removed when dropped.
52    ///
53    /// # Example
54    /// ```
55    /// # #[cfg(feature = "tempfile")] {
56    /// use strict_path::VirtualRoot;
57    ///
58    /// let uploads_root = VirtualRoot::<()>::try_new_temp()?;
59    /// let tenant_file = uploads_root.virtual_join("tenant/document.pdf")?;
60    /// let display = tenant_file.virtualpath_display().to_string();
61    /// assert!(display.starts_with("/"));
62    /// # }
63    /// # Ok::<(), Box<dyn std::error::Error>>(())
64    /// ```
65    #[cfg(feature = "tempfile")]
66    #[inline]
67    pub fn try_new_temp() -> Result<Self> {
68        let root = PathBoundary::try_new_temp()?;
69        let temp_dir = root.temp_dir_arc();
70        Ok(Self {
71            root,
72            #[cfg(feature = "tempfile")]
73            _temp_dir: temp_dir,
74            _marker: PhantomData,
75        })
76    }
77
78    /// Creates a VirtualRoot in a temporary directory with a custom prefix and RAII cleanup.
79    ///
80    /// Returns a virtual root whose backing directory is automatically removed when dropped.
81    ///
82    /// # Example
83    /// ```
84    /// # #[cfg(feature = "tempfile")] {
85    /// use strict_path::VirtualRoot;
86    ///
87    /// let session_root = VirtualRoot::<()>::try_new_temp_with_prefix("session")?;
88    /// let export_path = session_root.virtual_join("exports/report.txt")?;
89    /// let display = export_path.virtualpath_display().to_string();
90    /// assert!(display.starts_with("/exports"));
91    /// # }
92    /// # Ok::<(), Box<dyn std::error::Error>>(())
93    /// ```
94    #[cfg(feature = "tempfile")]
95    #[inline]
96    pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
97        let root = PathBoundary::try_new_temp_with_prefix(prefix)?;
98        let temp_dir = root.temp_dir_arc();
99        Ok(Self {
100            root,
101            #[cfg(feature = "tempfile")]
102            _temp_dir: temp_dir,
103            _marker: PhantomData,
104        })
105    }
106
107    /// Returns filesystem metadata for the underlying root directory.
108    #[inline]
109    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
110        self.root.metadata()
111    }
112
113    /// Creates a symbolic link at `link_path` that points to this VirtualRoot's underlying directory.
114    pub fn virtual_symlink(
115        &self,
116        link_path: &crate::path::virtual_path::VirtualPath<Marker>,
117    ) -> std::io::Result<()> {
118        let root = self
119            .virtual_join("")
120            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
121
122        root.as_unvirtual().strict_symlink(link_path.as_unvirtual())
123    }
124
125    /// Creates a hard link at `link_path` that points to this VirtualRoot's underlying directory.
126    pub fn virtual_hard_link(
127        &self,
128        link_path: &crate::path::virtual_path::VirtualPath<Marker>,
129    ) -> std::io::Result<()> {
130        let root = self
131            .virtual_join("")
132            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
133
134        root.as_unvirtual()
135            .strict_hard_link(link_path.as_unvirtual())
136    }
137
138    /// Reads the directory entries at the virtual root (like `std::fs::read_dir`).
139    ///
140    /// This is intended for discovery. Prefer collecting each entry's file name via
141    /// `entry.file_name()` and re-joining using `virtual_join(...)` (or `strict_join(...)`
142    /// after converting) before performing I/O on child paths.
143    #[inline]
144    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
145        self.root.read_dir()
146    }
147
148    /// Removes the underlying root directory (non-recursive).
149    ///
150    /// Equivalent to `std::fs::remove_dir(root)`. Fails if the directory is not empty.
151    #[inline]
152    pub fn remove_dir(&self) -> std::io::Result<()> {
153        self.root.remove_dir()
154    }
155
156    /// Recursively removes the underlying root directory and all its contents.
157    ///
158    /// Equivalent to `std::fs::remove_dir_all(root)`.
159    #[inline]
160    pub fn remove_dir_all(&self) -> std::io::Result<()> {
161        self.root.remove_dir_all()
162    }
163
164    /// Creates the directory if missing, then returns a `VirtualRoot`.
165    ///
166    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
167    /// ```rust
168    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
169    /// use strict_path::VirtualRoot;
170    /// let tmp_dir = tempfile::tempdir()?;
171    /// let tmp_dir = VirtualRoot::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
172    /// # Ok(())
173    /// # }
174    /// ```
175    #[inline]
176    pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
177        let root = PathBoundary::try_new_create(root_path)?;
178        Ok(Self {
179            root,
180            #[cfg(feature = "tempfile")]
181            _temp_dir: None,
182            _marker: PhantomData,
183        })
184    }
185
186    /// Joins a path to this virtual root, producing a clamped `VirtualPath`.
187    ///
188    /// Preserves the virtual root through clamping and validates against the restriction.
189    /// May return an error if resolution (e.g., via symlinks) would escape the restriction.
190    #[inline]
191    pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
192        // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
193        let user_candidate = candidate_path.as_ref().to_path_buf();
194        let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
195
196        // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
197        let validated = anchored.boundary_check(self.root.stated_path())?;
198
199        // 3) Construct a StrictPath directly and then virtualize
200        let jp = crate::path::strict_path::StrictPath::new(
201            std::sync::Arc::new(self.root.clone()),
202            validated,
203        );
204        Ok(jp.virtualize())
205    }
206
207    /// Returns the underlying path boundary root as a system path.
208    #[inline]
209    pub(crate) fn path(&self) -> &Path {
210        self.root.path()
211    }
212
213    /// Returns the virtual root path for interop with `AsRef<Path>` APIs.
214    ///
215    /// This provides allocation-free, OS-native string access to the virtual root
216    /// for use with standard library APIs that accept `AsRef<Path>`.
217    #[inline]
218    pub fn interop_path(&self) -> &std::ffi::OsStr {
219        self.root.interop_path()
220    }
221
222    /// Returns true if the underlying path boundary root exists.
223    #[inline]
224    pub fn exists(&self) -> bool {
225        self.root.exists()
226    }
227
228    /// Returns a reference to the underlying `PathBoundary`.
229    ///
230    /// This allows access to path boundary-specific operations like `strictpath_display()`
231    /// while maintaining the borrowed relationship.
232    #[inline]
233    pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
234        &self.root
235    }
236
237    /// Consumes this `VirtualRoot` and returns the underlying `PathBoundary`.
238    ///
239    /// This provides symmetry with `PathBoundary::virtualize()` and allows conversion
240    /// back to the path boundary representation when virtual semantics are no longer needed.
241    #[inline]
242    pub fn unvirtual(self) -> PathBoundary<Marker> {
243        self.root
244    }
245
246    // OS Standard Directory Constructors
247    //
248    // Creates virtual roots in OS standard directories following platform conventions.
249    // Applications see clean virtual paths ("/config.toml") while the system manages
250    // the actual location (e.g., "~/.config/myapp/config.toml").
251
252    /// Creates a virtual root in the OS standard config directory.
253    ///
254    /// **Cross-Platform Behavior:**
255    /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
256    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
257    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
258    #[cfg(feature = "dirs")]
259    pub fn try_new_os_config(app_name: &str) -> Result<Self> {
260        let root = crate::PathBoundary::try_new_os_config(app_name)?;
261        Ok(Self {
262            root,
263            #[cfg(feature = "tempfile")]
264            _temp_dir: None,
265            _marker: PhantomData,
266        })
267    }
268
269    /// Creates a virtual root in the OS standard data directory.
270    #[cfg(feature = "dirs")]
271    pub fn try_new_os_data(app_name: &str) -> Result<Self> {
272        let root = crate::PathBoundary::try_new_os_data(app_name)?;
273        Ok(Self {
274            root,
275            #[cfg(feature = "tempfile")]
276            _temp_dir: None,
277            _marker: PhantomData,
278        })
279    }
280
281    /// Creates a virtual root in the OS standard cache directory.
282    #[cfg(feature = "dirs")]
283    pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
284        let root = crate::PathBoundary::try_new_os_cache(app_name)?;
285        Ok(Self {
286            root,
287            #[cfg(feature = "tempfile")]
288            _temp_dir: None,
289            _marker: PhantomData,
290        })
291    }
292
293    /// Creates a virtual root in the OS local config directory.
294    #[cfg(feature = "dirs")]
295    pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
296        let root = crate::PathBoundary::try_new_os_config_local(app_name)?;
297        Ok(Self {
298            root,
299            #[cfg(feature = "tempfile")]
300            _temp_dir: None,
301            _marker: PhantomData,
302        })
303    }
304
305    /// Creates a virtual root in the OS local data directory.
306    #[cfg(feature = "dirs")]
307    pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
308        let root = crate::PathBoundary::try_new_os_data_local(app_name)?;
309        Ok(Self {
310            root,
311            #[cfg(feature = "tempfile")]
312            _temp_dir: None,
313            _marker: PhantomData,
314        })
315    }
316
317    /// Creates a virtual root in the user's home directory.
318    #[cfg(feature = "dirs")]
319    pub fn try_new_os_home() -> Result<Self> {
320        let root = crate::PathBoundary::try_new_os_home()?;
321        Ok(Self {
322            root,
323            #[cfg(feature = "tempfile")]
324            _temp_dir: None,
325            _marker: PhantomData,
326        })
327    }
328
329    /// Creates a virtual root in the user's desktop directory.
330    #[cfg(feature = "dirs")]
331    pub fn try_new_os_desktop() -> Result<Self> {
332        let root = crate::PathBoundary::try_new_os_desktop()?;
333        Ok(Self {
334            root,
335            #[cfg(feature = "tempfile")]
336            _temp_dir: None,
337            _marker: PhantomData,
338        })
339    }
340
341    /// Creates a virtual root in the user's documents directory.
342    #[cfg(feature = "dirs")]
343    pub fn try_new_os_documents() -> Result<Self> {
344        let root = crate::PathBoundary::try_new_os_documents()?;
345        Ok(Self {
346            root,
347            #[cfg(feature = "tempfile")]
348            _temp_dir: None,
349            _marker: PhantomData,
350        })
351    }
352
353    /// Creates a virtual root in the user's downloads directory.
354    #[cfg(feature = "dirs")]
355    pub fn try_new_os_downloads() -> Result<Self> {
356        let root = crate::PathBoundary::try_new_os_downloads()?;
357        Ok(Self {
358            root,
359            #[cfg(feature = "tempfile")]
360            _temp_dir: None,
361            _marker: PhantomData,
362        })
363    }
364
365    /// Creates a virtual root in the user's pictures directory.
366    #[cfg(feature = "dirs")]
367    pub fn try_new_os_pictures() -> Result<Self> {
368        let root = crate::PathBoundary::try_new_os_pictures()?;
369        Ok(Self {
370            root,
371            #[cfg(feature = "tempfile")]
372            _temp_dir: None,
373            _marker: PhantomData,
374        })
375    }
376
377    /// Creates a virtual root in the user's music/audio directory.
378    #[cfg(feature = "dirs")]
379    pub fn try_new_os_audio() -> Result<Self> {
380        let root = crate::PathBoundary::try_new_os_audio()?;
381        Ok(Self {
382            root,
383            #[cfg(feature = "tempfile")]
384            _temp_dir: None,
385            _marker: PhantomData,
386        })
387    }
388
389    /// Creates a virtual root in the user's videos directory.
390    #[cfg(feature = "dirs")]
391    pub fn try_new_os_videos() -> Result<Self> {
392        let root = crate::PathBoundary::try_new_os_videos()?;
393        Ok(Self {
394            root,
395            #[cfg(feature = "tempfile")]
396            _temp_dir: None,
397            _marker: PhantomData,
398        })
399    }
400
401    /// Creates a virtual root in the OS executable directory (Linux only).
402    #[cfg(feature = "dirs")]
403    pub fn try_new_os_executables() -> Result<Self> {
404        let root = crate::PathBoundary::try_new_os_executables()?;
405        Ok(Self {
406            root,
407            #[cfg(feature = "tempfile")]
408            _temp_dir: None,
409            _marker: PhantomData,
410        })
411    }
412
413    /// Creates a virtual root in the OS runtime directory (Linux only).
414    #[cfg(feature = "dirs")]
415    pub fn try_new_os_runtime() -> Result<Self> {
416        let root = crate::PathBoundary::try_new_os_runtime()?;
417        Ok(Self {
418            root,
419            #[cfg(feature = "tempfile")]
420            _temp_dir: None,
421            _marker: PhantomData,
422        })
423    }
424
425    /// Creates a virtual root in the OS state directory (Linux only).
426    #[cfg(feature = "dirs")]
427    pub fn try_new_os_state(app_name: &str) -> Result<Self> {
428        let root = crate::PathBoundary::try_new_os_state(app_name)?;
429        Ok(Self {
430            root,
431            #[cfg(feature = "tempfile")]
432            _temp_dir: None,
433            _marker: PhantomData,
434        })
435    }
436
437    /// Creates a virtual root using app-path for portable applications.
438    ///
439    /// Creates a directory relative to the executable location, with optional
440    /// environment variable override support for deployment flexibility.
441    #[cfg(feature = "app-path")]
442    pub fn try_new_app_path(subdir: &str, env_override: Option<&str>) -> Result<Self> {
443        let root = crate::PathBoundary::try_new_app_path(subdir, env_override)?;
444        Ok(Self {
445            root,
446            #[cfg(feature = "tempfile")]
447            _temp_dir: None,
448            _marker: PhantomData,
449        })
450    }
451}
452
453impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        write!(f, "{}", self.path().display())
456    }
457}
458
459impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
460    fn as_ref(&self) -> &Path {
461        self.path()
462    }
463}
464
465impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
466    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467        f.debug_struct("VirtualRoot")
468            .field("root", &self.path())
469            .field("marker", &std::any::type_name::<Marker>())
470            .finish()
471    }
472}
473
474impl<Marker> PartialEq for VirtualRoot<Marker> {
475    #[inline]
476    fn eq(&self, other: &Self) -> bool {
477        self.path() == other.path()
478    }
479}
480
481impl<Marker> Eq for VirtualRoot<Marker> {}
482
483impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
484    #[inline]
485    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
486        self.path().hash(state);
487    }
488}
489
490impl<Marker> PartialOrd for VirtualRoot<Marker> {
491    #[inline]
492    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
493        Some(self.cmp(other))
494    }
495}
496
497impl<Marker> Ord for VirtualRoot<Marker> {
498    #[inline]
499    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
500        self.path().cmp(other.path())
501    }
502}
503
504impl<Marker> PartialEq<crate::PathBoundary<Marker>> for VirtualRoot<Marker> {
505    #[inline]
506    fn eq(&self, other: &crate::PathBoundary<Marker>) -> bool {
507        self.path() == other.path()
508    }
509}
510
511impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
512    #[inline]
513    fn eq(&self, other: &std::path::Path) -> bool {
514        // Compare as virtual root path (always "/")
515        // VirtualRoot represents the virtual "/" regardless of underlying system path
516        let other_str = other.to_string_lossy();
517
518        #[cfg(windows)]
519        let other_normalized = other_str.replace('\\', "/");
520        #[cfg(not(windows))]
521        let other_normalized = other_str.to_string();
522
523        let normalized_other = if other_normalized.starts_with('/') {
524            other_normalized
525        } else {
526            format!("/{}", other_normalized)
527        };
528
529        "/" == normalized_other
530    }
531}
532
533impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
534    #[inline]
535    fn eq(&self, other: &std::path::PathBuf) -> bool {
536        self.eq(other.as_path())
537    }
538}
539
540impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
541    #[inline]
542    fn eq(&self, other: &&std::path::Path) -> bool {
543        self.eq(*other)
544    }
545}
546
547impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
548    #[inline]
549    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
550        // Compare as virtual root path (always "/")
551        let other_str = other.to_string_lossy();
552
553        // Handle empty path specially - "/" is greater than ""
554        if other_str.is_empty() {
555            return Some(std::cmp::Ordering::Greater);
556        }
557
558        #[cfg(windows)]
559        let other_normalized = other_str.replace('\\', "/");
560        #[cfg(not(windows))]
561        let other_normalized = other_str.to_string();
562
563        let normalized_other = if other_normalized.starts_with('/') {
564            other_normalized
565        } else {
566            format!("/{}", other_normalized)
567        };
568
569        Some("/".cmp(&normalized_other))
570    }
571}
572
573impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
574    #[inline]
575    fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
576        self.partial_cmp(*other)
577    }
578}
579
580impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
581    #[inline]
582    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
583        self.partial_cmp(other.as_path())
584    }
585}
586
587impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
588    type Err = crate::StrictPathError;
589
590    /// Parse a VirtualRoot from a string path for universal ergonomics.
591    ///
592    /// Creates the directory if it doesn't exist, enabling seamless integration
593    /// with any string-parsing context (clap, config files, environment variables, etc.):
594    /// ```rust
595    /// # use strict_path::VirtualRoot;
596    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
597    /// let temp_dir = tempfile::tempdir()?;
598    /// let virtual_path = temp_dir.path().join("virtual_dir");
599    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
600    /// assert!(virtual_path.exists());
601    /// # Ok(())
602    /// # }
603    /// ```
604    #[inline]
605    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
606        Self::try_new_create(path)
607    }
608}