strict_path/validator/
path_boundary.rs

1// Content copied from original src/validator/restriction.rs
2use crate::error::StrictPathError;
3use crate::path::strict_path::StrictPath;
4use crate::validator::path_history::*;
5use crate::Result;
6
7use std::io::{Error as IoError, ErrorKind};
8use std::marker::PhantomData;
9use std::path::Path;
10use std::sync::Arc;
11
12/// SUMMARY:
13/// Canonicalize a candidate path and enforce the `PathBoundary` boundary, returning a `StrictPath`.
14///
15/// PARAMETERS:
16/// - `path` (`AsRef<Path>`): Candidate path to validate (absolute or relative).
17/// - `restriction` (&`PathBoundary<Marker>`): Boundary to enforce during resolution.
18///
19/// RETURNS:
20/// - `Result<StrictPath<Marker>>`: Canonicalized path proven to be within `restriction`.
21///
22/// ERRORS:
23/// - `StrictPathError::PathResolutionError`: Canonicalization fails (I/O or resolution error).
24/// - `StrictPathError::PathEscapesBoundary`: Resolved path would escape the boundary.
25///
26/// EXAMPLE:
27/// ```rust
28/// # use strict_path::{PathBoundary, Result};
29/// # fn main() -> Result<()> {
30/// let boundary = PathBoundary::<()>::try_new_create("./sandbox")?;
31/// // Untrusted input from request/CLI/config/etc.
32/// let user_input = "sub/file.txt";
33/// // Use the public API that exercises the same validation pipeline
34/// // as this internal helper.
35/// let file = boundary.strict_join(user_input)?;
36/// assert!(file.interop_path().to_string_lossy().contains("sandbox"));
37/// # Ok(())
38/// # }
39/// ```
40pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
41    path: impl AsRef<Path>,
42    restriction: &PathBoundary<Marker>,
43) -> Result<StrictPath<Marker>> {
44    let target_path = if path.as_ref().is_absolute() {
45        path.as_ref().to_path_buf()
46    } else {
47        restriction.path().join(path.as_ref())
48    };
49
50    let canonicalized = PathHistory::<Raw>::new(target_path).canonicalize()?;
51
52    let validated_path = canonicalized.boundary_check(&restriction.path)?;
53
54    Ok(StrictPath::new(
55        Arc::new(restriction.clone()),
56        validated_path,
57    ))
58}
59
60/// A path boundary that serves as the secure foundation for validated path operations.
61///
62/// SUMMARY:
63/// Represent the trusted filesystem boundary directory for all strict and virtual path
64/// operations. All `StrictPath`/`VirtualPath` values derived from a `PathBoundary` are
65/// guaranteed to remain within this boundary.
66///
67/// EXAMPLE:
68/// ```rust
69/// # use strict_path::PathBoundary;
70/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
71/// let boundary = PathBoundary::<()>::try_new_create("./data")?;
72/// // Untrusted input from request/CLI/config/etc.
73/// let requested_file = "logs/app.log";
74/// let file = boundary.strict_join(requested_file)?;
75/// println!("{}", file.strictpath_display());
76/// # Ok(())
77/// # }
78/// ```
79pub struct PathBoundary<Marker = ()> {
80    path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
81    _marker: PhantomData<Marker>,
82}
83
84impl<Marker> Clone for PathBoundary<Marker> {
85    fn clone(&self) -> Self {
86        Self {
87            path: self.path.clone(),
88            _marker: PhantomData,
89        }
90    }
91}
92
93impl<Marker> Eq for PathBoundary<Marker> {}
94
95impl<M1, M2> PartialEq<PathBoundary<M2>> for PathBoundary<M1> {
96    #[inline]
97    fn eq(&self, other: &PathBoundary<M2>) -> bool {
98        self.path() == other.path()
99    }
100}
101
102impl<Marker> std::hash::Hash for PathBoundary<Marker> {
103    #[inline]
104    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
105        self.path().hash(state);
106    }
107}
108
109impl<Marker> PartialOrd for PathBoundary<Marker> {
110    #[inline]
111    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
112        Some(self.cmp(other))
113    }
114}
115
116impl<Marker> Ord for PathBoundary<Marker> {
117    #[inline]
118    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
119        self.path().cmp(other.path())
120    }
121}
122
123#[cfg(feature = "virtual-path")]
124impl<M1, M2> PartialEq<crate::validator::virtual_root::VirtualRoot<M2>> for PathBoundary<M1> {
125    #[inline]
126    fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<M2>) -> bool {
127        self.path() == other.path()
128    }
129}
130
131impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
132    #[inline]
133    fn eq(&self, other: &Path) -> bool {
134        self.path() == other
135    }
136}
137
138impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
139    #[inline]
140    fn eq(&self, other: &std::path::PathBuf) -> bool {
141        self.eq(other.as_path())
142    }
143}
144
145impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
146    #[inline]
147    fn eq(&self, other: &&std::path::Path) -> bool {
148        self.eq(*other)
149    }
150}
151
152impl<Marker> PathBoundary<Marker> {
153    /// Creates a new `PathBoundary` anchored at `restriction_path` (which must already exist and be a directory).
154    ///
155    /// SUMMARY:
156    /// Create a boundary anchored at an existing directory (must exist and be a directory).
157    ///
158    /// PARAMETERS:
159    /// - `restriction_path` (`AsRef<Path>`): Existing directory to anchor the boundary.
160    ///
161    /// RETURNS:
162    /// - `Result<PathBoundary<Marker>>`: New boundary whose directory is canonicalized and verified to exist.
163    ///
164    /// ERRORS:
165    /// - `StrictPathError::InvalidRestriction`: Boundary directory is missing, not a directory, or cannot be canonicalized.
166    ///
167    /// EXAMPLE:
168    /// ```rust
169    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
170    /// use strict_path::PathBoundary;
171    /// let boundary = PathBoundary::<()>::try_new("./data")?;
172    /// # Ok(())
173    /// # }
174    /// ```
175    #[inline]
176    pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
177        let restriction_path = restriction_path.as_ref();
178        let raw = PathHistory::<Raw>::new(restriction_path);
179
180        let canonicalized = raw.canonicalize()?;
181
182        let verified_exists = match canonicalized.verify_exists() {
183            Some(path) => path,
184            None => {
185                let io = IoError::new(
186                    ErrorKind::NotFound,
187                    "The specified PathBoundary path does not exist.",
188                );
189                return Err(StrictPathError::invalid_restriction(
190                    restriction_path.to_path_buf(),
191                    io,
192                ));
193            }
194        };
195
196        if !verified_exists.is_dir() {
197            let error = IoError::new(
198                ErrorKind::InvalidInput,
199                "The specified PathBoundary path exists but is not a directory.",
200            );
201            return Err(StrictPathError::invalid_restriction(
202                restriction_path.to_path_buf(),
203                error,
204            ));
205        }
206
207        Ok(Self {
208            path: Arc::new(verified_exists),
209            _marker: PhantomData,
210        })
211    }
212
213    /// Creates the directory if missing, then constructs a new `PathBoundary`.
214    ///
215    /// SUMMARY:
216    /// Ensure the boundary directory exists (create if missing) and construct a new boundary.
217    ///
218    /// PARAMETERS:
219    /// - `boundary_dir` (`AsRef<Path>`): Directory to create if needed and use as the boundary directory.
220    ///
221    /// RETURNS:
222    /// - `Result<PathBoundary<Marker>>`: New boundary anchored at `boundary_dir`.
223    ///
224    /// ERRORS:
225    /// - `StrictPathError::InvalidRestriction`: Directory creation/canonicalization fails.
226    ///
227    /// EXAMPLE:
228    /// ```rust
229    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
230    /// use strict_path::PathBoundary;
231    /// let boundary = PathBoundary::<()>::try_new_create("./data")?;
232    /// # Ok(())
233    /// # }
234    /// ```
235    pub fn try_new_create<P: AsRef<Path>>(boundary_dir: P) -> Result<Self> {
236        let boundary_path = boundary_dir.as_ref();
237        if !boundary_path.exists() {
238            std::fs::create_dir_all(boundary_path).map_err(|e| {
239                StrictPathError::invalid_restriction(boundary_path.to_path_buf(), e)
240            })?;
241        }
242        Self::try_new(boundary_path)
243    }
244
245    /// SUMMARY:
246    /// Join a candidate path to the boundary and return a validated `StrictPath`.
247    ///
248    /// PARAMETERS:
249    /// - `candidate_path` (`AsRef<Path>`): Absolute or relative path to validate within this boundary.
250    ///
251    /// RETURNS:
252    /// - `Result<StrictPath<Marker>>`: Canonicalized, boundary-checked path.
253    ///
254    /// ERRORS:
255    /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
256    #[inline]
257    pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
258        canonicalize_and_enforce_restriction_boundary(candidate_path, self)
259    }
260
261    /// SUMMARY:
262    /// Consume this boundary and substitute a new marker type.
263    ///
264    /// DETAILS:
265    /// Mirrors [`crate::StrictPath::change_marker`] and [`crate::VirtualPath::change_marker`], enabling
266    /// marker transformation after authorization checks. Use this when encoding proven
267    /// authorization into the type system (e.g., after validating a user's permissions).
268    /// The consumption makes marker changes explicit during code review.
269    ///
270    /// PARAMETERS:
271    /// - `NewMarker` (type parameter): Marker to associate with the boundary.
272    ///
273    /// RETURNS:
274    /// - `PathBoundary<NewMarker>`: Same underlying boundary, rebranded with `NewMarker`.
275    ///
276    /// EXAMPLE:
277    /// ```rust
278    /// # use strict_path::PathBoundary;
279    /// struct ReadOnly;
280    /// struct ReadWrite;
281    ///
282    /// let read_boundary: PathBoundary<ReadOnly> = PathBoundary::try_new_create("./data")?;
283    ///
284    /// // After authorization check...
285    /// let write_boundary: PathBoundary<ReadWrite> = read_boundary.change_marker();
286    /// # Ok::<_, Box<dyn std::error::Error>>(())
287    /// ```
288    #[inline]
289    pub fn change_marker<NewMarker>(self) -> PathBoundary<NewMarker> {
290        PathBoundary {
291            path: self.path,
292            _marker: PhantomData,
293        }
294    }
295
296    /// SUMMARY:
297    /// Consume this boundary and return a `StrictPath` anchored at the boundary directory.
298    ///
299    /// PARAMETERS:
300    /// - _none_
301    ///
302    /// RETURNS:
303    /// - `Result<StrictPath<Marker>>`: Strict path for the canonicalized boundary directory.
304    ///
305    /// ERRORS:
306    /// - `StrictPathError::PathResolutionError`: Canonicalization fails (directory removed or inaccessible).
307    /// - `StrictPathError::PathEscapesBoundary`: Guard against race conditions that move the directory.
308    ///
309    /// EXAMPLE:
310    /// ```rust
311    /// # use strict_path::{PathBoundary, StrictPath};
312    /// let boundary: PathBoundary = PathBoundary::try_new_create("./data")?;
313    /// let boundary_path: StrictPath = boundary.into_strictpath()?;
314    /// assert!(boundary_path.is_dir());
315    /// # Ok::<_, Box<dyn std::error::Error>>(())
316    /// ```
317    #[inline]
318    pub fn into_strictpath(self) -> Result<StrictPath<Marker>> {
319        let root_history = self.path.clone();
320        let validated = PathHistory::<Raw>::new(root_history.as_ref().to_path_buf())
321            .canonicalize()?
322            .boundary_check(root_history.as_ref())?;
323        Ok(StrictPath::new(Arc::new(self), validated))
324    }
325
326    /// Returns the canonicalized PathBoundary directory path. Kept crate-private to avoid leaking raw path.
327    #[inline]
328    pub(crate) fn path(&self) -> &Path {
329        self.path.as_ref()
330    }
331
332    /// Internal: returns the canonicalized PathHistory of the PathBoundary directory for boundary checks.
333    #[cfg(feature = "virtual-path")]
334    #[inline]
335    pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
336        &self.path
337    }
338
339    /// Returns true if the PathBoundary directory exists.
340    ///
341    /// This is always true for a constructed PathBoundary, but we query the filesystem for robustness.
342    #[inline]
343    pub fn exists(&self) -> bool {
344        self.path.exists()
345    }
346
347    /// SUMMARY:
348    /// Return the boundary directory path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop (no allocation).
349    #[inline]
350    pub fn interop_path(&self) -> &std::ffi::OsStr {
351        self.path.as_os_str()
352    }
353
354    /// Returns a Display wrapper that shows the PathBoundary directory system path.
355    #[inline]
356    pub fn strictpath_display(&self) -> std::path::Display<'_> {
357        self.path().display()
358    }
359
360    /// SUMMARY:
361    /// Return filesystem metadata for the boundary directory.
362    #[inline]
363    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
364        std::fs::metadata(self.path())
365    }
366
367    /// SUMMARY:
368    /// Create a symbolic link at `link_path` pointing to this boundary's directory.
369    ///
370    /// PARAMETERS:
371    /// - `link_path` (`impl AsRef<Path>`): Destination for the symlink, within the same boundary.
372    ///
373    /// RETURNS:
374    /// - `io::Result<()>`: Mirrors std semantics.
375    pub fn strict_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
376        let root = self
377            .clone()
378            .into_strictpath()
379            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
380
381        root.strict_symlink(link_path)
382    }
383
384    /// SUMMARY:
385    /// Create a hard link at `link_path` pointing to this boundary's directory.
386    ///
387    /// PARAMETERS and RETURNS mirror `strict_symlink`.
388    pub fn strict_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
389        let root = self
390            .clone()
391            .into_strictpath()
392            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
393
394        root.strict_hard_link(link_path)
395    }
396
397    /// SUMMARY:
398    /// Create a Windows NTFS directory junction at `link_path` pointing to this boundary's directory.
399    ///
400    /// DETAILS:
401    /// - Windows-only and behind the `junctions` crate feature.
402    /// - Junctions are directory-only.
403    #[cfg(all(windows, feature = "junctions"))]
404    pub fn strict_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
405        let root = self
406            .clone()
407            .into_strictpath()
408            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
409
410        root.strict_junction(link_path)
411    }
412
413    /// SUMMARY:
414    /// Read directory entries under the boundary directory (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    /// Iterate directory entries under the boundary, yielding validated `StrictPath` values.
422    ///
423    /// DETAILS:
424    /// Unlike `read_dir()` which returns raw `std::fs::DirEntry` values requiring manual
425    /// re-validation, this method yields `StrictPath` entries directly. Each entry is
426    /// automatically validated through `strict_join()` so you can use it immediately
427    /// for I/O operations without additional validation.
428    ///
429    /// RETURNS:
430    /// - `io::Result<BoundaryReadDir<Marker>>`: Iterator over validated `StrictPath` entries.
431    ///
432    /// EXAMPLE:
433    /// ```rust
434    /// use strict_path::PathBoundary;
435    ///
436    /// # let temp = tempfile::tempdir()?;
437    /// let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
438    /// # boundary.strict_join("file.txt")?.write("test")?;
439    ///
440    /// // Auto-validated iteration - no manual re-join needed!
441    /// for entry in boundary.strict_read_dir()? {
442    ///     let child = entry?;
443    ///     println!("Found: {}", child.strictpath_display());
444    /// }
445    /// # Ok::<_, Box<dyn std::error::Error>>(())
446    /// ```
447    #[inline]
448    pub fn strict_read_dir(&self) -> std::io::Result<BoundaryReadDir<'_, Marker>> {
449        Ok(BoundaryReadDir {
450            inner: std::fs::read_dir(self.path())?,
451            boundary: self,
452        })
453    }
454
455    /// SUMMARY:
456    /// Remove the boundary directory (non-recursive); fails if not empty.
457    #[inline]
458    pub fn remove_dir(&self) -> std::io::Result<()> {
459        std::fs::remove_dir(self.path())
460    }
461
462    /// SUMMARY:
463    /// Recursively remove the boundary directory and its contents.
464    #[inline]
465    pub fn remove_dir_all(&self) -> std::io::Result<()> {
466        std::fs::remove_dir_all(self.path())
467    }
468
469    /// SUMMARY:
470    /// Convert this boundary into a `VirtualRoot` for virtual path operations.
471    #[cfg(feature = "virtual-path")]
472    #[inline]
473    pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
474        crate::VirtualRoot {
475            root: self,
476            _marker: PhantomData,
477        }
478    }
479
480    // Note: Do not add new crate-private helpers unless necessary; use existing flows.
481}
482
483impl<Marker> AsRef<Path> for PathBoundary<Marker> {
484    #[inline]
485    fn as_ref(&self) -> &Path {
486        // PathHistory implements AsRef<Path>, so forward to it
487        self.path.as_ref()
488    }
489}
490
491impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        f.debug_struct("PathBoundary")
494            .field("path", &self.path.as_ref())
495            .field("marker", &std::any::type_name::<Marker>())
496            .finish()
497    }
498}
499
500impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
501    type Err = crate::StrictPathError;
502
503    /// Parse a PathBoundary from a string path for universal ergonomics.
504    ///
505    /// Creates the directory if it doesn't exist, enabling seamless integration
506    /// with any string-parsing context (clap, config files, environment variables, etc.):
507    /// ```rust
508    /// # use strict_path::PathBoundary;
509    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
510    /// let boundary: PathBoundary<()> = "./data".parse()?;
511    /// assert!(boundary.exists());
512    /// # Ok(())
513    /// # }
514    /// ```
515    #[inline]
516    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
517        Self::try_new_create(path)
518    }
519}
520
521// ============================================================
522// BoundaryReadDir — Iterator for validated directory entries
523// ============================================================
524
525/// SUMMARY:
526/// Iterator over directory entries that yields validated `StrictPath` values.
527///
528/// DETAILS:
529/// Created by `PathBoundary::strict_read_dir()`. Each iteration automatically validates
530/// the directory entry through `strict_join()`, so you get `StrictPath` values directly
531/// instead of raw `std::fs::DirEntry` that would require manual re-validation.
532///
533/// EXAMPLE:
534/// ```rust
535/// # use strict_path::PathBoundary;
536/// # let temp = tempfile::tempdir()?;
537/// let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
538/// # boundary.strict_join("readme.md")?.write("# Docs")?;
539/// for entry in boundary.strict_read_dir()? {
540///     let child = entry?;
541///     if child.is_file() {
542///         println!("File: {}", child.strictpath_display());
543///     }
544/// }
545/// # Ok::<_, Box<dyn std::error::Error>>(())
546/// ```
547pub struct BoundaryReadDir<'a, Marker> {
548    inner: std::fs::ReadDir,
549    boundary: &'a PathBoundary<Marker>,
550}
551
552impl<Marker> std::fmt::Debug for BoundaryReadDir<'_, Marker> {
553    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554        f.debug_struct("BoundaryReadDir")
555            .field("boundary", &self.boundary.strictpath_display())
556            .finish_non_exhaustive()
557    }
558}
559
560impl<Marker: Clone> Iterator for BoundaryReadDir<'_, Marker> {
561    type Item = std::io::Result<crate::StrictPath<Marker>>;
562
563    fn next(&mut self) -> Option<Self::Item> {
564        match self.inner.next()? {
565            Ok(entry) => {
566                let file_name = entry.file_name();
567                match self.boundary.strict_join(file_name) {
568                    Ok(strict_path) => Some(Ok(strict_path)),
569                    Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
570                }
571            }
572            Err(e) => Some(Err(e)),
573        }
574    }
575}
576//