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