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//