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