strict_path/validator/path_boundary.rs
1// Content copied from original src/validator/restriction.rs
2use crate::error::StrictPathError;
3use crate::path::strict_path::StrictPath;
4use crate::validator::path_history::*;
5use crate::Result;
6
7use std::io::{Error as IoError, ErrorKind};
8use std::marker::PhantomData;
9use std::path::Path;
10use std::sync::Arc;
11
12#[cfg(feature = "tempfile")]
13use tempfile::TempDir;
14
15/// SUMMARY:
16/// Canonicalize a candidate path and enforce the `PathBoundary` boundary, returning a `StrictPath`.
17///
18/// PARAMETERS:
19/// - `path` (`AsRef<Path>`): Candidate path to validate (absolute or relative).
20/// - `restriction` (&`PathBoundary<Marker>`): Boundary to enforce during resolution.
21///
22/// RETURNS:
23/// - `Result<StrictPath<Marker>>`: Canonicalized path proven to be within `restriction`.
24///
25/// ERRORS:
26/// - `StrictPathError::PathResolutionError`: Canonicalization fails (I/O or resolution error).
27/// - `StrictPathError::PathEscapesBoundary`: Resolved path would escape the boundary.
28///
29/// EXAMPLE:
30/// ```rust
31/// # use strict_path::{PathBoundary, Result};
32/// # fn main() -> Result<()> {
33/// let boundary = PathBoundary::<()>::try_new_create("./sandbox")?;
34/// // Use the public API that exercises the same validation pipeline
35/// // as this internal helper.
36/// let file = boundary.strict_join("sub/file.txt")?;
37/// assert!(file.interop_path().to_string_lossy().contains("sandbox"));
38/// # Ok(())
39/// # }
40/// ```
41pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
42 path: impl AsRef<Path>,
43 restriction: &PathBoundary<Marker>,
44) -> Result<StrictPath<Marker>> {
45 let target_path = if path.as_ref().is_absolute() {
46 path.as_ref().to_path_buf()
47 } else {
48 restriction.path().join(path.as_ref())
49 };
50
51 let canonicalized = PathHistory::<Raw>::new(target_path).canonicalize()?;
52
53 let validated_path = canonicalized.boundary_check(&restriction.path)?;
54
55 Ok(StrictPath::new(
56 Arc::new(restriction.clone()),
57 validated_path,
58 ))
59}
60
61/// A path boundary that serves as the secure foundation for validated path operations.
62///
63/// SUMMARY:
64/// Represent the trusted filesystem boundary directory for all strict and virtual path
65/// operations. All `StrictPath`/`VirtualPath` values derived from a `PathBoundary` are
66/// guaranteed to remain within this boundary.
67///
68/// EXAMPLE:
69/// ```rust
70/// # use strict_path::PathBoundary;
71/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
72/// let boundary = PathBoundary::<()>::try_new_create("./data")?;
73/// let file = boundary.strict_join("logs/app.log")?;
74/// println!("{}", file.strictpath_display());
75/// # Ok(())
76/// # }
77/// ```
78pub struct PathBoundary<Marker = ()> {
79 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
80 #[cfg(feature = "tempfile")]
81 _temp_dir: Option<Arc<TempDir>>,
82 _marker: PhantomData<Marker>,
83}
84
85impl<Marker> Clone for PathBoundary<Marker> {
86 fn clone(&self) -> Self {
87 Self {
88 path: self.path.clone(),
89 #[cfg(feature = "tempfile")]
90 _temp_dir: self._temp_dir.clone(),
91 _marker: PhantomData,
92 }
93 }
94}
95
96impl<Marker> Eq for PathBoundary<Marker> {}
97
98impl<M1, M2> PartialEq<PathBoundary<M2>> for PathBoundary<M1> {
99 #[inline]
100 fn eq(&self, other: &PathBoundary<M2>) -> bool {
101 self.path() == other.path()
102 }
103}
104
105impl<Marker> std::hash::Hash for PathBoundary<Marker> {
106 #[inline]
107 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
108 self.path().hash(state);
109 }
110}
111
112impl<Marker> PartialOrd for PathBoundary<Marker> {
113 #[inline]
114 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
115 Some(self.cmp(other))
116 }
117}
118
119impl<Marker> Ord for PathBoundary<Marker> {
120 #[inline]
121 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
122 self.path().cmp(other.path())
123 }
124}
125
126#[cfg(feature = "virtual-path")]
127impl<M1, M2> PartialEq<crate::validator::virtual_root::VirtualRoot<M2>> for PathBoundary<M1> {
128 #[inline]
129 fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<M2>) -> bool {
130 self.path() == other.path()
131 }
132}
133
134impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
135 #[inline]
136 fn eq(&self, other: &Path) -> bool {
137 self.path() == other
138 }
139}
140
141impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
142 #[inline]
143 fn eq(&self, other: &std::path::PathBuf) -> bool {
144 self.eq(other.as_path())
145 }
146}
147
148impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
149 #[inline]
150 fn eq(&self, other: &&std::path::Path) -> bool {
151 self.eq(*other)
152 }
153}
154
155impl<Marker> PathBoundary<Marker> {
156 /// Private constructor that allows setting the temp_dir during construction
157 #[cfg(feature = "tempfile")]
158 fn new_with_temp_dir(
159 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
160 temp_dir: Option<Arc<TempDir>>,
161 ) -> Self {
162 Self {
163 path,
164 _temp_dir: temp_dir,
165 _marker: PhantomData,
166 }
167 }
168
169 /// Creates a new `PathBoundary` anchored at `restriction_path` (which must already exist and be a directory).
170 ///
171 /// SUMMARY:
172 /// Create a boundary anchored at an existing directory (must exist and be a directory).
173 ///
174 /// PARAMETERS:
175 /// - `restriction_path` (`AsRef<Path>`): Existing directory to anchor the boundary.
176 ///
177 /// RETURNS:
178 /// - `Result<PathBoundary<Marker>>`: New boundary whose directory is canonicalized and verified to exist.
179 ///
180 /// ERRORS:
181 /// - `StrictPathError::InvalidRestriction`: Boundary directory is missing, not a directory, or cannot be canonicalized.
182 ///
183 /// EXAMPLE:
184 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
185 /// ```rust
186 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
187 /// use strict_path::PathBoundary;
188 /// let tmp_dir = tempfile::tempdir()?;
189 /// let tmp_dir = PathBoundary::<()>::try_new(tmp_dir)?; // Clean variable shadowing
190 /// # Ok(())
191 /// # }
192 /// ```
193 #[inline]
194 pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
195 let restriction_path = restriction_path.as_ref();
196 let raw = PathHistory::<Raw>::new(restriction_path);
197
198 let canonicalized = raw.canonicalize()?;
199
200 let verified_exists = match canonicalized.verify_exists() {
201 Some(path) => path,
202 None => {
203 let io = IoError::new(
204 ErrorKind::NotFound,
205 "The specified PathBoundary path does not exist.",
206 );
207 return Err(StrictPathError::invalid_restriction(
208 restriction_path.to_path_buf(),
209 io,
210 ));
211 }
212 };
213
214 if !verified_exists.is_dir() {
215 let error = IoError::new(
216 ErrorKind::InvalidInput,
217 "The specified PathBoundary path exists but is not a directory.",
218 );
219 return Err(StrictPathError::invalid_restriction(
220 restriction_path.to_path_buf(),
221 error,
222 ));
223 }
224
225 #[cfg(feature = "tempfile")]
226 {
227 Ok(Self::new_with_temp_dir(Arc::new(verified_exists), None))
228 }
229 #[cfg(not(feature = "tempfile"))]
230 {
231 Ok(Self {
232 path: Arc::new(verified_exists),
233 _marker: PhantomData,
234 })
235 }
236 }
237
238 /// Creates the directory if missing, then constructs a new `PathBoundary`.
239 ///
240 /// SUMMARY:
241 /// Ensure the boundary directory exists (create if missing) and construct a new boundary.
242 ///
243 /// PARAMETERS:
244 /// - `boundary_dir` (`AsRef<Path>`): Directory to create if needed and use as the boundary directory.
245 ///
246 /// RETURNS:
247 /// - `Result<PathBoundary<Marker>>`: New boundary anchored at `boundary_dir`.
248 ///
249 /// ERRORS:
250 /// - `StrictPathError::InvalidRestriction`: Directory creation/canonicalization fails.
251 ///
252 /// EXAMPLE:
253 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
254 /// ```rust
255 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
256 /// use strict_path::PathBoundary;
257 /// let tmp_dir = tempfile::tempdir()?;
258 /// let tmp_dir = PathBoundary::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
259 /// # Ok(())
260 /// # }
261 /// ```
262 pub fn try_new_create<P: AsRef<Path>>(boundary_dir: P) -> Result<Self> {
263 let boundary_path = boundary_dir.as_ref();
264 if !boundary_path.exists() {
265 std::fs::create_dir_all(boundary_path).map_err(|e| {
266 StrictPathError::invalid_restriction(boundary_path.to_path_buf(), e)
267 })?;
268 }
269 Self::try_new(boundary_path)
270 }
271
272 /// SUMMARY:
273 /// Join a candidate path to the boundary and return a validated `StrictPath`.
274 ///
275 /// PARAMETERS:
276 /// - `candidate_path` (`AsRef<Path>`): Absolute or relative path to validate within this boundary.
277 ///
278 /// RETURNS:
279 /// - `Result<StrictPath<Marker>>`: Canonicalized, boundary-checked path.
280 ///
281 /// ERRORS:
282 /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
283 #[inline]
284 pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
285 canonicalize_and_enforce_restriction_boundary(candidate_path, self)
286 }
287
288 /// SUMMARY:
289 /// Consume this boundary and substitute a new marker type.
290 ///
291 /// DETAILS:
292 /// Mirrors [`crate::StrictPath::change_marker`] and [`crate::VirtualPath::change_marker`], enabling
293 /// marker transformation after authorization checks. Use this when encoding proven
294 /// authorization into the type system (e.g., after validating a user's permissions).
295 /// The consumption makes marker changes explicit during code review.
296 ///
297 /// PARAMETERS:
298 /// - `NewMarker` (type parameter): Marker to associate with the boundary.
299 ///
300 /// RETURNS:
301 /// - `PathBoundary<NewMarker>`: Same underlying boundary, rebranded with `NewMarker`.
302 ///
303 /// EXAMPLE:
304 /// ```rust
305 /// # use strict_path::PathBoundary;
306 /// # let boundary_dir = std::env::temp_dir().join("change-marker-example");
307 /// # std::fs::create_dir_all(&boundary_dir)?;
308 /// struct ReadOnly;
309 /// struct ReadWrite;
310 ///
311 /// let read_boundary: PathBoundary<ReadOnly> = PathBoundary::try_new(&boundary_dir)?;
312 ///
313 /// // After authorization check...
314 /// let write_boundary: PathBoundary<ReadWrite> = read_boundary.change_marker();
315 /// # std::fs::remove_dir_all(&boundary_dir)?;
316 /// # Ok::<_, Box<dyn std::error::Error>>(())
317 /// ```
318 #[inline]
319 pub fn change_marker<NewMarker>(self) -> PathBoundary<NewMarker> {
320 PathBoundary {
321 path: self.path,
322 #[cfg(feature = "tempfile")]
323 _temp_dir: self._temp_dir,
324 _marker: PhantomData,
325 }
326 }
327
328 /// SUMMARY:
329 /// Consume this boundary and return a `StrictPath` anchored at the boundary directory.
330 ///
331 /// PARAMETERS:
332 /// - _none_
333 ///
334 /// RETURNS:
335 /// - `Result<StrictPath<Marker>>`: Strict path for the canonicalized boundary directory.
336 ///
337 /// ERRORS:
338 /// - `StrictPathError::PathResolutionError`: Canonicalization fails (directory removed or inaccessible).
339 /// - `StrictPathError::PathEscapesBoundary`: Guard against race conditions that move the directory.
340 ///
341 /// EXAMPLE:
342 /// ```rust
343 /// # use strict_path::{PathBoundary, StrictPath};
344 /// # let boundary_dir = std::env::temp_dir().join("into-strictpath-example");
345 /// # std::fs::create_dir_all(&boundary_dir)?;
346 /// let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
347 /// let boundary_path: StrictPath = boundary.into_strictpath()?;
348 /// assert!(boundary_path.is_dir());
349 /// # std::fs::remove_dir_all(&boundary_dir)?;
350 /// # Ok::<_, Box<dyn std::error::Error>>(())
351 /// ```
352 #[inline]
353 pub fn into_strictpath(self) -> Result<StrictPath<Marker>> {
354 let root_history = self.path.clone();
355 let validated = PathHistory::<Raw>::new(root_history.as_ref().to_path_buf())
356 .canonicalize()?
357 .boundary_check(root_history.as_ref())?;
358 Ok(StrictPath::new(Arc::new(self), validated))
359 }
360
361 /// Returns the canonicalized PathBoundary directory path. Kept crate-private to avoid leaking raw path.
362 #[inline]
363 pub(crate) fn path(&self) -> &Path {
364 self.path.as_ref()
365 }
366
367 /// Internal: returns the canonicalized PathHistory of the PathBoundary directory for boundary checks.
368 #[cfg(feature = "virtual-path")]
369 #[inline]
370 pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
371 &self.path
372 }
373
374 /// Returns true if the PathBoundary directory exists.
375 ///
376 /// This is always true for a constructed PathBoundary, but we query the filesystem for robustness.
377 #[inline]
378 pub fn exists(&self) -> bool {
379 self.path.exists()
380 }
381
382 /// SUMMARY:
383 /// Return the boundary directory path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop (no allocation).
384 #[inline]
385 pub fn interop_path(&self) -> &std::ffi::OsStr {
386 self.path.as_os_str()
387 }
388
389 /// Returns a Display wrapper that shows the PathBoundary directory system path.
390 #[inline]
391 pub fn strictpath_display(&self) -> std::path::Display<'_> {
392 self.path().display()
393 }
394
395 /// Internal helper: exposes the tempfile RAII handle so `VirtualRoot` constructors can mirror cleanup semantics when constructed from temporary directories.
396 #[cfg(feature = "tempfile")]
397 #[inline]
398 pub(crate) fn temp_dir_arc(&self) -> Option<Arc<TempDir>> {
399 self._temp_dir.clone()
400 }
401
402 /// SUMMARY:
403 /// Return filesystem metadata for the boundary directory.
404 #[inline]
405 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
406 std::fs::metadata(self.path())
407 }
408
409 /// SUMMARY:
410 /// Create a symbolic link at `link_path` pointing to this boundary's directory.
411 ///
412 /// PARAMETERS:
413 /// - `link_path` (&`StrictPath<Marker>`): Destination for the symlink, within the same boundary.
414 ///
415 /// RETURNS:
416 /// - `io::Result<()>`: Mirrors std semantics.
417 pub fn strict_symlink(
418 &self,
419 link_path: &crate::path::strict_path::StrictPath<Marker>,
420 ) -> std::io::Result<()> {
421 let root = self
422 .clone()
423 .into_strictpath()
424 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
425
426 root.strict_symlink(link_path)
427 }
428
429 /// SUMMARY:
430 /// Create a hard link at `link_path` pointing to this boundary's directory.
431 ///
432 /// PARAMETERS and RETURNS mirror `strict_symlink`.
433 pub fn strict_hard_link(
434 &self,
435 link_path: &crate::path::strict_path::StrictPath<Marker>,
436 ) -> std::io::Result<()> {
437 let root = self
438 .clone()
439 .into_strictpath()
440 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
441
442 root.strict_hard_link(link_path)
443 }
444
445 /// SUMMARY:
446 /// Read directory entries under the boundary directory (discovery only).
447 #[inline]
448 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
449 std::fs::read_dir(self.path())
450 }
451
452 /// SUMMARY:
453 /// Remove the boundary directory (non-recursive); fails if not empty.
454 #[inline]
455 pub fn remove_dir(&self) -> std::io::Result<()> {
456 std::fs::remove_dir(self.path())
457 }
458
459 /// SUMMARY:
460 /// Recursively remove the boundary directory and its contents.
461 #[inline]
462 pub fn remove_dir_all(&self) -> std::io::Result<()> {
463 std::fs::remove_dir_all(self.path())
464 }
465
466 /// SUMMARY:
467 /// Convert this boundary into a `VirtualRoot` for virtual path operations.
468 #[cfg(feature = "virtual-path")]
469 #[inline]
470 pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
471 crate::VirtualRoot {
472 root: self,
473 #[cfg(feature = "tempfile")]
474 _temp_dir: None,
475 _marker: PhantomData,
476 }
477 }
478
479 // Note: Do not add new crate-private helpers unless necessary; use existing flows.
480
481 // OS Standard Directory Constructors
482 //
483 // These constructors provide secure access to operating system standard directories
484 // following platform-specific conventions (XDG on Linux, Known Folder API on Windows,
485 // Apple Standard Directories on macOS). Each creates an app-specific subdirectory
486 // and enforces path boundaries for secure file operations.
487
488 /// Creates a PathBoundary in the OS standard config directory for the given application.
489 ///
490 /// **Cross-Platform Behavior:**
491 /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
492 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
493 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
494 ///
495 /// Respects environment variables like `$XDG_CONFIG_HOME` on Linux systems.
496 #[cfg(feature = "dirs")]
497 pub fn try_new_os_config(app_name: &str) -> Result<Self> {
498 let config_dir = dirs::config_dir()
499 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
500 restriction: "os-config".into(),
501 source: std::io::Error::new(
502 std::io::ErrorKind::NotFound,
503 "OS config directory not available",
504 ),
505 })?
506 .join(app_name);
507 Self::try_new_create(config_dir)
508 }
509
510 /// Creates a PathBoundary in the OS standard data directory for the given application.
511 ///
512 /// **Cross-Platform Behavior:**
513 /// - **Linux**: `~/.local/share/{app_name}` (XDG Base Directory Specification)
514 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
515 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
516 ///
517 /// Respects environment variables like `$XDG_DATA_HOME` on Linux systems.
518 #[cfg(feature = "dirs")]
519 pub fn try_new_os_data(app_name: &str) -> Result<Self> {
520 let data_dir = dirs::data_dir()
521 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
522 restriction: "os-data".into(),
523 source: std::io::Error::new(
524 std::io::ErrorKind::NotFound,
525 "OS data directory not available",
526 ),
527 })?
528 .join(app_name);
529 Self::try_new_create(data_dir)
530 }
531
532 /// Creates a PathBoundary in the OS standard cache directory for the given application.
533 ///
534 /// **Cross-Platform Behavior:**
535 /// - **Linux**: `~/.cache/{app_name}` (XDG Base Directory Specification)
536 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
537 /// - **macOS**: `~/Library/Caches/{app_name}` (Apple Standard Directories)
538 ///
539 /// Respects environment variables like `$XDG_CACHE_HOME` on Linux systems.
540 #[cfg(feature = "dirs")]
541 pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
542 let cache_dir = dirs::cache_dir()
543 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
544 restriction: "os-cache".into(),
545 source: std::io::Error::new(
546 std::io::ErrorKind::NotFound,
547 "OS cache directory not available",
548 ),
549 })?
550 .join(app_name);
551 Self::try_new_create(cache_dir)
552 }
553
554 /// Creates a PathBoundary in the OS local config directory (non-roaming on Windows).
555 ///
556 /// **Cross-Platform Behavior:**
557 /// - **Linux**: `~/.config/{app_name}` (same as config_dir)
558 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
559 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as config_dir)
560 #[cfg(feature = "dirs")]
561 pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
562 let config_dir = dirs::config_local_dir()
563 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
564 restriction: "os-config-local".into(),
565 source: std::io::Error::new(
566 std::io::ErrorKind::NotFound,
567 "OS local config directory not available",
568 ),
569 })?
570 .join(app_name);
571 Self::try_new_create(config_dir)
572 }
573
574 /// Creates a PathBoundary in the OS local data directory.
575 ///
576 /// **Cross-Platform Behavior:**
577 /// - **Linux**: `~/.local/share/{app_name}` (same as data_dir)
578 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
579 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as data_dir)
580 #[cfg(feature = "dirs")]
581 pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
582 let data_dir = dirs::data_local_dir()
583 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
584 restriction: "os-data-local".into(),
585 source: std::io::Error::new(
586 std::io::ErrorKind::NotFound,
587 "OS local data directory not available",
588 ),
589 })?
590 .join(app_name);
591 Self::try_new_create(data_dir)
592 }
593
594 /// Creates a PathBoundary in the user's home directory.
595 ///
596 /// **Cross-Platform Behavior:**
597 /// - **Linux**: `$HOME`
598 /// - **Windows**: `%USERPROFILE%` (e.g., `C:\Users\Username`)
599 /// - **macOS**: `$HOME`
600 #[cfg(feature = "dirs")]
601 pub fn try_new_os_home() -> Result<Self> {
602 let home_dir =
603 dirs::home_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
604 restriction: "os-home".into(),
605 source: std::io::Error::new(
606 std::io::ErrorKind::NotFound,
607 "OS home directory not available",
608 ),
609 })?;
610 Self::try_new(home_dir)
611 }
612
613 /// Creates a PathBoundary in the user's desktop directory.
614 ///
615 /// **Cross-Platform Behavior:**
616 /// - **Linux**: `$HOME/Desktop` or XDG_DESKTOP_DIR
617 /// - **Windows**: `%USERPROFILE%\Desktop`
618 /// - **macOS**: `$HOME/Desktop`
619 #[cfg(feature = "dirs")]
620 pub fn try_new_os_desktop() -> Result<Self> {
621 let desktop_dir =
622 dirs::desktop_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
623 restriction: "os-desktop".into(),
624 source: std::io::Error::new(
625 std::io::ErrorKind::NotFound,
626 "OS desktop directory not available",
627 ),
628 })?;
629 Self::try_new(desktop_dir)
630 }
631
632 /// Creates a PathBoundary in the user's documents directory.
633 ///
634 /// **Cross-Platform Behavior:**
635 /// - **Linux**: `$HOME/Documents` or XDG_DOCUMENTS_DIR
636 /// - **Windows**: `%USERPROFILE%\Documents`
637 /// - **macOS**: `$HOME/Documents`
638 #[cfg(feature = "dirs")]
639 pub fn try_new_os_documents() -> Result<Self> {
640 let docs_dir =
641 dirs::document_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
642 restriction: "os-documents".into(),
643 source: std::io::Error::new(
644 std::io::ErrorKind::NotFound,
645 "OS documents directory not available",
646 ),
647 })?;
648 Self::try_new(docs_dir)
649 }
650
651 /// Creates a PathBoundary in the user's downloads directory.
652 ///
653 /// **Cross-Platform Behavior:**
654 /// - **Linux**: `$HOME/Downloads` or XDG_DOWNLOAD_DIR
655 /// - **Windows**: `%USERPROFILE%\Downloads`
656 /// - **macOS**: `$HOME/Downloads`
657 #[cfg(feature = "dirs")]
658 pub fn try_new_os_downloads() -> Result<Self> {
659 let downloads_dir =
660 dirs::download_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
661 restriction: "os-downloads".into(),
662 source: std::io::Error::new(
663 std::io::ErrorKind::NotFound,
664 "OS downloads directory not available",
665 ),
666 })?;
667 Self::try_new(downloads_dir)
668 }
669
670 /// Creates a PathBoundary in the user's pictures directory.
671 ///
672 /// **Cross-Platform Behavior:**
673 /// - **Linux**: `$HOME/Pictures` or XDG_PICTURES_DIR
674 /// - **Windows**: `%USERPROFILE%\Pictures`
675 /// - **macOS**: `$HOME/Pictures`
676 #[cfg(feature = "dirs")]
677 pub fn try_new_os_pictures() -> Result<Self> {
678 let pictures_dir =
679 dirs::picture_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
680 restriction: "os-pictures".into(),
681 source: std::io::Error::new(
682 std::io::ErrorKind::NotFound,
683 "OS pictures directory not available",
684 ),
685 })?;
686 Self::try_new(pictures_dir)
687 }
688
689 /// Creates a PathBoundary in the user's music/audio directory.
690 ///
691 /// **Cross-Platform Behavior:**
692 /// - **Linux**: `$HOME/Music` or XDG_MUSIC_DIR
693 /// - **Windows**: `%USERPROFILE%\Music`
694 /// - **macOS**: `$HOME/Music`
695 #[cfg(feature = "dirs")]
696 pub fn try_new_os_audio() -> Result<Self> {
697 let audio_dir =
698 dirs::audio_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
699 restriction: "os-audio".into(),
700 source: std::io::Error::new(
701 std::io::ErrorKind::NotFound,
702 "OS audio directory not available",
703 ),
704 })?;
705 Self::try_new(audio_dir)
706 }
707
708 /// Creates a PathBoundary in the user's videos directory.
709 ///
710 /// **Cross-Platform Behavior:**
711 /// - **Linux**: `$HOME/Videos` or XDG_VIDEOS_DIR
712 /// - **Windows**: `%USERPROFILE%\Videos`
713 /// - **macOS**: `$HOME/Movies`
714 #[cfg(feature = "dirs")]
715 pub fn try_new_os_videos() -> Result<Self> {
716 let videos_dir =
717 dirs::video_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
718 restriction: "os-videos".into(),
719 source: std::io::Error::new(
720 std::io::ErrorKind::NotFound,
721 "OS videos directory not available",
722 ),
723 })?;
724 Self::try_new(videos_dir)
725 }
726
727 /// Creates a PathBoundary in the OS executable directory (Linux only).
728 ///
729 /// **Platform Availability:**
730 /// - **Linux**: `~/.local/bin` or $XDG_BIN_HOME
731 /// - **Windows**: Returns error (not available)
732 /// - **macOS**: Returns error (not available)
733 #[cfg(feature = "dirs")]
734 pub fn try_new_os_executables() -> Result<Self> {
735 let exec_dir =
736 dirs::executable_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
737 restriction: "os-executables".into(),
738 source: std::io::Error::new(
739 std::io::ErrorKind::NotFound,
740 "OS executables directory not available on this platform",
741 ),
742 })?;
743 Self::try_new(exec_dir)
744 }
745
746 /// Creates a PathBoundary in the OS runtime directory (Linux only).
747 ///
748 /// **Platform Availability:**
749 /// - **Linux**: `$XDG_RUNTIME_DIR` (session-specific, user-only access)
750 /// - **Windows**: Returns error (not available)
751 /// - **macOS**: Returns error (not available)
752 #[cfg(feature = "dirs")]
753 pub fn try_new_os_runtime() -> Result<Self> {
754 let runtime_dir =
755 dirs::runtime_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
756 restriction: "os-runtime".into(),
757 source: std::io::Error::new(
758 std::io::ErrorKind::NotFound,
759 "OS runtime directory not available on this platform",
760 ),
761 })?;
762 Self::try_new(runtime_dir)
763 }
764
765 /// Creates a PathBoundary in the OS state directory (Linux only).
766 ///
767 /// **Platform Availability:**
768 /// - **Linux**: `~/.local/state/{app_name}` or $XDG_STATE_HOME/{app_name}
769 /// - **Windows**: Returns error (not available)
770 /// - **macOS**: Returns error (not available)
771 #[cfg(feature = "dirs")]
772 pub fn try_new_os_state(app_name: &str) -> Result<Self> {
773 let state_dir = dirs::state_dir()
774 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
775 restriction: "os-state".into(),
776 source: std::io::Error::new(
777 std::io::ErrorKind::NotFound,
778 "OS state directory not available on this platform",
779 ),
780 })?
781 .join(app_name);
782 Self::try_new_create(state_dir)
783 }
784
785 /// Creates a PathBoundary in a unique temporary directory with RAII cleanup.
786 ///
787 /// Returns a `StrictPath` pointing to the temporary boundary directory. The
788 /// directory will be automatically cleaned up when the `StrictPath` is dropped.
789 ///
790 /// # Example
791 /// ```
792 /// # #[cfg(feature = "tempfile")] {
793 /// use strict_path::PathBoundary;
794 ///
795 /// // Get a validated temp directory path directly
796 /// let temp_boundary = PathBoundary::<()>::try_new_temp()?;
797 /// let user_input = "uploads/document.pdf";
798 /// let validated_path = temp_boundary.strict_join(user_input)?; // Returns StrictPath
799 /// // Ensure parent directories exist before writing
800 /// validated_path.create_parent_dir_all()?;
801 /// validated_path.write(b"content")?; // Prefer strict-path helpers over std::fs
802 /// // temp_boundary is dropped here, directory gets cleaned up automatically
803 /// # }
804 /// # Ok::<(), Box<dyn std::error::Error>>(())
805 /// ```
806 #[cfg(feature = "tempfile")]
807 pub fn try_new_temp() -> Result<Self> {
808 let temp_dir =
809 tempfile::tempdir().map_err(|e| crate::StrictPathError::InvalidRestriction {
810 restriction: "temp".into(),
811 source: e,
812 })?;
813
814 let temp_path = temp_dir.path();
815 let raw = PathHistory::<Raw>::new(temp_path);
816 let canonicalized = raw.canonicalize()?;
817 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
818 crate::StrictPathError::InvalidRestriction {
819 restriction: "temp".into(),
820 source: std::io::Error::new(
821 std::io::ErrorKind::NotFound,
822 "Temp directory verification failed",
823 ),
824 }
825 })?;
826
827 Ok(Self::new_with_temp_dir(
828 Arc::new(verified_exists),
829 Some(Arc::new(temp_dir)),
830 ))
831 }
832
833 /// Creates a PathBoundary in a temporary directory with a custom prefix and RAII cleanup.
834 ///
835 /// Returns a `StrictPath` pointing to the prefixed temporary boundary directory. The
836 /// directory will be automatically cleaned up when the `StrictPath` is dropped.
837 ///
838 /// # Example
839 /// ```
840 /// # #[cfg(feature = "tempfile")] {
841 /// use strict_path::PathBoundary;
842 ///
843 /// // Get a validated temp directory path with session prefix
844 /// let upload_boundary = PathBoundary::<()>::try_new_temp_with_prefix("upload_batch")?;
845 /// let user_file = upload_boundary.strict_join("user_document.pdf")?; // Validate path
846 /// // Process validated path with direct filesystem operations
847 /// // upload_boundary is dropped here, directory gets cleaned up automatically
848 /// # }
849 /// # Ok::<(), Box<dyn std::error::Error>>(())
850 /// ```
851 #[cfg(feature = "tempfile")]
852 pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
853 let temp_dir = tempfile::Builder::new()
854 .prefix(prefix)
855 .tempdir()
856 .map_err(|e| crate::StrictPathError::InvalidRestriction {
857 restriction: "temp".into(),
858 source: e,
859 })?;
860
861 let temp_path = temp_dir.path();
862 let raw = PathHistory::<Raw>::new(temp_path);
863 let canonicalized = raw.canonicalize()?;
864 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
865 crate::StrictPathError::InvalidRestriction {
866 restriction: "temp".into(),
867 source: std::io::Error::new(
868 std::io::ErrorKind::NotFound,
869 "Temp directory verification failed",
870 ),
871 }
872 })?;
873
874 Ok(Self::new_with_temp_dir(
875 Arc::new(verified_exists),
876 Some(Arc::new(temp_dir)),
877 ))
878 }
879
880 /// SUMMARY:
881 /// Create a boundary using `app-path` semantics (portable app-relative directory) with optional env override.
882 ///
883 /// PARAMETERS:
884 /// - `subdir` (`AsRef<Path>`): Subdirectory path relative to the executable (or override directory).
885 /// - `env_override` (Option<&str>): Optional environment variable name; when present and set,
886 /// its value is used as the base directory instead of the executable directory.
887 ///
888 /// RETURNS:
889 /// - `Result<PathBoundary<Marker>>`: Created/validated boundary at the resolved app-path location.
890 ///
891 /// ERRORS:
892 /// - `StrictPathError::InvalidRestriction`: If resolution fails or directory cannot be created/validated.
893 ///
894 /// EXAMPLE:
895 /// ```
896 /// # #[cfg(feature = "app-path")] {
897 /// use strict_path::PathBoundary;
898 ///
899 /// // Creates ./config/ relative to executable
900 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", None)?;
901 ///
902 /// // With environment override (checks MYAPP_CONFIG_DIR first)
903 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", Some("MYAPP_CONFIG_DIR"))?;
904 /// # }
905 /// # Ok::<(), Box<dyn std::error::Error>>(())
906 /// ```
907 #[cfg(feature = "app-path")]
908 pub fn try_new_app_path<P: AsRef<std::path::Path>>(
909 subdir: P,
910 env_override: Option<&str>,
911 ) -> Result<Self> {
912 let subdir_path = subdir.as_ref();
913 // Resolve the override environment variable name (if provided) to its value.
914 // app-path expects the override PATH value, not the variable name.
915 let override_value: Option<String> = env_override.and_then(|key| std::env::var(key).ok());
916 let app_path = app_path::AppPath::try_with_override(subdir_path, override_value.as_deref())
917 .map_err(|e| crate::StrictPathError::InvalidRestriction {
918 restriction: format!("app-path: {}", subdir_path.display()).into(),
919 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
920 })?;
921
922 Self::try_new_create(app_path)
923 }
924
925 /// SUMMARY:
926 /// Create a boundary using `app-path`, always consulting a specific environment variable first.
927 ///
928 /// PARAMETERS:
929 /// - `subdir` (`AsRef<Path>`): Subdirectory used with `app-path` resolution.
930 /// - `env_override` (&str): Environment variable name to check for a base directory.
931 ///
932 /// RETURNS:
933 /// - `Result<PathBoundary<Marker>>`: New boundary anchored using `app-path` semantics.
934 ///
935 /// ERRORS:
936 /// - `StrictPathError::InvalidRestriction`: If resolution fails or the directory can't be created/validated.
937 #[cfg(feature = "app-path")]
938 pub fn try_new_app_path_with_env<P: AsRef<std::path::Path>>(
939 subdir: P,
940 env_override: &str,
941 ) -> Result<Self> {
942 let subdir_path = subdir.as_ref();
943 Self::try_new_app_path(subdir_path, Some(env_override))
944 }
945}
946
947impl<Marker> AsRef<Path> for PathBoundary<Marker> {
948 #[inline]
949 fn as_ref(&self) -> &Path {
950 // PathHistory implements AsRef<Path>, so forward to it
951 self.path.as_ref()
952 }
953}
954
955impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
956 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
957 f.debug_struct("PathBoundary")
958 .field("path", &self.path.as_ref())
959 .field("marker", &std::any::type_name::<Marker>())
960 .finish()
961 }
962}
963
964impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
965 type Err = crate::StrictPathError;
966
967 /// Parse a PathBoundary from a string path for universal ergonomics.
968 ///
969 /// Creates the directory if it doesn't exist, enabling seamless integration
970 /// with any string-parsing context (clap, config files, environment variables, etc.):
971 /// ```rust
972 /// # use strict_path::PathBoundary;
973 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
974 /// let temp_dir = tempfile::tempdir()?;
975 /// let safe_path = temp_dir.path().join("safe_dir");
976 /// let boundary: PathBoundary<()> = safe_path.to_string_lossy().parse()?;
977 /// assert!(safe_path.exists());
978 /// # Ok(())
979 /// # }
980 /// ```
981 #[inline]
982 fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
983 Self::try_new_create(path)
984 }
985}
986//