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