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    /// Remove the boundary 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 directory and its 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    #[cfg(feature = "virtual-path")]
437    #[inline]
438    pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
439        crate::VirtualRoot {
440            root: self,
441            _marker: PhantomData,
442        }
443    }
444
445    // Note: Do not add new crate-private helpers unless necessary; use existing flows.
446}
447
448impl<Marker> AsRef<Path> for PathBoundary<Marker> {
449    #[inline]
450    fn as_ref(&self) -> &Path {
451        // PathHistory implements AsRef<Path>, so forward to it
452        self.path.as_ref()
453    }
454}
455
456impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        f.debug_struct("PathBoundary")
459            .field("path", &self.path.as_ref())
460            .field("marker", &std::any::type_name::<Marker>())
461            .finish()
462    }
463}
464
465impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
466    type Err = crate::StrictPathError;
467
468    /// Parse a PathBoundary from a string path for universal ergonomics.
469    ///
470    /// Creates the directory if it doesn't exist, enabling seamless integration
471    /// with any string-parsing context (clap, config files, environment variables, etc.):
472    /// ```rust
473    /// # use strict_path::PathBoundary;
474    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
475    /// let boundary: PathBoundary<()> = "./data".parse()?;
476    /// assert!(boundary.exists());
477    /// # Ok(())
478    /// # }
479    /// ```
480    #[inline]
481    fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
482        Self::try_new_create(path)
483    }
484}
485//