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