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 = "tempdir")]
9use std::sync::Arc;
10
11// keep feature-gated TempDir RAII field using Arc from std::sync
12#[cfg(feature = "tempdir")]
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 = "tempdir")]
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 = "tempdir")]
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 = "tempdir")]
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    // Convenience constructors for system and temporary directories
132
133    /// Creates a virtual root in the user's config directory for the given application.
134    ///
135    /// Creates `~/.config/{app_name}` on Linux/macOS or `%APPDATA%\{app_name}` on Windows.
136    /// The application directory is created if it doesn't exist.
137    /// Provides a sandboxed config environment where applications use standard paths.
138    ///
139    /// # Example
140    /// ```
141    /// # #[cfg(feature = "dirs")] {
142    /// use strict_path::VirtualRoot;
143    ///
144    /// // Sandbox for app config - app sees normal paths
145    /// let config_root: VirtualRoot = VirtualRoot::try_new_config("myapp")?;
146    /// let settings = config_root.virtual_join("settings.toml")?;    // VirtualPath
147    /// let themes = config_root.virtual_join("themes/dark.css")?;    // App sees normal structure
148    /// // Application code doesn't know it's sandboxed in user config dir
149    /// # }
150    /// # Ok::<(), Box<dyn std::error::Error>>(())
151    /// ```
152    #[cfg(feature = "dirs")]
153    pub fn try_new_config(app_name: &str) -> Result<Self> {
154        let root = crate::PathBoundary::try_new_config(app_name)?;
155        Ok(Self {
156            root,
157            #[cfg(feature = "tempdir")]
158            _temp_dir: None,
159            _marker: PhantomData,
160        })
161    }
162
163    /// Creates a virtual root in the user's data directory for the given application.
164    ///
165    /// Creates `~/.local/share/{app_name}` on Linux, `~/Library/Application Support/{app_name}` on macOS,
166    /// or `%APPDATA%\{app_name}` on Windows.
167    /// The application directory is created if it doesn't exist.
168    /// Provides a sandboxed data environment for application storage.
169    ///
170    /// # Example
171    /// ```
172    /// # #[cfg(feature = "dirs")] {
173    /// use strict_path::VirtualRoot;
174    ///
175    /// // Sandbox for app data - app sees familiar structure
176    /// let data_root: VirtualRoot = VirtualRoot::try_new_data("myapp")?;
177    /// let database = data_root.virtual_join("db/users.sqlite")?;   // VirtualPath
178    /// let exports = data_root.virtual_join("exports/report.csv")?; // Normal-looking paths
179    /// // App manages data without knowing actual location
180    /// # }
181    /// # Ok::<(), Box<dyn std::error::Error>>(())
182    /// ```
183    #[cfg(feature = "dirs")]
184    pub fn try_new_data(app_name: &str) -> Result<Self> {
185        let root = crate::PathBoundary::try_new_data(app_name)?;
186        Ok(Self {
187            root,
188            #[cfg(feature = "tempdir")]
189            _temp_dir: None,
190            _marker: PhantomData,
191        })
192    }
193
194    /// Creates a virtual root in the user's cache directory for the given application.
195    ///
196    /// Creates `~/.cache/{app_name}` on Linux, `~/Library/Caches/{app_name}` on macOS,
197    /// or `%LOCALAPPDATA%\{app_name}` on Windows.
198    /// The application directory is created if it doesn't exist.
199    #[cfg(feature = "dirs")]
200    pub fn try_new_cache(app_name: &str) -> Result<Self> {
201        let root = crate::PathBoundary::try_new_cache(app_name)?;
202        Ok(Self {
203            root,
204            #[cfg(feature = "tempdir")]
205            _temp_dir: None,
206            _marker: PhantomData,
207        })
208    }
209
210    /// Creates a virtual root using app-path for portable applications.
211    ///
212    /// Creates a directory relative to the executable location, with optional
213    /// environment variable override support for deployment flexibility.
214    #[cfg(feature = "app-path")]
215    pub fn try_new_app_path(subdir: &str, env_override: Option<&str>) -> Result<Self> {
216        let root = crate::PathBoundary::try_new_app_path(subdir, env_override)?;
217        Ok(Self {
218            root,
219            #[cfg(feature = "tempdir")]
220            _temp_dir: None,
221            _marker: PhantomData,
222        })
223    }
224}
225
226impl<Marker> std::fmt::Display for VirtualRoot<Marker> {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        write!(f, "{}", self.path().display())
229    }
230}
231
232impl<Marker> AsRef<Path> for VirtualRoot<Marker> {
233    fn as_ref(&self) -> &Path {
234        self.path()
235    }
236}
237
238impl<Marker> std::fmt::Debug for VirtualRoot<Marker> {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        f.debug_struct("VirtualRoot")
241            .field("root", &self.path())
242            .field("marker", &std::any::type_name::<Marker>())
243            .finish()
244    }
245}
246
247impl<Marker> PartialEq for VirtualRoot<Marker> {
248    #[inline]
249    fn eq(&self, other: &Self) -> bool {
250        self.path() == other.path()
251    }
252}
253
254impl<Marker> Eq for VirtualRoot<Marker> {}
255
256impl<Marker> std::hash::Hash for VirtualRoot<Marker> {
257    #[inline]
258    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
259        self.path().hash(state);
260    }
261}
262
263impl<Marker> PartialOrd for VirtualRoot<Marker> {
264    #[inline]
265    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
266        Some(self.cmp(other))
267    }
268}
269
270impl<Marker> Ord for VirtualRoot<Marker> {
271    #[inline]
272    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
273        self.path().cmp(other.path())
274    }
275}
276
277impl<Marker> PartialEq<crate::PathBoundary<Marker>> for VirtualRoot<Marker> {
278    #[inline]
279    fn eq(&self, other: &crate::PathBoundary<Marker>) -> bool {
280        self.path() == other.path()
281    }
282}
283
284impl<Marker> PartialEq<std::path::Path> for VirtualRoot<Marker> {
285    #[inline]
286    fn eq(&self, other: &std::path::Path) -> bool {
287        // Compare as virtual root path (always "/")
288        // VirtualRoot represents the virtual "/" regardless of underlying system path
289        let other_str = other.to_string_lossy();
290
291        #[cfg(windows)]
292        let other_normalized = other_str.replace('\\', "/");
293        #[cfg(not(windows))]
294        let other_normalized = other_str.to_string();
295
296        let normalized_other = if other_normalized.starts_with('/') {
297            other_normalized
298        } else {
299            format!("/{}", other_normalized)
300        };
301
302        "/" == normalized_other
303    }
304}
305
306impl<Marker> PartialEq<std::path::PathBuf> for VirtualRoot<Marker> {
307    #[inline]
308    fn eq(&self, other: &std::path::PathBuf) -> bool {
309        self.eq(other.as_path())
310    }
311}
312
313impl<Marker> PartialEq<&std::path::Path> for VirtualRoot<Marker> {
314    #[inline]
315    fn eq(&self, other: &&std::path::Path) -> bool {
316        self.eq(*other)
317    }
318}
319
320impl<Marker> PartialOrd<std::path::Path> for VirtualRoot<Marker> {
321    #[inline]
322    fn partial_cmp(&self, other: &std::path::Path) -> Option<std::cmp::Ordering> {
323        // Compare as virtual root path (always "/")
324        let other_str = other.to_string_lossy();
325
326        // Handle empty path specially - "/" is greater than ""
327        if other_str.is_empty() {
328            return Some(std::cmp::Ordering::Greater);
329        }
330
331        #[cfg(windows)]
332        let other_normalized = other_str.replace('\\', "/");
333        #[cfg(not(windows))]
334        let other_normalized = other_str.to_string();
335
336        let normalized_other = if other_normalized.starts_with('/') {
337            other_normalized
338        } else {
339            format!("/{}", other_normalized)
340        };
341
342        Some("/".cmp(&normalized_other))
343    }
344}
345
346impl<Marker> PartialOrd<&std::path::Path> for VirtualRoot<Marker> {
347    #[inline]
348    fn partial_cmp(&self, other: &&std::path::Path) -> Option<std::cmp::Ordering> {
349        self.partial_cmp(*other)
350    }
351}
352
353impl<Marker> PartialOrd<std::path::PathBuf> for VirtualRoot<Marker> {
354    #[inline]
355    fn partial_cmp(&self, other: &std::path::PathBuf) -> Option<std::cmp::Ordering> {
356        self.partial_cmp(other.as_path())
357    }
358}
359
360impl<Marker: Default> std::str::FromStr for VirtualRoot<Marker> {
361    type Err = crate::StrictPathError;
362
363    /// Parse a VirtualRoot from a string path for universal ergonomics.
364    ///
365    /// Creates the directory if it doesn't exist, enabling seamless integration
366    /// with any string-parsing context (clap, config files, environment variables, etc.):
367    /// ```rust
368    /// # use strict_path::VirtualRoot;
369    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
370    /// let temp_dir = tempfile::tempdir()?;
371    /// let virtual_path = temp_dir.path().join("virtual_dir");
372    /// let vroot: VirtualRoot<()> = virtual_path.to_string_lossy().parse()?;
373    /// assert!(virtual_path.exists());
374    /// # Ok(())
375    /// # }
376    /// ```
377    #[inline]
378    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
379        Self::try_new_create(path)
380    }
381}