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 the directory if missing, then returns a `VirtualRoot`.
50    ///
51    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
52    /// ```rust
53    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
54    /// use strict_path::VirtualRoot;
55    /// let tmp_dir = tempfile::tempdir()?;
56    /// let tmp_dir = VirtualRoot::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
57    /// # Ok(())
58    /// # }
59    /// ```
60    #[inline]
61    pub fn try_new_create<P: AsRef<Path>>(root_path: P) -> Result<Self> {
62        let root = PathBoundary::try_new_create(root_path)?;
63        Ok(Self {
64            root,
65            #[cfg(feature = "tempfile")]
66            _temp_dir: None,
67            _marker: PhantomData,
68        })
69    }
70
71    /// Joins a path to this virtual root, producing a clamped `VirtualPath`.
72    ///
73    /// Preserves the virtual root through clamping and validates against the restriction.
74    /// May return an error if resolution (e.g., via symlinks) would escape the restriction.
75    #[inline]
76    pub fn virtual_join<P: AsRef<Path>>(&self, candidate_path: P) -> Result<VirtualPath<Marker>> {
77        // 1) Anchor in virtual space (clamps virtual root and resolves relative parts)
78        let user_candidate = candidate_path.as_ref().to_path_buf();
79        let anchored = PathHistory::new(user_candidate).canonicalize_anchored(&self.root)?;
80
81        // 2) Boundary-check once against the PathBoundary's canonicalized root (no re-canonicalization)
82        let validated = anchored.boundary_check(self.root.stated_path())?;
83
84        // 3) Construct a StrictPath directly and then virtualize
85        let jp = crate::path::strict_path::StrictPath::new(
86            std::sync::Arc::new(self.root.clone()),
87            validated,
88        );
89        Ok(jp.virtualize())
90    }
91
92    /// Returns the underlying path boundary root as a system path.
93    #[inline]
94    pub(crate) fn path(&self) -> &Path {
95        self.root.path()
96    }
97
98    /// Returns the virtual root path for interop with `AsRef<Path>` APIs.
99    ///
100    /// This provides allocation-free, OS-native string access to the virtual root
101    /// for use with standard library APIs that accept `AsRef<Path>`.
102    #[inline]
103    pub fn interop_path(&self) -> &std::ffi::OsStr {
104        self.root.interop_path()
105    }
106
107    /// Returns true if the underlying path boundary root exists.
108    #[inline]
109    pub fn exists(&self) -> bool {
110        self.root.exists()
111    }
112
113    /// Returns a reference to the underlying `PathBoundary`.
114    ///
115    /// This allows access to path boundary-specific operations like `strictpath_display()`
116    /// while maintaining the borrowed relationship.
117    #[inline]
118    pub fn as_unvirtual(&self) -> &PathBoundary<Marker> {
119        &self.root
120    }
121
122    /// Consumes this `VirtualRoot` and returns the underlying `PathBoundary`.
123    ///
124    /// This provides symmetry with `PathBoundary::virtualize()` and allows conversion
125    /// back to the path boundary representation when virtual semantics are no longer needed.
126    #[inline]
127    pub fn unvirtual(self) -> PathBoundary<Marker> {
128        self.root
129    }
130
131    // OS Standard Directory Constructors
132    //
133    // Creates virtual roots in OS standard directories following platform conventions.
134    // Applications see clean virtual paths ("/config.toml") while the system manages
135    // the actual location (e.g., "~/.config/myapp/config.toml").
136
137    /// Creates a virtual root in the OS standard config directory.
138    ///
139    /// **Cross-Platform Behavior:**
140    /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
141    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
142    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
143    #[cfg(feature = "dirs")]
144    pub fn try_new_os_config(app_name: &str) -> Result<Self> {
145        let root = crate::PathBoundary::try_new_os_config(app_name)?;
146        Ok(Self {
147            root,
148            #[cfg(feature = "tempfile")]
149            _temp_dir: None,
150            _marker: PhantomData,
151        })
152    }
153
154    /// Creates a virtual root in the OS standard data directory.
155    #[cfg(feature = "dirs")]
156    pub fn try_new_os_data(app_name: &str) -> Result<Self> {
157        let root = crate::PathBoundary::try_new_os_data(app_name)?;
158        Ok(Self {
159            root,
160            #[cfg(feature = "tempfile")]
161            _temp_dir: None,
162            _marker: PhantomData,
163        })
164    }
165
166    /// Creates a virtual root in the OS standard cache directory.
167    #[cfg(feature = "dirs")]
168    pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
169        let root = crate::PathBoundary::try_new_os_cache(app_name)?;
170        Ok(Self {
171            root,
172            #[cfg(feature = "tempfile")]
173            _temp_dir: None,
174            _marker: PhantomData,
175        })
176    }
177
178    /// Creates a virtual root in the OS local config directory.
179    #[cfg(feature = "dirs")]
180    pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
181        let root = crate::PathBoundary::try_new_os_config_local(app_name)?;
182        Ok(Self {
183            root,
184            #[cfg(feature = "tempfile")]
185            _temp_dir: None,
186            _marker: PhantomData,
187        })
188    }
189
190    /// Creates a virtual root in the OS local data directory.
191    #[cfg(feature = "dirs")]
192    pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
193        let root = crate::PathBoundary::try_new_os_data_local(app_name)?;
194        Ok(Self {
195            root,
196            #[cfg(feature = "tempfile")]
197            _temp_dir: None,
198            _marker: PhantomData,
199        })
200    }
201
202    /// Creates a virtual root in the user's home directory.
203    #[cfg(feature = "dirs")]
204    pub fn try_new_os_home() -> Result<Self> {
205        let root = crate::PathBoundary::try_new_os_home()?;
206        Ok(Self {
207            root,
208            #[cfg(feature = "tempfile")]
209            _temp_dir: None,
210            _marker: PhantomData,
211        })
212    }
213
214    /// Creates a virtual root in the user's desktop directory.
215    #[cfg(feature = "dirs")]
216    pub fn try_new_os_desktop() -> Result<Self> {
217        let root = crate::PathBoundary::try_new_os_desktop()?;
218        Ok(Self {
219            root,
220            #[cfg(feature = "tempfile")]
221            _temp_dir: None,
222            _marker: PhantomData,
223        })
224    }
225
226    /// Creates a virtual root in the user's documents directory.
227    #[cfg(feature = "dirs")]
228    pub fn try_new_os_documents() -> Result<Self> {
229        let root = crate::PathBoundary::try_new_os_documents()?;
230        Ok(Self {
231            root,
232            #[cfg(feature = "tempfile")]
233            _temp_dir: None,
234            _marker: PhantomData,
235        })
236    }
237
238    /// Creates a virtual root in the user's downloads directory.
239    #[cfg(feature = "dirs")]
240    pub fn try_new_os_downloads() -> Result<Self> {
241        let root = crate::PathBoundary::try_new_os_downloads()?;
242        Ok(Self {
243            root,
244            #[cfg(feature = "tempfile")]
245            _temp_dir: None,
246            _marker: PhantomData,
247        })
248    }
249
250    /// Creates a virtual root in the user's pictures directory.
251    #[cfg(feature = "dirs")]
252    pub fn try_new_os_pictures() -> Result<Self> {
253        let root = crate::PathBoundary::try_new_os_pictures()?;
254        Ok(Self {
255            root,
256            #[cfg(feature = "tempfile")]
257            _temp_dir: None,
258            _marker: PhantomData,
259        })
260    }
261
262    /// Creates a virtual root in the user's music/audio directory.
263    #[cfg(feature = "dirs")]
264    pub fn try_new_os_audio() -> Result<Self> {
265        let root = crate::PathBoundary::try_new_os_audio()?;
266        Ok(Self {
267            root,
268            #[cfg(feature = "tempfile")]
269            _temp_dir: None,
270            _marker: PhantomData,
271        })
272    }
273
274    /// Creates a virtual root in the user's videos directory.
275    #[cfg(feature = "dirs")]
276    pub fn try_new_os_videos() -> Result<Self> {
277        let root = crate::PathBoundary::try_new_os_videos()?;
278        Ok(Self {
279            root,
280            #[cfg(feature = "tempfile")]
281            _temp_dir: None,
282            _marker: PhantomData,
283        })
284    }
285
286    /// Creates a virtual root in the OS executable directory (Linux only).
287    #[cfg(feature = "dirs")]
288    pub fn try_new_os_executables() -> Result<Self> {
289        let root = crate::PathBoundary::try_new_os_executables()?;
290        Ok(Self {
291            root,
292            #[cfg(feature = "tempfile")]
293            _temp_dir: None,
294            _marker: PhantomData,
295        })
296    }
297
298    /// Creates a virtual root in the OS runtime directory (Linux only).
299    #[cfg(feature = "dirs")]
300    pub fn try_new_os_runtime() -> Result<Self> {
301        let root = crate::PathBoundary::try_new_os_runtime()?;
302        Ok(Self {
303            root,
304            #[cfg(feature = "tempfile")]
305            _temp_dir: None,
306            _marker: PhantomData,
307        })
308    }
309
310    /// Creates a virtual root in the OS state directory (Linux only).
311    #[cfg(feature = "dirs")]
312    pub fn try_new_os_state(app_name: &str) -> Result<Self> {
313        let root = crate::PathBoundary::try_new_os_state(app_name)?;
314        Ok(Self {
315            root,
316            #[cfg(feature = "tempfile")]
317            _temp_dir: None,
318            _marker: PhantomData,
319        })
320    }
321
322    /// Creates a virtual root using app-path for portable applications.
323    ///
324    /// Creates a directory relative to the executable location, with optional
325    /// environment variable override support for deployment flexibility.
326    #[cfg(feature = "app-path")]
327    pub fn try_new_app_path(subdir: &str, env_override: Option<&str>) -> Result<Self> {
328        let root = crate::PathBoundary::try_new_app_path(subdir, env_override)?;
329        Ok(Self {
330            root,
331            #[cfg(feature = "tempfile")]
332            _temp_dir: None,
333            _marker: PhantomData,
334        })
335    }
336}
337
338impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        write!(f, "{}", self.path().display())
341    }
342}
343
344impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
345    fn as_ref(&self) -> &Path {
346        self.path()
347    }
348}
349
350impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        f.debug_struct("VirtualRoot")
353            .field("root", &self.path())
354            .field("marker", &std::any::type_name::<Marker>())
355            .finish()
356    }
357}
358
359impl<Marker> PartialEq for VirtualRoot<Marker> {
360    #[inline]
361    fn eq(&self, other: &Self) -> bool {
362        self.path() == other.path()
363    }
364}
365
366impl<Marker> Eq for VirtualRoot<Marker> {}
367
368impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
369    #[inline]
370    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
371        self.path().hash(state);
372    }
373}
374
375impl<Marker> PartialOrd for VirtualRoot<Marker> {
376    #[inline]
377    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
378        Some(self.cmp(other))
379    }
380}
381
382impl<Marker> Ord for VirtualRoot<Marker> {
383    #[inline]
384    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
385        self.path().cmp(other.path())
386    }
387}
388
389impl<Marker> PartialEq<crate::PathBoundary<Marker>> for VirtualRoot<Marker> {
390    #[inline]
391    fn eq(&self, other: &crate::PathBoundary<Marker>) -> bool {
392        self.path() == other.path()
393    }
394}
395
396impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
397    #[inline]
398    fn eq(&self, other: &std::path::Path) -> bool {
399        // Compare as virtual root path (always "/")
400        // VirtualRoot represents the virtual "/" regardless of underlying system path
401        let other_str = other.to_string_lossy();
402
403        #[cfg(windows)]
404        let other_normalized = other_str.replace('\\', "/");
405        #[cfg(not(windows))]
406        let other_normalized = other_str.to_string();
407
408        let normalized_other = if other_normalized.starts_with('/') {
409            other_normalized
410        } else {
411            format!("/{}", other_normalized)
412        };
413
414        "/" == normalized_other
415    }
416}
417
418impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
419    #[inline]
420    fn eq(&self, other: &std::path::PathBuf) -> bool {
421        self.eq(other.as_path())
422    }
423}
424
425impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
426    #[inline]
427    fn eq(&self, other: &&std::path::Path) -> bool {
428        self.eq(*other)
429    }
430}
431
432impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
433    #[inline]
434    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
435        // Compare as virtual root path (always "/")
436        let other_str = other.to_string_lossy();
437
438        // Handle empty path specially - "/" is greater than ""
439        if other_str.is_empty() {
440            return Some(std::cmp::Ordering::Greater);
441        }
442
443        #[cfg(windows)]
444        let other_normalized = other_str.replace('\\', "/");
445        #[cfg(not(windows))]
446        let other_normalized = other_str.to_string();
447
448        let normalized_other = if other_normalized.starts_with('/') {
449            other_normalized
450        } else {
451            format!("/{}", other_normalized)
452        };
453
454        Some("/".cmp(&normalized_other))
455    }
456}
457
458impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
459    #[inline]
460    fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
461        self.partial_cmp(*other)
462    }
463}
464
465impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
466    #[inline]
467    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
468        self.partial_cmp(other.as_path())
469    }
470}
471
472impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
473    type Err = crate::StrictPathError;
474
475    /// Parse a VirtualRoot from a string path for universal ergonomics.
476    ///
477    /// Creates the directory if it doesn't exist, enabling seamless integration
478    /// with any string-parsing context (clap, config files, environment variables, etc.):
479    /// ```rust
480    /// # use strict_path::VirtualRoot;
481    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
482    /// let temp_dir = tempfile::tempdir()?;
483    /// let virtual_path = temp_dir.path().join("virtual_dir");
484    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
485    /// assert!(virtual_path.exists());
486    /// # Ok(())
487    /// # }
488    /// ```
489    #[inline]
490    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
491        Self::try_new_create(path)
492    }
493}