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