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
7#[cfg(windows)]
8use std::ffi::OsStr;
9use std::io::{Error as IoError, ErrorKind};
10use std::marker::PhantomData;
11use std::path::Path;
12use std::sync::Arc;
13
14#[cfg(feature = "tempfile")]
15use tempfile::TempDir;
16
17#[cfg(windows)]
18use std::path::Component;
19
20#[cfg(windows)]
21fn is_potential_83_short_name(os: &OsStr) -> bool {
22 let s = os.to_string_lossy();
23 if let Some(pos) = s.find('~') {
24 s[pos + 1..]
25 .chars()
26 .next()
27 .is_some_and(|ch| ch.is_ascii_digit())
28 } else {
29 false
30 }
31}
32
33/// SUMMARY:
34/// Canonicalize a candidate path and enforce the `PathBoundary` boundary, returning a `StrictPath`.
35///
36/// PARAMETERS:
37/// - `path` (`AsRef<Path>`): Candidate path to validate (absolute or relative).
38/// - `restriction` (&`PathBoundary<Marker>`): Boundary to enforce during resolution.
39///
40/// RETURNS:
41/// - `Result<StrictPath<Marker>>`: Canonicalized path proven to be within `restriction`.
42///
43/// ERRORS:
44/// - `StrictPathError::WindowsShortName` (windows): Relative input contains a DOS 8.3 short name segment.
45/// - `StrictPathError::PathResolutionError`: Canonicalization fails (I/O or resolution error).
46/// - `StrictPathError::PathEscapesBoundary`: Resolved path would escape the boundary.
47///
48/// EXAMPLE:
49/// ```rust
50/// # use strict_path::{PathBoundary, Result};
51/// # fn main() -> Result<()> {
52/// let boundary = PathBoundary::<()>::try_new_create("./sandbox")?;
53/// // Use the public API that exercises the same validation pipeline
54/// // as this internal helper.
55/// let file = boundary.strict_join("sub/file.txt")?;
56/// assert!(file.interop_path().to_string_lossy().contains("sandbox"));
57/// # Ok(())
58/// # }
59/// ```
60pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
61 path: impl AsRef<Path>,
62 restriction: &PathBoundary<Marker>,
63) -> Result<StrictPath<Marker>> {
64 #[cfg(windows)]
65 {
66 let original_user_path = path.as_ref().to_path_buf();
67 if !path.as_ref().is_absolute() {
68 let mut probe = restriction.path().to_path_buf();
69 for comp in path.as_ref().components() {
70 match comp {
71 Component::CurDir | Component::ParentDir => continue,
72 Component::RootDir | Component::Prefix(_) => continue,
73 Component::Normal(name) => {
74 if is_potential_83_short_name(name) {
75 return Err(StrictPathError::windows_short_name(
76 name.to_os_string(),
77 original_user_path,
78 probe.clone(),
79 ));
80 }
81 probe.push(name);
82 }
83 }
84 }
85 }
86 }
87
88 let target_path = if path.as_ref().is_absolute() {
89 path.as_ref().to_path_buf()
90 } else {
91 restriction.path().join(path.as_ref())
92 };
93
94 let validated_path = PathHistory::<Raw>::new(target_path)
95 .canonicalize()?
96 .boundary_check(&restriction.path)?;
97
98 Ok(StrictPath::new(
99 Arc::new(restriction.clone()),
100 validated_path,
101 ))
102}
103
104/// A path boundary that serves as the secure foundation for validated path operations.
105///
106/// SUMMARY:
107/// Represent the trusted filesystem boundary directory for all strict and virtual path
108/// operations. All `StrictPath`/`VirtualPath` values derived from a `PathBoundary` are
109/// guaranteed to remain within this boundary.
110///
111/// EXAMPLE:
112/// ```rust
113/// # use strict_path::PathBoundary;
114/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
115/// let boundary = PathBoundary::<()>::try_new_create("./data")?;
116/// let file = boundary.strict_join("logs/app.log")?;
117/// println!("{}", file.strictpath_display());
118/// # Ok(())
119/// # }
120/// ```
121pub struct PathBoundary<Marker = ()> {
122 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
123 #[cfg(feature = "tempfile")]
124 _temp_dir: Option<Arc<TempDir>>,
125 _marker: PhantomData<Marker>,
126}
127
128impl<Marker> Clone for PathBoundary<Marker> {
129 fn clone(&self) -> Self {
130 Self {
131 path: self.path.clone(),
132 #[cfg(feature = "tempfile")]
133 _temp_dir: self._temp_dir.clone(),
134 _marker: PhantomData,
135 }
136 }
137}
138
139impl<Marker> Eq for PathBoundary<Marker> {}
140
141impl<M1, M2> PartialEq<PathBoundary<M2>> for PathBoundary<M1> {
142 #[inline]
143 fn eq(&self, other: &PathBoundary<M2>) -> bool {
144 self.path() == other.path()
145 }
146}
147
148impl<Marker> std::hash::Hash for PathBoundary<Marker> {
149 #[inline]
150 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
151 self.path().hash(state);
152 }
153}
154
155impl<Marker> PartialOrd for PathBoundary<Marker> {
156 #[inline]
157 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
158 Some(self.cmp(other))
159 }
160}
161
162impl<Marker> Ord for PathBoundary<Marker> {
163 #[inline]
164 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
165 self.path().cmp(other.path())
166 }
167}
168
169impl<M1, M2> PartialEq<crate::validator::virtual_root::VirtualRoot<M2>> for PathBoundary<M1> {
170 #[inline]
171 fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<M2>) -> bool {
172 self.path() == other.path()
173 }
174}
175
176impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
177 #[inline]
178 fn eq(&self, other: &Path) -> bool {
179 self.path() == other
180 }
181}
182
183impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
184 #[inline]
185 fn eq(&self, other: &std::path::PathBuf) -> bool {
186 self.eq(other.as_path())
187 }
188}
189
190impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
191 #[inline]
192 fn eq(&self, other: &&std::path::Path) -> bool {
193 self.eq(*other)
194 }
195}
196
197impl<Marker> PathBoundary<Marker> {
198 /// Private constructor that allows setting the temp_dir during construction
199 #[cfg(feature = "tempfile")]
200 fn new_with_temp_dir(
201 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
202 temp_dir: Option<Arc<TempDir>>,
203 ) -> Self {
204 Self {
205 path,
206 _temp_dir: temp_dir,
207 _marker: PhantomData,
208 }
209 }
210
211 /// Creates a new `PathBoundary` anchored at `restriction_path` (which must already exist and be a directory).
212 ///
213 /// SUMMARY:
214 /// Create a boundary anchored at an existing directory (must exist and be a directory).
215 ///
216 /// PARAMETERS:
217 /// - `restriction_path` (`AsRef<Path>`): Existing directory to anchor the boundary.
218 ///
219 /// RETURNS:
220 /// - `Result<PathBoundary<Marker>>`: New boundary whose directory is canonicalized and verified to exist.
221 ///
222 /// ERRORS:
223 /// - `StrictPathError::InvalidRestriction`: Boundary directory is missing, not a directory, or cannot be canonicalized.
224 ///
225 /// EXAMPLE:
226 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
227 /// ```rust
228 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
229 /// use strict_path::PathBoundary;
230 /// let tmp_dir = tempfile::tempdir()?;
231 /// let tmp_dir = PathBoundary::<()>::try_new(tmp_dir)?; // Clean variable shadowing
232 /// # Ok(())
233 /// # }
234 /// ```
235 #[inline]
236 pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
237 let restriction_path = restriction_path.as_ref();
238 let raw = PathHistory::<Raw>::new(restriction_path);
239
240 let canonicalized = raw.canonicalize()?;
241
242 let verified_exists = match canonicalized.verify_exists() {
243 Some(path) => path,
244 None => {
245 let io = IoError::new(
246 ErrorKind::NotFound,
247 "The specified PathBoundary path does not exist.",
248 );
249 return Err(StrictPathError::invalid_restriction(
250 restriction_path.to_path_buf(),
251 io,
252 ));
253 }
254 };
255
256 if !verified_exists.is_dir() {
257 let error = IoError::new(
258 ErrorKind::InvalidInput,
259 "The specified PathBoundary path exists but is not a directory.",
260 );
261 return Err(StrictPathError::invalid_restriction(
262 restriction_path.to_path_buf(),
263 error,
264 ));
265 }
266
267 #[cfg(feature = "tempfile")]
268 {
269 Ok(Self::new_with_temp_dir(Arc::new(verified_exists), None))
270 }
271 #[cfg(not(feature = "tempfile"))]
272 {
273 Ok(Self {
274 path: Arc::new(verified_exists),
275 _marker: PhantomData,
276 })
277 }
278 }
279
280 /// Creates the directory if missing, then constructs a new `PathBoundary`.
281 ///
282 /// SUMMARY:
283 /// Ensure the boundary directory exists (create if missing) and construct a new boundary.
284 ///
285 /// PARAMETERS:
286 /// - `boundary_dir` (`AsRef<Path>`): Directory to create if needed and use as the boundary directory.
287 ///
288 /// RETURNS:
289 /// - `Result<PathBoundary<Marker>>`: New boundary anchored at `boundary_dir`.
290 ///
291 /// ERRORS:
292 /// - `StrictPathError::InvalidRestriction`: Directory creation/canonicalization fails.
293 ///
294 /// EXAMPLE:
295 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
296 /// ```rust
297 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
298 /// use strict_path::PathBoundary;
299 /// let tmp_dir = tempfile::tempdir()?;
300 /// let tmp_dir = PathBoundary::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
301 /// # Ok(())
302 /// # }
303 /// ```
304 pub fn try_new_create<P: AsRef<Path>>(boundary_dir: P) -> Result<Self> {
305 let boundary_path = boundary_dir.as_ref();
306 if !boundary_path.exists() {
307 std::fs::create_dir_all(boundary_path).map_err(|e| {
308 StrictPathError::invalid_restriction(boundary_path.to_path_buf(), e)
309 })?;
310 }
311 Self::try_new(boundary_path)
312 }
313
314 /// SUMMARY:
315 /// Join a candidate path to the boundary and return a validated `StrictPath`.
316 ///
317 /// PARAMETERS:
318 /// - `candidate_path` (`AsRef<Path>`): Absolute or relative path to validate within this boundary.
319 ///
320 /// RETURNS:
321 /// - `Result<StrictPath<Marker>>`: Canonicalized, boundary-checked path.
322 ///
323 /// ERRORS:
324 /// - `StrictPathError::WindowsShortName` (windows), `StrictPathError::PathResolutionError`,
325 /// `StrictPathError::PathEscapesBoundary`.
326 #[inline]
327 pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
328 canonicalize_and_enforce_restriction_boundary(candidate_path, self)
329 }
330
331 /// SUMMARY:
332 /// Consume this boundary and substitute a new marker type.
333 ///
334 /// DETAILS:
335 /// Mirrors [`crate::StrictPath::change_marker`] and [`crate::VirtualPath::change_marker`], enabling
336 /// marker transformation after authorization checks. Use this when encoding proven
337 /// authorization into the type system (e.g., after validating a user's permissions).
338 /// The consumption makes marker changes explicit during code review.
339 ///
340 /// PARAMETERS:
341 /// - `NewMarker` (type parameter): Marker to associate with the boundary.
342 ///
343 /// RETURNS:
344 /// - `PathBoundary<NewMarker>`: Same underlying boundary, rebranded with `NewMarker`.
345 ///
346 /// EXAMPLE:
347 /// ```rust
348 /// # use strict_path::PathBoundary;
349 /// # let boundary_dir = std::env::temp_dir().join("change-marker-example");
350 /// # std::fs::create_dir_all(&boundary_dir)?;
351 /// struct ReadOnly;
352 /// struct ReadWrite;
353 ///
354 /// let read_boundary: PathBoundary<ReadOnly> = PathBoundary::try_new(&boundary_dir)?;
355 ///
356 /// // After authorization check...
357 /// let write_boundary: PathBoundary<ReadWrite> = read_boundary.change_marker();
358 /// # std::fs::remove_dir_all(&boundary_dir)?;
359 /// # Ok::<_, Box<dyn std::error::Error>>(())
360 /// ```
361 #[inline]
362 pub fn change_marker<NewMarker>(self) -> PathBoundary<NewMarker> {
363 PathBoundary {
364 path: self.path,
365 #[cfg(feature = "tempfile")]
366 _temp_dir: self._temp_dir,
367 _marker: PhantomData,
368 }
369 }
370
371 /// SUMMARY:
372 /// Consume this boundary and return a `StrictPath` anchored at the boundary directory.
373 ///
374 /// PARAMETERS:
375 /// - _none_
376 ///
377 /// RETURNS:
378 /// - `Result<StrictPath<Marker>>`: Strict path for the canonicalized boundary directory.
379 ///
380 /// ERRORS:
381 /// - `StrictPathError::PathResolutionError`: Canonicalization fails (directory removed or inaccessible).
382 /// - `StrictPathError::PathEscapesBoundary`: Guard against race conditions that move the directory.
383 ///
384 /// EXAMPLE:
385 /// ```rust
386 /// # use strict_path::{PathBoundary, StrictPath};
387 /// # let boundary_dir = std::env::temp_dir().join("into-strictpath-example");
388 /// # std::fs::create_dir_all(&boundary_dir)?;
389 /// let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
390 /// let boundary_path: StrictPath = boundary.into_strictpath()?;
391 /// assert!(boundary_path.is_dir());
392 /// # std::fs::remove_dir_all(&boundary_dir)?;
393 /// # Ok::<_, Box<dyn std::error::Error>>(())
394 /// ```
395 #[inline]
396 pub fn into_strictpath(self) -> Result<StrictPath<Marker>> {
397 let root_history = self.path.clone();
398 let validated = PathHistory::<Raw>::new(root_history.as_ref().to_path_buf())
399 .canonicalize()?
400 .boundary_check(root_history.as_ref())?;
401 Ok(StrictPath::new(Arc::new(self), validated))
402 }
403
404 /// Returns the canonicalized PathBoundary directory path. Kept crate-private to avoid leaking raw path.
405 #[inline]
406 pub(crate) fn path(&self) -> &Path {
407 self.path.as_ref()
408 }
409
410 /// Internal: returns the canonicalized PathHistory of the PathBoundary directory for boundary checks.
411 #[inline]
412 pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
413 &self.path
414 }
415
416 /// Returns true if the PathBoundary directory exists.
417 ///
418 /// This is always true for a constructed PathBoundary, but we query the filesystem for robustness.
419 #[inline]
420 pub fn exists(&self) -> bool {
421 self.path.exists()
422 }
423
424 /// SUMMARY:
425 /// Return the boundary directory path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop (no allocation).
426 #[inline]
427 pub fn interop_path(&self) -> &std::ffi::OsStr {
428 self.path.as_os_str()
429 }
430
431 /// Returns a Display wrapper that shows the PathBoundary directory system path.
432 #[inline]
433 pub fn strictpath_display(&self) -> std::path::Display<'_> {
434 self.path().display()
435 }
436
437 /// Internal helper: exposes the tempfile RAII handle so `VirtualRoot` constructors can mirror cleanup semantics when constructed from temporary directories.
438 #[cfg(feature = "tempfile")]
439 #[inline]
440 pub(crate) fn temp_dir_arc(&self) -> Option<Arc<TempDir>> {
441 self._temp_dir.clone()
442 }
443
444 /// SUMMARY:
445 /// Return filesystem metadata for the boundary directory.
446 #[inline]
447 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
448 std::fs::metadata(self.path())
449 }
450
451 /// SUMMARY:
452 /// Create a symbolic link at `link_path` pointing to this boundary's directory.
453 ///
454 /// PARAMETERS:
455 /// - `link_path` (&`StrictPath<Marker>`): Destination for the symlink, within the same boundary.
456 ///
457 /// RETURNS:
458 /// - `io::Result<()>`: Mirrors std semantics.
459 pub fn strict_symlink(
460 &self,
461 link_path: &crate::path::strict_path::StrictPath<Marker>,
462 ) -> std::io::Result<()> {
463 let root = self
464 .clone()
465 .into_strictpath()
466 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
467
468 root.strict_symlink(link_path)
469 }
470
471 /// SUMMARY:
472 /// Create a hard link at `link_path` pointing to this boundary's directory.
473 ///
474 /// PARAMETERS and RETURNS mirror `strict_symlink`.
475 pub fn strict_hard_link(
476 &self,
477 link_path: &crate::path::strict_path::StrictPath<Marker>,
478 ) -> std::io::Result<()> {
479 let root = self
480 .clone()
481 .into_strictpath()
482 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
483
484 root.strict_hard_link(link_path)
485 }
486
487 /// SUMMARY:
488 /// Read directory entries under the boundary directory (discovery only).
489 #[inline]
490 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
491 std::fs::read_dir(self.path())
492 }
493
494 /// SUMMARY:
495 /// Remove the boundary directory (non-recursive); fails if not empty.
496 #[inline]
497 pub fn remove_dir(&self) -> std::io::Result<()> {
498 std::fs::remove_dir(self.path())
499 }
500
501 /// SUMMARY:
502 /// Recursively remove the boundary directory and its contents.
503 #[inline]
504 pub fn remove_dir_all(&self) -> std::io::Result<()> {
505 std::fs::remove_dir_all(self.path())
506 }
507
508 /// SUMMARY:
509 /// Convert this boundary into a `VirtualRoot` for virtual path operations.
510 #[inline]
511 pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
512 crate::VirtualRoot {
513 root: self,
514 #[cfg(feature = "tempfile")]
515 _temp_dir: None,
516 _marker: PhantomData,
517 }
518 }
519
520 // Note: Do not add new crate-private helpers unless necessary; use existing flows.
521
522 // OS Standard Directory Constructors
523 //
524 // These constructors provide secure access to operating system standard directories
525 // following platform-specific conventions (XDG on Linux, Known Folder API on Windows,
526 // Apple Standard Directories on macOS). Each creates an app-specific subdirectory
527 // and enforces path boundaries for secure file operations.
528
529 /// Creates a PathBoundary in the OS standard config directory for the given application.
530 ///
531 /// **Cross-Platform Behavior:**
532 /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
533 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
534 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
535 ///
536 /// Respects environment variables like `$XDG_CONFIG_HOME` on Linux systems.
537 #[cfg(feature = "dirs")]
538 pub fn try_new_os_config(app_name: &str) -> Result<Self> {
539 let config_dir = dirs::config_dir()
540 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
541 restriction: "os-config".into(),
542 source: std::io::Error::new(
543 std::io::ErrorKind::NotFound,
544 "OS config directory not available",
545 ),
546 })?
547 .join(app_name);
548 Self::try_new_create(config_dir)
549 }
550
551 /// Creates a PathBoundary in the OS standard data directory for the given application.
552 ///
553 /// **Cross-Platform Behavior:**
554 /// - **Linux**: `~/.local/share/{app_name}` (XDG Base Directory Specification)
555 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
556 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
557 ///
558 /// Respects environment variables like `$XDG_DATA_HOME` on Linux systems.
559 #[cfg(feature = "dirs")]
560 pub fn try_new_os_data(app_name: &str) -> Result<Self> {
561 let data_dir = dirs::data_dir()
562 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
563 restriction: "os-data".into(),
564 source: std::io::Error::new(
565 std::io::ErrorKind::NotFound,
566 "OS data directory not available",
567 ),
568 })?
569 .join(app_name);
570 Self::try_new_create(data_dir)
571 }
572
573 /// Creates a PathBoundary in the OS standard cache directory for the given application.
574 ///
575 /// **Cross-Platform Behavior:**
576 /// - **Linux**: `~/.cache/{app_name}` (XDG Base Directory Specification)
577 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
578 /// - **macOS**: `~/Library/Caches/{app_name}` (Apple Standard Directories)
579 ///
580 /// Respects environment variables like `$XDG_CACHE_HOME` on Linux systems.
581 #[cfg(feature = "dirs")]
582 pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
583 let cache_dir = dirs::cache_dir()
584 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
585 restriction: "os-cache".into(),
586 source: std::io::Error::new(
587 std::io::ErrorKind::NotFound,
588 "OS cache directory not available",
589 ),
590 })?
591 .join(app_name);
592 Self::try_new_create(cache_dir)
593 }
594
595 /// Creates a PathBoundary in the OS local config directory (non-roaming on Windows).
596 ///
597 /// **Cross-Platform Behavior:**
598 /// - **Linux**: `~/.config/{app_name}` (same as config_dir)
599 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
600 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as config_dir)
601 #[cfg(feature = "dirs")]
602 pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
603 let config_dir = dirs::config_local_dir()
604 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
605 restriction: "os-config-local".into(),
606 source: std::io::Error::new(
607 std::io::ErrorKind::NotFound,
608 "OS local config directory not available",
609 ),
610 })?
611 .join(app_name);
612 Self::try_new_create(config_dir)
613 }
614
615 /// Creates a PathBoundary in the OS local data directory.
616 ///
617 /// **Cross-Platform Behavior:**
618 /// - **Linux**: `~/.local/share/{app_name}` (same as data_dir)
619 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
620 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as data_dir)
621 #[cfg(feature = "dirs")]
622 pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
623 let data_dir = dirs::data_local_dir()
624 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
625 restriction: "os-data-local".into(),
626 source: std::io::Error::new(
627 std::io::ErrorKind::NotFound,
628 "OS local data directory not available",
629 ),
630 })?
631 .join(app_name);
632 Self::try_new_create(data_dir)
633 }
634
635 /// Creates a PathBoundary in the user's home directory.
636 ///
637 /// **Cross-Platform Behavior:**
638 /// - **Linux**: `$HOME`
639 /// - **Windows**: `%USERPROFILE%` (e.g., `C:\Users\Username`)
640 /// - **macOS**: `$HOME`
641 #[cfg(feature = "dirs")]
642 pub fn try_new_os_home() -> Result<Self> {
643 let home_dir =
644 dirs::home_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
645 restriction: "os-home".into(),
646 source: std::io::Error::new(
647 std::io::ErrorKind::NotFound,
648 "OS home directory not available",
649 ),
650 })?;
651 Self::try_new(home_dir)
652 }
653
654 /// Creates a PathBoundary in the user's desktop directory.
655 ///
656 /// **Cross-Platform Behavior:**
657 /// - **Linux**: `$HOME/Desktop` or XDG_DESKTOP_DIR
658 /// - **Windows**: `%USERPROFILE%\Desktop`
659 /// - **macOS**: `$HOME/Desktop`
660 #[cfg(feature = "dirs")]
661 pub fn try_new_os_desktop() -> Result<Self> {
662 let desktop_dir =
663 dirs::desktop_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
664 restriction: "os-desktop".into(),
665 source: std::io::Error::new(
666 std::io::ErrorKind::NotFound,
667 "OS desktop directory not available",
668 ),
669 })?;
670 Self::try_new(desktop_dir)
671 }
672
673 /// Creates a PathBoundary in the user's documents directory.
674 ///
675 /// **Cross-Platform Behavior:**
676 /// - **Linux**: `$HOME/Documents` or XDG_DOCUMENTS_DIR
677 /// - **Windows**: `%USERPROFILE%\Documents`
678 /// - **macOS**: `$HOME/Documents`
679 #[cfg(feature = "dirs")]
680 pub fn try_new_os_documents() -> Result<Self> {
681 let docs_dir =
682 dirs::document_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
683 restriction: "os-documents".into(),
684 source: std::io::Error::new(
685 std::io::ErrorKind::NotFound,
686 "OS documents directory not available",
687 ),
688 })?;
689 Self::try_new(docs_dir)
690 }
691
692 /// Creates a PathBoundary in the user's downloads directory.
693 ///
694 /// **Cross-Platform Behavior:**
695 /// - **Linux**: `$HOME/Downloads` or XDG_DOWNLOAD_DIR
696 /// - **Windows**: `%USERPROFILE%\Downloads`
697 /// - **macOS**: `$HOME/Downloads`
698 #[cfg(feature = "dirs")]
699 pub fn try_new_os_downloads() -> Result<Self> {
700 let downloads_dir =
701 dirs::download_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
702 restriction: "os-downloads".into(),
703 source: std::io::Error::new(
704 std::io::ErrorKind::NotFound,
705 "OS downloads directory not available",
706 ),
707 })?;
708 Self::try_new(downloads_dir)
709 }
710
711 /// Creates a PathBoundary in the user's pictures directory.
712 ///
713 /// **Cross-Platform Behavior:**
714 /// - **Linux**: `$HOME/Pictures` or XDG_PICTURES_DIR
715 /// - **Windows**: `%USERPROFILE%\Pictures`
716 /// - **macOS**: `$HOME/Pictures`
717 #[cfg(feature = "dirs")]
718 pub fn try_new_os_pictures() -> Result<Self> {
719 let pictures_dir =
720 dirs::picture_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
721 restriction: "os-pictures".into(),
722 source: std::io::Error::new(
723 std::io::ErrorKind::NotFound,
724 "OS pictures directory not available",
725 ),
726 })?;
727 Self::try_new(pictures_dir)
728 }
729
730 /// Creates a PathBoundary in the user's music/audio directory.
731 ///
732 /// **Cross-Platform Behavior:**
733 /// - **Linux**: `$HOME/Music` or XDG_MUSIC_DIR
734 /// - **Windows**: `%USERPROFILE%\Music`
735 /// - **macOS**: `$HOME/Music`
736 #[cfg(feature = "dirs")]
737 pub fn try_new_os_audio() -> Result<Self> {
738 let audio_dir =
739 dirs::audio_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
740 restriction: "os-audio".into(),
741 source: std::io::Error::new(
742 std::io::ErrorKind::NotFound,
743 "OS audio directory not available",
744 ),
745 })?;
746 Self::try_new(audio_dir)
747 }
748
749 /// Creates a PathBoundary in the user's videos directory.
750 ///
751 /// **Cross-Platform Behavior:**
752 /// - **Linux**: `$HOME/Videos` or XDG_VIDEOS_DIR
753 /// - **Windows**: `%USERPROFILE%\Videos`
754 /// - **macOS**: `$HOME/Movies`
755 #[cfg(feature = "dirs")]
756 pub fn try_new_os_videos() -> Result<Self> {
757 let videos_dir =
758 dirs::video_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
759 restriction: "os-videos".into(),
760 source: std::io::Error::new(
761 std::io::ErrorKind::NotFound,
762 "OS videos directory not available",
763 ),
764 })?;
765 Self::try_new(videos_dir)
766 }
767
768 /// Creates a PathBoundary in the OS executable directory (Linux only).
769 ///
770 /// **Platform Availability:**
771 /// - **Linux**: `~/.local/bin` or $XDG_BIN_HOME
772 /// - **Windows**: Returns error (not available)
773 /// - **macOS**: Returns error (not available)
774 #[cfg(feature = "dirs")]
775 pub fn try_new_os_executables() -> Result<Self> {
776 let exec_dir =
777 dirs::executable_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
778 restriction: "os-executables".into(),
779 source: std::io::Error::new(
780 std::io::ErrorKind::NotFound,
781 "OS executables directory not available on this platform",
782 ),
783 })?;
784 Self::try_new(exec_dir)
785 }
786
787 /// Creates a PathBoundary in the OS runtime directory (Linux only).
788 ///
789 /// **Platform Availability:**
790 /// - **Linux**: `$XDG_RUNTIME_DIR` (session-specific, user-only access)
791 /// - **Windows**: Returns error (not available)
792 /// - **macOS**: Returns error (not available)
793 #[cfg(feature = "dirs")]
794 pub fn try_new_os_runtime() -> Result<Self> {
795 let runtime_dir =
796 dirs::runtime_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
797 restriction: "os-runtime".into(),
798 source: std::io::Error::new(
799 std::io::ErrorKind::NotFound,
800 "OS runtime directory not available on this platform",
801 ),
802 })?;
803 Self::try_new(runtime_dir)
804 }
805
806 /// Creates a PathBoundary in the OS state directory (Linux only).
807 ///
808 /// **Platform Availability:**
809 /// - **Linux**: `~/.local/state/{app_name}` or $XDG_STATE_HOME/{app_name}
810 /// - **Windows**: Returns error (not available)
811 /// - **macOS**: Returns error (not available)
812 #[cfg(feature = "dirs")]
813 pub fn try_new_os_state(app_name: &str) -> Result<Self> {
814 let state_dir = dirs::state_dir()
815 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
816 restriction: "os-state".into(),
817 source: std::io::Error::new(
818 std::io::ErrorKind::NotFound,
819 "OS state directory not available on this platform",
820 ),
821 })?
822 .join(app_name);
823 Self::try_new_create(state_dir)
824 }
825
826 /// Creates a PathBoundary in a unique temporary directory with RAII cleanup.
827 ///
828 /// Returns a `StrictPath` pointing to the temporary boundary directory. The
829 /// directory will be automatically cleaned up when the `StrictPath` is dropped.
830 ///
831 /// # Example
832 /// ```
833 /// # #[cfg(feature = "tempfile")] {
834 /// use strict_path::PathBoundary;
835 ///
836 /// // Get a validated temp directory path directly
837 /// let temp_boundary = PathBoundary::<()>::try_new_temp()?;
838 /// let user_input = "uploads/document.pdf";
839 /// let validated_path = temp_boundary.strict_join(user_input)?; // Returns StrictPath
840 /// // Ensure parent directories exist before writing
841 /// validated_path.create_parent_dir_all()?;
842 /// validated_path.write(b"content")?; // Prefer strict-path helpers over std::fs
843 /// // temp_boundary is dropped here, directory gets cleaned up automatically
844 /// # }
845 /// # Ok::<(), Box<dyn std::error::Error>>(())
846 /// ```
847 #[cfg(feature = "tempfile")]
848 pub fn try_new_temp() -> Result<Self> {
849 let temp_dir =
850 tempfile::tempdir().map_err(|e| crate::StrictPathError::InvalidRestriction {
851 restriction: "temp".into(),
852 source: e,
853 })?;
854
855 let temp_path = temp_dir.path();
856 let raw = PathHistory::<Raw>::new(temp_path);
857 let canonicalized = raw.canonicalize()?;
858 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
859 crate::StrictPathError::InvalidRestriction {
860 restriction: "temp".into(),
861 source: std::io::Error::new(
862 std::io::ErrorKind::NotFound,
863 "Temp directory verification failed",
864 ),
865 }
866 })?;
867
868 Ok(Self::new_with_temp_dir(
869 Arc::new(verified_exists),
870 Some(Arc::new(temp_dir)),
871 ))
872 }
873
874 /// Creates a PathBoundary in a temporary directory with a custom prefix and RAII cleanup.
875 ///
876 /// Returns a `StrictPath` pointing to the prefixed temporary boundary directory. The
877 /// directory will be automatically cleaned up when the `StrictPath` is dropped.
878 ///
879 /// # Example
880 /// ```
881 /// # #[cfg(feature = "tempfile")] {
882 /// use strict_path::PathBoundary;
883 ///
884 /// // Get a validated temp directory path with session prefix
885 /// let upload_boundary = PathBoundary::<()>::try_new_temp_with_prefix("upload_batch")?;
886 /// let user_file = upload_boundary.strict_join("user_document.pdf")?; // Validate path
887 /// // Process validated path with direct filesystem operations
888 /// // upload_boundary is dropped here, directory gets cleaned up automatically
889 /// # }
890 /// # Ok::<(), Box<dyn std::error::Error>>(())
891 /// ```
892 #[cfg(feature = "tempfile")]
893 pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
894 let temp_dir = tempfile::Builder::new()
895 .prefix(prefix)
896 .tempdir()
897 .map_err(|e| crate::StrictPathError::InvalidRestriction {
898 restriction: "temp".into(),
899 source: e,
900 })?;
901
902 let temp_path = temp_dir.path();
903 let raw = PathHistory::<Raw>::new(temp_path);
904 let canonicalized = raw.canonicalize()?;
905 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
906 crate::StrictPathError::InvalidRestriction {
907 restriction: "temp".into(),
908 source: std::io::Error::new(
909 std::io::ErrorKind::NotFound,
910 "Temp directory verification failed",
911 ),
912 }
913 })?;
914
915 Ok(Self::new_with_temp_dir(
916 Arc::new(verified_exists),
917 Some(Arc::new(temp_dir)),
918 ))
919 }
920
921 /// SUMMARY:
922 /// Create a boundary using `app-path` semantics (portable app-relative directory) with optional env override.
923 ///
924 /// PARAMETERS:
925 /// - `subdir` (`AsRef<Path>`): Subdirectory path relative to the executable (or override directory).
926 /// - `env_override` (Option<&str>): Optional environment variable name; when present and set,
927 /// its value is used as the base directory instead of the executable directory.
928 ///
929 /// RETURNS:
930 /// - `Result<PathBoundary<Marker>>`: Created/validated boundary at the resolved app-path location.
931 ///
932 /// ERRORS:
933 /// - `StrictPathError::InvalidRestriction`: If resolution fails or directory cannot be created/validated.
934 ///
935 /// EXAMPLE:
936 /// ```
937 /// # #[cfg(feature = "app-path")] {
938 /// use strict_path::PathBoundary;
939 ///
940 /// // Creates ./config/ relative to executable
941 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", None)?;
942 ///
943 /// // With environment override (checks MYAPP_CONFIG_DIR first)
944 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", Some("MYAPP_CONFIG_DIR"))?;
945 /// # }
946 /// # Ok::<(), Box<dyn std::error::Error>>(())
947 /// ```
948 #[cfg(feature = "app-path")]
949 pub fn try_new_app_path<P: AsRef<std::path::Path>>(
950 subdir: P,
951 env_override: Option<&str>,
952 ) -> Result<Self> {
953 let subdir_path = subdir.as_ref();
954 // Resolve the override environment variable name (if provided) to its value.
955 // app-path expects the override PATH value, not the variable name.
956 let override_value: Option<String> = env_override.and_then(|key| std::env::var(key).ok());
957 let app_path = app_path::AppPath::try_with_override(subdir_path, override_value.as_deref())
958 .map_err(|e| crate::StrictPathError::InvalidRestriction {
959 restriction: format!("app-path: {}", subdir_path.display()).into(),
960 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
961 })?;
962
963 Self::try_new_create(app_path)
964 }
965
966 /// SUMMARY:
967 /// Create a boundary using `app-path`, always consulting a specific environment variable first.
968 ///
969 /// PARAMETERS:
970 /// - `subdir` (`AsRef<Path>`): Subdirectory used with `app-path` resolution.
971 /// - `env_override` (&str): Environment variable name to check for a base directory.
972 ///
973 /// RETURNS:
974 /// - `Result<PathBoundary<Marker>>`: New boundary anchored using `app-path` semantics.
975 ///
976 /// ERRORS:
977 /// - `StrictPathError::InvalidRestriction`: If resolution fails or the directory can't be created/validated.
978 #[cfg(feature = "app-path")]
979 pub fn try_new_app_path_with_env<P: AsRef<std::path::Path>>(
980 subdir: P,
981 env_override: &str,
982 ) -> Result<Self> {
983 let subdir_path = subdir.as_ref();
984 Self::try_new_app_path(subdir_path, Some(env_override))
985 }
986}
987
988impl<Marker> AsRef<Path> for PathBoundary<Marker> {
989 #[inline]
990 fn as_ref(&self) -> &Path {
991 // PathHistory implements AsRef<Path>, so forward to it
992 self.path.as_ref()
993 }
994}
995
996impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
997 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
998 f.debug_struct("PathBoundary")
999 .field("path", &self.path.as_ref())
1000 .field("marker", &std::any::type_name::<Marker>())
1001 .finish()
1002 }
1003}
1004
1005impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
1006 type Err = crate::StrictPathError;
1007
1008 /// Parse a PathBoundary from a string path for universal ergonomics.
1009 ///
1010 /// Creates the directory if it doesn't exist, enabling seamless integration
1011 /// with any string-parsing context (clap, config files, environment variables, etc.):
1012 /// ```rust
1013 /// # use strict_path::PathBoundary;
1014 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1015 /// let temp_dir = tempfile::tempdir()?;
1016 /// let safe_path = temp_dir.path().join("safe_dir");
1017 /// let boundary: PathBoundary<()> = safe_path.to_string_lossy().parse()?;
1018 /// assert!(safe_path.exists());
1019 /// # Ok(())
1020 /// # }
1021 /// ```
1022 #[inline]
1023 fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
1024 Self::try_new_create(path)
1025 }
1026}
1027//