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/// Canonicalize a candidate path and enforce the PathBoundary boundary, returning a `StrictPath`.
34///
35/// What this does:
36/// - Windows prefilter: rejects DOS 8.3 short-name segments (e.g., `PROGRA~1`) in relative inputs
37///   to avoid aliasing-based escapes before any filesystem calls.
38/// - Input interpretation: absolute inputs are validated as-is; relative inputs are joined under
39///   the PathBoundary root.
40/// - Resolution: canonicalizes the composed path, fully resolving `.`/`..`, symlinks/junctions,
41///   and platform prefixes.
42/// - Boundary enforcement: verifies the canonicalized result is strictly within the PathBoundary's
43///   canonicalized root; rejects any resolution that would escape the boundary.
44/// - Returns: a `StrictPath<Marker>` that borrows the PathBoundary and holds the validated system path.
45pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
46    path: impl AsRef<Path>,
47    restriction: &PathBoundary<Marker>,
48) -> Result<StrictPath<Marker>> {
49    #[cfg(windows)]
50    {
51        let original_user_path = path.as_ref().to_path_buf();
52        if !path.as_ref().is_absolute() {
53            let mut probe = restriction.path().to_path_buf();
54            for comp in path.as_ref().components() {
55                match comp {
56                    Component::CurDir | Component::ParentDir => continue,
57                    Component::RootDir | Component::Prefix(_) => continue,
58                    Component::Normal(name) => {
59                        if is_potential_83_short_name(name) {
60                            return Err(StrictPathError::windows_short_name(
61                                name.to_os_string(),
62                                original_user_path,
63                                probe.clone(),
64                            ));
65                        }
66                        probe.push(name);
67                    }
68                }
69            }
70        }
71    }
72
73    let target_path = if path.as_ref().is_absolute() {
74        path.as_ref().to_path_buf()
75    } else {
76        restriction.path().join(path.as_ref())
77    };
78
79    let validated_path = PathHistory::<Raw>::new(target_path)
80        .canonicalize()?
81        .boundary_check(&restriction.path)?;
82
83    Ok(StrictPath::new(
84        Arc::new(restriction.clone()),
85        validated_path,
86    ))
87}
88
89/// A path boundary that serves as the secure foundation for validated path operations.
90///
91/// `PathBoundary` represents the trusted starting point (like `/home/users/alice`) from which
92/// all path operations begin. When you call `path_boundary.strict_join("documents/file.txt")`,
93/// you're building outward from this secure boundary with validated path construction.
94pub struct PathBoundary<Marker = ()> {
95    path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
96    #[cfg(feature = "tempfile")]
97    _temp_dir: Option<Arc<TempDir>>,
98    _marker: PhantomData<Marker>,
99}
100
101impl<Marker> Clone for PathBoundary<Marker> {
102    fn clone(&self) -> Self {
103        Self {
104            path: self.path.clone(),
105            #[cfg(feature = "tempfile")]
106            _temp_dir: self._temp_dir.clone(),
107            _marker: PhantomData,
108        }
109    }
110}
111
112impl<Marker> PartialEq for PathBoundary<Marker> {
113    #[inline]
114    fn eq(&self, other: &Self) -> bool {
115        self.path() == other.path()
116    }
117}
118
119impl<Marker> Eq for PathBoundary<Marker> {}
120
121impl<Marker> std::hash::Hash for PathBoundary<Marker> {
122    #[inline]
123    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
124        self.path().hash(state);
125    }
126}
127
128impl<Marker> PartialOrd for PathBoundary<Marker> {
129    #[inline]
130    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
131        Some(self.cmp(other))
132    }
133}
134
135impl<Marker> Ord for PathBoundary<Marker> {
136    #[inline]
137    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
138        self.path().cmp(other.path())
139    }
140}
141
142impl<Marker> PartialEq<crate::validator::virtual_root::VirtualRoot<Marker>>
143    for PathBoundary<Marker>
144{
145    #[inline]
146    fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<Marker>) -> bool {
147        self.path() == other.path()
148    }
149}
150
151impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
152    #[inline]
153    fn eq(&self, other: &Path) -> bool {
154        self.path() == other
155    }
156}
157
158impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
159    #[inline]
160    fn eq(&self, other: &std::path::PathBuf) -> bool {
161        self.eq(other.as_path())
162    }
163}
164
165impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
166    #[inline]
167    fn eq(&self, other: &&std::path::Path) -> bool {
168        self.eq(*other)
169    }
170}
171
172impl<Marker> PathBoundary<Marker> {
173    /// Private constructor that allows setting the temp_dir during construction
174    #[cfg(feature = "tempfile")]
175    fn new_with_temp_dir(
176        path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
177        temp_dir: Option<Arc<TempDir>>,
178    ) -> Self {
179        Self {
180            path,
181            _temp_dir: temp_dir,
182            _marker: PhantomData,
183        }
184    }
185
186    /// Creates a new `PathBoundary` rooted at `restriction_path` (which must already exist and be a directory).
187    ///
188    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
189    /// ```rust
190    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
191    /// use strict_path::PathBoundary;
192    /// let tmp_dir = tempfile::tempdir()?;
193    /// let tmp_dir = PathBoundary::<()>::try_new(tmp_dir)?; // Clean variable shadowing
194    /// # Ok(())
195    /// # }
196    /// ```
197    #[inline]
198    pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
199        let restriction_path = restriction_path.as_ref();
200        let raw = PathHistory::<Raw>::new(restriction_path);
201
202        let canonicalized = raw.canonicalize()?;
203
204        let verified_exists = match canonicalized.verify_exists() {
205            Some(path) => path,
206            None => {
207                let io = IoError::new(
208                    ErrorKind::NotFound,
209                    "The specified PathBoundary path does not exist.",
210                );
211                return Err(StrictPathError::invalid_restriction(
212                    restriction_path.to_path_buf(),
213                    io,
214                ));
215            }
216        };
217
218        if !verified_exists.is_dir() {
219            let error = IoError::new(
220                ErrorKind::InvalidInput,
221                "The specified PathBoundary path exists but is not a directory.",
222            );
223            return Err(StrictPathError::invalid_restriction(
224                restriction_path.to_path_buf(),
225                error,
226            ));
227        }
228
229        #[cfg(feature = "tempfile")]
230        {
231            Ok(Self::new_with_temp_dir(Arc::new(verified_exists), None))
232        }
233        #[cfg(not(feature = "tempfile"))]
234        {
235            Ok(Self {
236                path: Arc::new(verified_exists),
237                _marker: PhantomData,
238            })
239        }
240    }
241
242    /// Creates the directory if missing, then constructs a new `PathBoundary`.
243    ///
244    /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
245    /// ```rust
246    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
247    /// use strict_path::PathBoundary;
248    /// let tmp_dir = tempfile::tempdir()?;
249    /// let tmp_dir = PathBoundary::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
250    /// # Ok(())
251    /// # }
252    /// ```
253    pub fn try_new_create<P: AsRef<Path>>(root: P) -> Result<Self> {
254        let root_path = root.as_ref();
255        if !root_path.exists() {
256            std::fs::create_dir_all(root_path)
257                .map_err(|e| StrictPathError::invalid_restriction(root_path.to_path_buf(), e))?;
258        }
259        Self::try_new(root_path)
260    }
261
262    /// Joins a path to this restrictor root and validates it remains within the restriction boundary.
263    ///
264    /// Accepts absolute or relative inputs; ensures the resulting path remains within the restriction.
265    #[inline]
266    pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
267        canonicalize_and_enforce_restriction_boundary(candidate_path, self)
268    }
269
270    /// Returns the canonicalized PathBoundary root path. Kept crate-private to avoid leaking raw path.
271    #[inline]
272    pub(crate) fn path(&self) -> &Path {
273        self.path.as_ref()
274    }
275
276    /// Internal: returns the canonicalized PathHistory of the PathBoundary root for boundary checks.
277    #[inline]
278    pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
279        &self.path
280    }
281
282    /// Returns true if the PathBoundary root exists.
283    ///
284    /// This is always true for a constructed PathBoundary, but we query the filesystem for robustness.
285    #[inline]
286    pub fn exists(&self) -> bool {
287        self.path.exists()
288    }
289
290    /// Returns the PathBoundary root path for interop with `AsRef<Path>` APIs.
291    ///
292    /// This provides allocation-free, OS-native string access to the PathBoundary root
293    /// for use with standard library APIs that accept `AsRef<Path>`.
294    #[inline]
295    pub fn interop_path(&self) -> &std::ffi::OsStr {
296        self.path.as_os_str()
297    }
298
299    /// Returns a Display wrapper that shows the PathBoundary root system path.
300    #[inline]
301    pub fn strictpath_display(&self) -> std::path::Display<'_> {
302        self.path().display()
303    }
304
305    /// Internal helper: exposes the tempfile RAII handle so `VirtualRoot` constructors can mirror cleanup semantics when constructed from temporary directories.
306    #[cfg(feature = "tempfile")]
307    #[inline]
308    pub(crate) fn temp_dir_arc(&self) -> Option<Arc<TempDir>> {
309        self._temp_dir.clone()
310    }
311
312    /// Returns filesystem metadata for the PathBoundary root path.
313    #[inline]
314    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
315        std::fs::metadata(self.path())
316    }
317
318    /// Creates a symbolic link at `link_path` that points to this PathBoundary's root.
319    pub fn strict_symlink(
320        &self,
321        link_path: &crate::path::strict_path::StrictPath<Marker>,
322    ) -> std::io::Result<()> {
323        let root = self
324            .strict_join("")
325            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
326
327        root.strict_symlink(link_path)
328    }
329
330    /// Creates a hard link at `link_path` that points to this PathBoundary's root.
331    pub fn strict_hard_link(
332        &self,
333        link_path: &crate::path::strict_path::StrictPath<Marker>,
334    ) -> std::io::Result<()> {
335        let root = self
336            .strict_join("")
337            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
338
339        root.strict_hard_link(link_path)
340    }
341
342    /// Reads the directory entries under this PathBoundary root (like `std::fs::read_dir`).
343    ///
344    /// This is intended for discovery. Prefer collecting entry names and joining via
345    /// `strict_join`/`virtual_join` before performing I/O.
346    #[inline]
347    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
348        std::fs::read_dir(self.path())
349    }
350
351    /// Removes this PathBoundary root directory (non-recursive).
352    ///
353    /// Equivalent to `std::fs::remove_dir(root)`. Fails if the directory is not empty.
354    #[inline]
355    pub fn remove_dir(&self) -> std::io::Result<()> {
356        std::fs::remove_dir(self.path())
357    }
358
359    /// Recursively removes this PathBoundary root directory and all its contents.
360    ///
361    /// Equivalent to `std::fs::remove_dir_all(root)`.
362    #[inline]
363    pub fn remove_dir_all(&self) -> std::io::Result<()> {
364        std::fs::remove_dir_all(self.path())
365    }
366
367    /// Converts this `PathBoundary` into a `VirtualRoot`.
368    ///
369    /// This creates a virtual root view of the PathBoundary, allowing virtual path operations
370    /// that treat the PathBoundary root as the virtual filesystem root "/".
371    #[inline]
372    pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
373        crate::VirtualRoot {
374            root: self,
375            #[cfg(feature = "tempfile")]
376            _temp_dir: None,
377            _marker: PhantomData,
378        }
379    }
380
381    // Note: Do not add new crate-private helpers unless necessary; use existing flows.
382
383    // OS Standard Directory Constructors
384    //
385    // These constructors provide secure access to operating system standard directories
386    // following platform-specific conventions (XDG on Linux, Known Folder API on Windows,
387    // Apple Standard Directories on macOS). Each creates an app-specific subdirectory
388    // and enforces path boundaries for secure file operations.
389
390    /// Creates a PathBoundary in the OS standard config directory for the given application.
391    ///
392    /// **Cross-Platform Behavior:**
393    /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
394    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
395    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
396    ///
397    /// Respects environment variables like `$XDG_CONFIG_HOME` on Linux systems.
398    #[cfg(feature = "dirs")]
399    pub fn try_new_os_config(app_name: &str) -> Result<Self> {
400        let config_dir = dirs::config_dir()
401            .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
402                restriction: "os-config".into(),
403                source: std::io::Error::new(
404                    std::io::ErrorKind::NotFound,
405                    "OS config directory not available",
406                ),
407            })?
408            .join(app_name);
409        Self::try_new_create(config_dir)
410    }
411
412    /// Creates a PathBoundary in the OS standard data directory for the given application.
413    ///
414    /// **Cross-Platform Behavior:**
415    /// - **Linux**: `~/.local/share/{app_name}` (XDG Base Directory Specification)
416    /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
417    /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
418    ///
419    /// Respects environment variables like `$XDG_DATA_HOME` on Linux systems.
420    #[cfg(feature = "dirs")]
421    pub fn try_new_os_data(app_name: &str) -> Result<Self> {
422        let data_dir = dirs::data_dir()
423            .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
424                restriction: "os-data".into(),
425                source: std::io::Error::new(
426                    std::io::ErrorKind::NotFound,
427                    "OS data directory not available",
428                ),
429            })?
430            .join(app_name);
431        Self::try_new_create(data_dir)
432    }
433
434    /// Creates a PathBoundary in the OS standard cache directory for the given application.
435    ///
436    /// **Cross-Platform Behavior:**
437    /// - **Linux**: `~/.cache/{app_name}` (XDG Base Directory Specification)
438    /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
439    /// - **macOS**: `~/Library/Caches/{app_name}` (Apple Standard Directories)
440    ///
441    /// Respects environment variables like `$XDG_CACHE_HOME` on Linux systems.
442    #[cfg(feature = "dirs")]
443    pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
444        let cache_dir = dirs::cache_dir()
445            .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
446                restriction: "os-cache".into(),
447                source: std::io::Error::new(
448                    std::io::ErrorKind::NotFound,
449                    "OS cache directory not available",
450                ),
451            })?
452            .join(app_name);
453        Self::try_new_create(cache_dir)
454    }
455
456    /// Creates a PathBoundary in the OS local config directory (non-roaming on Windows).
457    ///
458    /// **Cross-Platform Behavior:**
459    /// - **Linux**: `~/.config/{app_name}` (same as config_dir)
460    /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
461    /// - **macOS**: `~/Library/Application Support/{app_name}` (same as config_dir)
462    #[cfg(feature = "dirs")]
463    pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
464        let config_dir = dirs::config_local_dir()
465            .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
466                restriction: "os-config-local".into(),
467                source: std::io::Error::new(
468                    std::io::ErrorKind::NotFound,
469                    "OS local config directory not available",
470                ),
471            })?
472            .join(app_name);
473        Self::try_new_create(config_dir)
474    }
475
476    /// Creates a PathBoundary in the OS local data directory.
477    ///
478    /// **Cross-Platform Behavior:**
479    /// - **Linux**: `~/.local/share/{app_name}` (same as data_dir)
480    /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
481    /// - **macOS**: `~/Library/Application Support/{app_name}` (same as data_dir)
482    #[cfg(feature = "dirs")]
483    pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
484        let data_dir = dirs::data_local_dir()
485            .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
486                restriction: "os-data-local".into(),
487                source: std::io::Error::new(
488                    std::io::ErrorKind::NotFound,
489                    "OS local data directory not available",
490                ),
491            })?
492            .join(app_name);
493        Self::try_new_create(data_dir)
494    }
495
496    /// Creates a PathBoundary in the user's home directory.
497    ///
498    /// **Cross-Platform Behavior:**
499    /// - **Linux**: `$HOME`
500    /// - **Windows**: `%USERPROFILE%` (e.g., `C:\Users\Username`)
501    /// - **macOS**: `$HOME`
502    #[cfg(feature = "dirs")]
503    pub fn try_new_os_home() -> Result<Self> {
504        let home_dir =
505            dirs::home_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
506                restriction: "os-home".into(),
507                source: std::io::Error::new(
508                    std::io::ErrorKind::NotFound,
509                    "OS home directory not available",
510                ),
511            })?;
512        Self::try_new(home_dir)
513    }
514
515    /// Creates a PathBoundary in the user's desktop directory.
516    ///
517    /// **Cross-Platform Behavior:**
518    /// - **Linux**: `$HOME/Desktop` or XDG_DESKTOP_DIR
519    /// - **Windows**: `%USERPROFILE%\Desktop`
520    /// - **macOS**: `$HOME/Desktop`
521    #[cfg(feature = "dirs")]
522    pub fn try_new_os_desktop() -> Result<Self> {
523        let desktop_dir =
524            dirs::desktop_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
525                restriction: "os-desktop".into(),
526                source: std::io::Error::new(
527                    std::io::ErrorKind::NotFound,
528                    "OS desktop directory not available",
529                ),
530            })?;
531        Self::try_new(desktop_dir)
532    }
533
534    /// Creates a PathBoundary in the user's documents directory.
535    ///
536    /// **Cross-Platform Behavior:**
537    /// - **Linux**: `$HOME/Documents` or XDG_DOCUMENTS_DIR
538    /// - **Windows**: `%USERPROFILE%\Documents`
539    /// - **macOS**: `$HOME/Documents`
540    #[cfg(feature = "dirs")]
541    pub fn try_new_os_documents() -> Result<Self> {
542        let docs_dir =
543            dirs::document_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
544                restriction: "os-documents".into(),
545                source: std::io::Error::new(
546                    std::io::ErrorKind::NotFound,
547                    "OS documents directory not available",
548                ),
549            })?;
550        Self::try_new(docs_dir)
551    }
552
553    /// Creates a PathBoundary in the user's downloads directory.
554    ///
555    /// **Cross-Platform Behavior:**
556    /// - **Linux**: `$HOME/Downloads` or XDG_DOWNLOAD_DIR
557    /// - **Windows**: `%USERPROFILE%\Downloads`
558    /// - **macOS**: `$HOME/Downloads`
559    #[cfg(feature = "dirs")]
560    pub fn try_new_os_downloads() -> Result<Self> {
561        let downloads_dir =
562            dirs::download_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
563                restriction: "os-downloads".into(),
564                source: std::io::Error::new(
565                    std::io::ErrorKind::NotFound,
566                    "OS downloads directory not available",
567                ),
568            })?;
569        Self::try_new(downloads_dir)
570    }
571
572    /// Creates a PathBoundary in the user's pictures directory.
573    ///
574    /// **Cross-Platform Behavior:**
575    /// - **Linux**: `$HOME/Pictures` or XDG_PICTURES_DIR
576    /// - **Windows**: `%USERPROFILE%\Pictures`
577    /// - **macOS**: `$HOME/Pictures`
578    #[cfg(feature = "dirs")]
579    pub fn try_new_os_pictures() -> Result<Self> {
580        let pictures_dir =
581            dirs::picture_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
582                restriction: "os-pictures".into(),
583                source: std::io::Error::new(
584                    std::io::ErrorKind::NotFound,
585                    "OS pictures directory not available",
586                ),
587            })?;
588        Self::try_new(pictures_dir)
589    }
590
591    /// Creates a PathBoundary in the user's music/audio directory.
592    ///
593    /// **Cross-Platform Behavior:**
594    /// - **Linux**: `$HOME/Music` or XDG_MUSIC_DIR
595    /// - **Windows**: `%USERPROFILE%\Music`
596    /// - **macOS**: `$HOME/Music`
597    #[cfg(feature = "dirs")]
598    pub fn try_new_os_audio() -> Result<Self> {
599        let audio_dir =
600            dirs::audio_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
601                restriction: "os-audio".into(),
602                source: std::io::Error::new(
603                    std::io::ErrorKind::NotFound,
604                    "OS audio directory not available",
605                ),
606            })?;
607        Self::try_new(audio_dir)
608    }
609
610    /// Creates a PathBoundary in the user's videos directory.
611    ///
612    /// **Cross-Platform Behavior:**
613    /// - **Linux**: `$HOME/Videos` or XDG_VIDEOS_DIR  
614    /// - **Windows**: `%USERPROFILE%\Videos`
615    /// - **macOS**: `$HOME/Movies`
616    #[cfg(feature = "dirs")]
617    pub fn try_new_os_videos() -> Result<Self> {
618        let videos_dir =
619            dirs::video_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
620                restriction: "os-videos".into(),
621                source: std::io::Error::new(
622                    std::io::ErrorKind::NotFound,
623                    "OS videos directory not available",
624                ),
625            })?;
626        Self::try_new(videos_dir)
627    }
628
629    /// Creates a PathBoundary in the OS executable directory (Linux only).
630    ///
631    /// **Platform Availability:**
632    /// - **Linux**: `~/.local/bin` or $XDG_BIN_HOME
633    /// - **Windows**: Returns error (not available)
634    /// - **macOS**: Returns error (not available)
635    #[cfg(feature = "dirs")]
636    pub fn try_new_os_executables() -> Result<Self> {
637        let exec_dir =
638            dirs::executable_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
639                restriction: "os-executables".into(),
640                source: std::io::Error::new(
641                    std::io::ErrorKind::NotFound,
642                    "OS executables directory not available on this platform",
643                ),
644            })?;
645        Self::try_new(exec_dir)
646    }
647
648    /// Creates a PathBoundary in the OS runtime directory (Linux only).
649    ///
650    /// **Platform Availability:**
651    /// - **Linux**: `$XDG_RUNTIME_DIR` (session-specific, user-only access)
652    /// - **Windows**: Returns error (not available)
653    /// - **macOS**: Returns error (not available)
654    #[cfg(feature = "dirs")]
655    pub fn try_new_os_runtime() -> Result<Self> {
656        let runtime_dir =
657            dirs::runtime_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
658                restriction: "os-runtime".into(),
659                source: std::io::Error::new(
660                    std::io::ErrorKind::NotFound,
661                    "OS runtime directory not available on this platform",
662                ),
663            })?;
664        Self::try_new(runtime_dir)
665    }
666
667    /// Creates a PathBoundary in the OS state directory (Linux only).
668    ///
669    /// **Platform Availability:**
670    /// - **Linux**: `~/.local/state/{app_name}` or $XDG_STATE_HOME/{app_name}
671    /// - **Windows**: Returns error (not available)
672    /// - **macOS**: Returns error (not available)
673    #[cfg(feature = "dirs")]
674    pub fn try_new_os_state(app_name: &str) -> Result<Self> {
675        let state_dir = dirs::state_dir()
676            .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
677                restriction: "os-state".into(),
678                source: std::io::Error::new(
679                    std::io::ErrorKind::NotFound,
680                    "OS state directory not available on this platform",
681                ),
682            })?
683            .join(app_name);
684        Self::try_new_create(state_dir)
685    }
686
687    /// Creates a PathBoundary in a unique temporary directory with RAII cleanup.
688    ///
689    /// Returns a `StrictPath` pointing to the temp directory root. The directory
690    /// will be automatically cleaned up when the `StrictPath` is dropped.
691    ///
692    /// # Example
693    /// ```
694    /// # #[cfg(feature = "tempfile")] {
695    /// use strict_path::PathBoundary;
696    ///
697    /// // Get a validated temp directory path directly
698    /// let temp_root = PathBoundary::<()>::try_new_temp()?;
699    /// let user_input = "uploads/document.pdf";
700    /// let validated_path = temp_root.strict_join(user_input)?; // Returns StrictPath
701    /// // Ensure parent directories exist before writing
702    /// validated_path.create_parent_dir_all()?;
703    /// std::fs::write(validated_path.interop_path(), b"content")?; // Direct filesystem access
704    /// // temp_root is dropped here, directory gets cleaned up automatically
705    /// # }
706    /// # Ok::<(), Box<dyn std::error::Error>>(())
707    /// ```
708    #[cfg(feature = "tempfile")]
709    pub fn try_new_temp() -> Result<Self> {
710        let temp_dir =
711            tempfile::tempdir().map_err(|e| crate::StrictPathError::InvalidRestriction {
712                restriction: "temp".into(),
713                source: e,
714            })?;
715
716        let temp_path = temp_dir.path();
717        let raw = PathHistory::<Raw>::new(temp_path);
718        let canonicalized = raw.canonicalize()?;
719        let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
720            crate::StrictPathError::InvalidRestriction {
721                restriction: "temp".into(),
722                source: std::io::Error::new(
723                    std::io::ErrorKind::NotFound,
724                    "Temp directory verification failed",
725                ),
726            }
727        })?;
728
729        Ok(Self::new_with_temp_dir(
730            Arc::new(verified_exists),
731            Some(Arc::new(temp_dir)),
732        ))
733    }
734
735    /// Creates a PathBoundary in a temporary directory with a custom prefix and RAII cleanup.
736    ///
737    /// Returns a `StrictPath` pointing to the temp directory root. The directory
738    /// will be automatically cleaned up when the `StrictPath` is dropped.
739    ///
740    /// # Example
741    /// ```
742    /// # #[cfg(feature = "tempfile")] {
743    /// use strict_path::PathBoundary;
744    ///
745    /// // Get a validated temp directory path with session prefix
746    /// let upload_root = PathBoundary::<()>::try_new_temp_with_prefix("upload_batch")?;
747    /// let user_file = upload_root.strict_join("user_document.pdf")?; // Validate path
748    /// // Process validated path with direct filesystem operations
749    /// // upload_root is dropped here, directory gets cleaned up automatically
750    /// # }
751    /// # Ok::<(), Box<dyn std::error::Error>>(())
752    /// ```
753    #[cfg(feature = "tempfile")]
754    pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
755        let temp_dir = tempfile::Builder::new()
756            .prefix(prefix)
757            .tempdir()
758            .map_err(|e| crate::StrictPathError::InvalidRestriction {
759                restriction: "temp".into(),
760                source: e,
761            })?;
762
763        let temp_path = temp_dir.path();
764        let raw = PathHistory::<Raw>::new(temp_path);
765        let canonicalized = raw.canonicalize()?;
766        let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
767            crate::StrictPathError::InvalidRestriction {
768                restriction: "temp".into(),
769                source: std::io::Error::new(
770                    std::io::ErrorKind::NotFound,
771                    "Temp directory verification failed",
772                ),
773            }
774        })?;
775
776        Ok(Self::new_with_temp_dir(
777            Arc::new(verified_exists),
778            Some(Arc::new(temp_dir)),
779        ))
780    }
781
782    /// Creates a PathBoundary using app-path for portable applications.
783    ///
784    /// Creates a directory relative to the executable location, with optional
785    /// environment variable override support for deployment flexibility.
786    ///
787    /// # Example
788    /// ```
789    /// # #[cfg(feature = "app-path")] {
790    /// use strict_path::PathBoundary;
791    ///
792    /// // Creates ./config/ relative to executable
793    /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", None)?;
794    ///
795    /// // With environment override (checks MYAPP_CONFIG_DIR first)
796    /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", Some("MYAPP_CONFIG_DIR"))?;
797    /// # }
798    /// # Ok::<(), Box<dyn std::error::Error>>(())
799    /// ```
800    #[cfg(feature = "app-path")]
801    pub fn try_new_app_path(subdir: &str, env_override: Option<&str>) -> Result<Self> {
802        let app_path = app_path::AppPath::try_with_override(subdir, env_override).map_err(|e| {
803            crate::StrictPathError::InvalidRestriction {
804                restriction: format!("app-path: {subdir}").into(),
805                source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
806            }
807        })?;
808
809        Self::try_new_create(app_path)
810    }
811}
812
813impl<Marker> AsRef<Path> for PathBoundary<Marker> {
814    #[inline]
815    fn as_ref(&self) -> &Path {
816        // PathHistory implements AsRef<Path>, so forward to it
817        self.path.as_ref()
818    }
819}
820
821impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
822    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
823        f.debug_struct("PathBoundary")
824            .field("path", &self.path.as_ref())
825            .field("marker", &std::any::type_name::<Marker>())
826            .finish()
827    }
828}
829
830impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
831    type Err = crate::StrictPathError;
832
833    /// Parse a PathBoundary from a string path for universal ergonomics.
834    ///
835    /// Creates the directory if it doesn't exist, enabling seamless integration
836    /// with any string-parsing context (clap, config files, environment variables, etc.):
837    /// ```rust
838    /// # use strict_path::PathBoundary;
839    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
840    /// let temp_dir = tempfile::tempdir()?;
841    /// let safe_path = temp_dir.path().join("safe_dir");
842    /// let boundary: PathBoundary<()> = safe_path.to_string_lossy().parse()?;
843    /// assert!(safe_path.exists());
844    /// # Ok(())
845    /// # }
846    /// ```
847    #[inline]
848    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
849        Self::try_new_create(path)
850    }
851}
852// hi