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}