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//