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 root for all strict and virtual path operations. All
108/// `StrictPath`/`VirtualPath` values derived from a `PathBoundary` are guaranteed to remain
109/// 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> PartialEq for PathBoundary<Marker> {
140 #[inline]
141 fn eq(&self, other: &Self) -> bool {
142 self.path() == other.path()
143 }
144}
145
146impl<Marker> Eq for PathBoundary<Marker> {}
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<Marker> PartialEq<crate::validator::virtual_root::VirtualRoot<Marker>>
170 for PathBoundary<Marker>
171{
172 #[inline]
173 fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<Marker>) -> bool {
174 self.path() == other.path()
175 }
176}
177
178impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
179 #[inline]
180 fn eq(&self, other: &Path) -> bool {
181 self.path() == other
182 }
183}
184
185impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
186 #[inline]
187 fn eq(&self, other: &std::path::PathBuf) -> bool {
188 self.eq(other.as_path())
189 }
190}
191
192impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
193 #[inline]
194 fn eq(&self, other: &&std::path::Path) -> bool {
195 self.eq(*other)
196 }
197}
198
199impl<Marker> PathBoundary<Marker> {
200 /// Private constructor that allows setting the temp_dir during construction
201 #[cfg(feature = "tempfile")]
202 fn new_with_temp_dir(
203 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
204 temp_dir: Option<Arc<TempDir>>,
205 ) -> Self {
206 Self {
207 path,
208 _temp_dir: temp_dir,
209 _marker: PhantomData,
210 }
211 }
212
213 /// Creates a new `PathBoundary` rooted at `restriction_path` (which must already exist and be a directory).
214 ///
215 /// SUMMARY:
216 /// Create a boundary anchored at an existing directory (must exist and be a directory).
217 ///
218 /// PARAMETERS:
219 /// - `restriction_path` (`AsRef<Path>`): Existing directory to anchor the boundary.
220 ///
221 /// RETURNS:
222 /// - `Result<PathBoundary<Marker>>`: New boundary whose root is canonicalized and verified to exist.
223 ///
224 /// ERRORS:
225 /// - `StrictPathError::InvalidRestriction`: Root is missing, not a directory, or cannot be canonicalized.
226 ///
227 /// EXAMPLE:
228 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
229 /// ```rust
230 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
231 /// use strict_path::PathBoundary;
232 /// let tmp_dir = tempfile::tempdir()?;
233 /// let tmp_dir = PathBoundary::<()>::try_new(tmp_dir)?; // Clean variable shadowing
234 /// # Ok(())
235 /// # }
236 /// ```
237 #[inline]
238 pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
239 let restriction_path = restriction_path.as_ref();
240 let raw = PathHistory::<Raw>::new(restriction_path);
241
242 let canonicalized = raw.canonicalize()?;
243
244 let verified_exists = match canonicalized.verify_exists() {
245 Some(path) => path,
246 None => {
247 let io = IoError::new(
248 ErrorKind::NotFound,
249 "The specified PathBoundary path does not exist.",
250 );
251 return Err(StrictPathError::invalid_restriction(
252 restriction_path.to_path_buf(),
253 io,
254 ));
255 }
256 };
257
258 if !verified_exists.is_dir() {
259 let error = IoError::new(
260 ErrorKind::InvalidInput,
261 "The specified PathBoundary path exists but is not a directory.",
262 );
263 return Err(StrictPathError::invalid_restriction(
264 restriction_path.to_path_buf(),
265 error,
266 ));
267 }
268
269 #[cfg(feature = "tempfile")]
270 {
271 Ok(Self::new_with_temp_dir(Arc::new(verified_exists), None))
272 }
273 #[cfg(not(feature = "tempfile"))]
274 {
275 Ok(Self {
276 path: Arc::new(verified_exists),
277 _marker: PhantomData,
278 })
279 }
280 }
281
282 /// Creates the directory if missing, then constructs a new `PathBoundary`.
283 ///
284 /// SUMMARY:
285 /// Ensure the root exists (create if missing) and construct a new boundary.
286 ///
287 /// PARAMETERS:
288 /// - `root` (`AsRef<Path>`): Directory to create if needed and use as boundary root.
289 ///
290 /// RETURNS:
291 /// - `Result<PathBoundary<Marker>>`: New boundary anchored at `root`.
292 ///
293 /// ERRORS:
294 /// - `StrictPathError::InvalidRestriction`: Directory creation/canonicalization fails.
295 ///
296 /// EXAMPLE:
297 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
298 /// ```rust
299 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
300 /// use strict_path::PathBoundary;
301 /// let tmp_dir = tempfile::tempdir()?;
302 /// let tmp_dir = PathBoundary::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
303 /// # Ok(())
304 /// # }
305 /// ```
306 pub fn try_new_create<P: AsRef<Path>>(root: P) -> Result<Self> {
307 let root_path = root.as_ref();
308 if !root_path.exists() {
309 std::fs::create_dir_all(root_path)
310 .map_err(|e| StrictPathError::invalid_restriction(root_path.to_path_buf(), e))?;
311 }
312 Self::try_new(root_path)
313 }
314
315 /// SUMMARY:
316 /// Join a candidate path to the boundary and return a validated `StrictPath`.
317 ///
318 /// PARAMETERS:
319 /// - `candidate_path` (`AsRef<Path>`): Absolute or relative path to validate within this boundary.
320 ///
321 /// RETURNS:
322 /// - `Result<StrictPath<Marker>>`: Canonicalized, boundary-checked path.
323 ///
324 /// ERRORS:
325 /// - `StrictPathError::WindowsShortName` (windows), `StrictPathError::PathResolutionError`,
326 /// `StrictPathError::PathEscapesBoundary`.
327 #[inline]
328 pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
329 canonicalize_and_enforce_restriction_boundary(candidate_path, self)
330 }
331
332 /// Returns the canonicalized PathBoundary root path. Kept crate-private to avoid leaking raw path.
333 #[inline]
334 pub(crate) fn path(&self) -> &Path {
335 self.path.as_ref()
336 }
337
338 /// Internal: returns the canonicalized PathHistory of the PathBoundary root for boundary checks.
339 #[inline]
340 pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
341 &self.path
342 }
343
344 /// Returns true if the PathBoundary root exists.
345 ///
346 /// This is always true for a constructed PathBoundary, but we query the filesystem for robustness.
347 #[inline]
348 pub fn exists(&self) -> bool {
349 self.path.exists()
350 }
351
352 /// SUMMARY:
353 /// Return the root path as `&OsStr` for `AsRef<Path>` interop (no allocation).
354 #[inline]
355 pub fn interop_path(&self) -> &std::ffi::OsStr {
356 self.path.as_os_str()
357 }
358
359 /// Returns a Display wrapper that shows the PathBoundary root system path.
360 #[inline]
361 pub fn strictpath_display(&self) -> std::path::Display<'_> {
362 self.path().display()
363 }
364
365 /// Internal helper: exposes the tempfile RAII handle so `VirtualRoot` constructors can mirror cleanup semantics when constructed from temporary directories.
366 #[cfg(feature = "tempfile")]
367 #[inline]
368 pub(crate) fn temp_dir_arc(&self) -> Option<Arc<TempDir>> {
369 self._temp_dir.clone()
370 }
371
372 /// SUMMARY:
373 /// Return filesystem metadata for the boundary root.
374 #[inline]
375 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
376 std::fs::metadata(self.path())
377 }
378
379 /// SUMMARY:
380 /// Create a symbolic link at `link_path` pointing to this boundary's root.
381 ///
382 /// PARAMETERS:
383 /// - `link_path` (&`StrictPath<Marker>`): Destination for the symlink, within the same boundary.
384 ///
385 /// RETURNS:
386 /// - `io::Result<()>`: Mirrors std semantics.
387 pub fn strict_symlink(
388 &self,
389 link_path: &crate::path::strict_path::StrictPath<Marker>,
390 ) -> std::io::Result<()> {
391 let root = self
392 .strict_join("")
393 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
394
395 root.strict_symlink(link_path)
396 }
397
398 /// SUMMARY:
399 /// Create a hard link at `link_path` pointing to this boundary's root.
400 ///
401 /// PARAMETERS and RETURNS mirror `strict_symlink`.
402 pub fn strict_hard_link(
403 &self,
404 link_path: &crate::path::strict_path::StrictPath<Marker>,
405 ) -> std::io::Result<()> {
406 let root = self
407 .strict_join("")
408 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
409
410 root.strict_hard_link(link_path)
411 }
412
413 /// SUMMARY:
414 /// Read directory entries under the boundary root (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 root 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 root directory and 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 #[inline]
437 pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
438 crate::VirtualRoot {
439 root: self,
440 #[cfg(feature = "tempfile")]
441 _temp_dir: None,
442 _marker: PhantomData,
443 }
444 }
445
446 // Note: Do not add new crate-private helpers unless necessary; use existing flows.
447
448 // OS Standard Directory Constructors
449 //
450 // These constructors provide secure access to operating system standard directories
451 // following platform-specific conventions (XDG on Linux, Known Folder API on Windows,
452 // Apple Standard Directories on macOS). Each creates an app-specific subdirectory
453 // and enforces path boundaries for secure file operations.
454
455 /// Creates a PathBoundary in the OS standard config directory for the given application.
456 ///
457 /// **Cross-Platform Behavior:**
458 /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
459 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
460 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
461 ///
462 /// Respects environment variables like `$XDG_CONFIG_HOME` on Linux systems.
463 #[cfg(feature = "dirs")]
464 pub fn try_new_os_config(app_name: &str) -> Result<Self> {
465 let config_dir = dirs::config_dir()
466 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
467 restriction: "os-config".into(),
468 source: std::io::Error::new(
469 std::io::ErrorKind::NotFound,
470 "OS config directory not available",
471 ),
472 })?
473 .join(app_name);
474 Self::try_new_create(config_dir)
475 }
476
477 /// Creates a PathBoundary in the OS standard data directory for the given application.
478 ///
479 /// **Cross-Platform Behavior:**
480 /// - **Linux**: `~/.local/share/{app_name}` (XDG Base Directory Specification)
481 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
482 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
483 ///
484 /// Respects environment variables like `$XDG_DATA_HOME` on Linux systems.
485 #[cfg(feature = "dirs")]
486 pub fn try_new_os_data(app_name: &str) -> Result<Self> {
487 let data_dir = dirs::data_dir()
488 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
489 restriction: "os-data".into(),
490 source: std::io::Error::new(
491 std::io::ErrorKind::NotFound,
492 "OS data directory not available",
493 ),
494 })?
495 .join(app_name);
496 Self::try_new_create(data_dir)
497 }
498
499 /// Creates a PathBoundary in the OS standard cache directory for the given application.
500 ///
501 /// **Cross-Platform Behavior:**
502 /// - **Linux**: `~/.cache/{app_name}` (XDG Base Directory Specification)
503 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
504 /// - **macOS**: `~/Library/Caches/{app_name}` (Apple Standard Directories)
505 ///
506 /// Respects environment variables like `$XDG_CACHE_HOME` on Linux systems.
507 #[cfg(feature = "dirs")]
508 pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
509 let cache_dir = dirs::cache_dir()
510 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
511 restriction: "os-cache".into(),
512 source: std::io::Error::new(
513 std::io::ErrorKind::NotFound,
514 "OS cache directory not available",
515 ),
516 })?
517 .join(app_name);
518 Self::try_new_create(cache_dir)
519 }
520
521 /// Creates a PathBoundary in the OS local config directory (non-roaming on Windows).
522 ///
523 /// **Cross-Platform Behavior:**
524 /// - **Linux**: `~/.config/{app_name}` (same as config_dir)
525 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
526 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as config_dir)
527 #[cfg(feature = "dirs")]
528 pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
529 let config_dir = dirs::config_local_dir()
530 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
531 restriction: "os-config-local".into(),
532 source: std::io::Error::new(
533 std::io::ErrorKind::NotFound,
534 "OS local config directory not available",
535 ),
536 })?
537 .join(app_name);
538 Self::try_new_create(config_dir)
539 }
540
541 /// Creates a PathBoundary in the OS local data directory.
542 ///
543 /// **Cross-Platform Behavior:**
544 /// - **Linux**: `~/.local/share/{app_name}` (same as data_dir)
545 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
546 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as data_dir)
547 #[cfg(feature = "dirs")]
548 pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
549 let data_dir = dirs::data_local_dir()
550 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
551 restriction: "os-data-local".into(),
552 source: std::io::Error::new(
553 std::io::ErrorKind::NotFound,
554 "OS local data directory not available",
555 ),
556 })?
557 .join(app_name);
558 Self::try_new_create(data_dir)
559 }
560
561 /// Creates a PathBoundary in the user's home directory.
562 ///
563 /// **Cross-Platform Behavior:**
564 /// - **Linux**: `$HOME`
565 /// - **Windows**: `%USERPROFILE%` (e.g., `C:\Users\Username`)
566 /// - **macOS**: `$HOME`
567 #[cfg(feature = "dirs")]
568 pub fn try_new_os_home() -> Result<Self> {
569 let home_dir =
570 dirs::home_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
571 restriction: "os-home".into(),
572 source: std::io::Error::new(
573 std::io::ErrorKind::NotFound,
574 "OS home directory not available",
575 ),
576 })?;
577 Self::try_new(home_dir)
578 }
579
580 /// Creates a PathBoundary in the user's desktop directory.
581 ///
582 /// **Cross-Platform Behavior:**
583 /// - **Linux**: `$HOME/Desktop` or XDG_DESKTOP_DIR
584 /// - **Windows**: `%USERPROFILE%\Desktop`
585 /// - **macOS**: `$HOME/Desktop`
586 #[cfg(feature = "dirs")]
587 pub fn try_new_os_desktop() -> Result<Self> {
588 let desktop_dir =
589 dirs::desktop_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
590 restriction: "os-desktop".into(),
591 source: std::io::Error::new(
592 std::io::ErrorKind::NotFound,
593 "OS desktop directory not available",
594 ),
595 })?;
596 Self::try_new(desktop_dir)
597 }
598
599 /// Creates a PathBoundary in the user's documents directory.
600 ///
601 /// **Cross-Platform Behavior:**
602 /// - **Linux**: `$HOME/Documents` or XDG_DOCUMENTS_DIR
603 /// - **Windows**: `%USERPROFILE%\Documents`
604 /// - **macOS**: `$HOME/Documents`
605 #[cfg(feature = "dirs")]
606 pub fn try_new_os_documents() -> Result<Self> {
607 let docs_dir =
608 dirs::document_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
609 restriction: "os-documents".into(),
610 source: std::io::Error::new(
611 std::io::ErrorKind::NotFound,
612 "OS documents directory not available",
613 ),
614 })?;
615 Self::try_new(docs_dir)
616 }
617
618 /// Creates a PathBoundary in the user's downloads directory.
619 ///
620 /// **Cross-Platform Behavior:**
621 /// - **Linux**: `$HOME/Downloads` or XDG_DOWNLOAD_DIR
622 /// - **Windows**: `%USERPROFILE%\Downloads`
623 /// - **macOS**: `$HOME/Downloads`
624 #[cfg(feature = "dirs")]
625 pub fn try_new_os_downloads() -> Result<Self> {
626 let downloads_dir =
627 dirs::download_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
628 restriction: "os-downloads".into(),
629 source: std::io::Error::new(
630 std::io::ErrorKind::NotFound,
631 "OS downloads directory not available",
632 ),
633 })?;
634 Self::try_new(downloads_dir)
635 }
636
637 /// Creates a PathBoundary in the user's pictures directory.
638 ///
639 /// **Cross-Platform Behavior:**
640 /// - **Linux**: `$HOME/Pictures` or XDG_PICTURES_DIR
641 /// - **Windows**: `%USERPROFILE%\Pictures`
642 /// - **macOS**: `$HOME/Pictures`
643 #[cfg(feature = "dirs")]
644 pub fn try_new_os_pictures() -> Result<Self> {
645 let pictures_dir =
646 dirs::picture_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
647 restriction: "os-pictures".into(),
648 source: std::io::Error::new(
649 std::io::ErrorKind::NotFound,
650 "OS pictures directory not available",
651 ),
652 })?;
653 Self::try_new(pictures_dir)
654 }
655
656 /// Creates a PathBoundary in the user's music/audio directory.
657 ///
658 /// **Cross-Platform Behavior:**
659 /// - **Linux**: `$HOME/Music` or XDG_MUSIC_DIR
660 /// - **Windows**: `%USERPROFILE%\Music`
661 /// - **macOS**: `$HOME/Music`
662 #[cfg(feature = "dirs")]
663 pub fn try_new_os_audio() -> Result<Self> {
664 let audio_dir =
665 dirs::audio_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
666 restriction: "os-audio".into(),
667 source: std::io::Error::new(
668 std::io::ErrorKind::NotFound,
669 "OS audio directory not available",
670 ),
671 })?;
672 Self::try_new(audio_dir)
673 }
674
675 /// Creates a PathBoundary in the user's videos directory.
676 ///
677 /// **Cross-Platform Behavior:**
678 /// - **Linux**: `$HOME/Videos` or XDG_VIDEOS_DIR
679 /// - **Windows**: `%USERPROFILE%\Videos`
680 /// - **macOS**: `$HOME/Movies`
681 #[cfg(feature = "dirs")]
682 pub fn try_new_os_videos() -> Result<Self> {
683 let videos_dir =
684 dirs::video_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
685 restriction: "os-videos".into(),
686 source: std::io::Error::new(
687 std::io::ErrorKind::NotFound,
688 "OS videos directory not available",
689 ),
690 })?;
691 Self::try_new(videos_dir)
692 }
693
694 /// Creates a PathBoundary in the OS executable directory (Linux only).
695 ///
696 /// **Platform Availability:**
697 /// - **Linux**: `~/.local/bin` or $XDG_BIN_HOME
698 /// - **Windows**: Returns error (not available)
699 /// - **macOS**: Returns error (not available)
700 #[cfg(feature = "dirs")]
701 pub fn try_new_os_executables() -> Result<Self> {
702 let exec_dir =
703 dirs::executable_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
704 restriction: "os-executables".into(),
705 source: std::io::Error::new(
706 std::io::ErrorKind::NotFound,
707 "OS executables directory not available on this platform",
708 ),
709 })?;
710 Self::try_new(exec_dir)
711 }
712
713 /// Creates a PathBoundary in the OS runtime directory (Linux only).
714 ///
715 /// **Platform Availability:**
716 /// - **Linux**: `$XDG_RUNTIME_DIR` (session-specific, user-only access)
717 /// - **Windows**: Returns error (not available)
718 /// - **macOS**: Returns error (not available)
719 #[cfg(feature = "dirs")]
720 pub fn try_new_os_runtime() -> Result<Self> {
721 let runtime_dir =
722 dirs::runtime_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
723 restriction: "os-runtime".into(),
724 source: std::io::Error::new(
725 std::io::ErrorKind::NotFound,
726 "OS runtime directory not available on this platform",
727 ),
728 })?;
729 Self::try_new(runtime_dir)
730 }
731
732 /// Creates a PathBoundary in the OS state directory (Linux only).
733 ///
734 /// **Platform Availability:**
735 /// - **Linux**: `~/.local/state/{app_name}` or $XDG_STATE_HOME/{app_name}
736 /// - **Windows**: Returns error (not available)
737 /// - **macOS**: Returns error (not available)
738 #[cfg(feature = "dirs")]
739 pub fn try_new_os_state(app_name: &str) -> Result<Self> {
740 let state_dir = dirs::state_dir()
741 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
742 restriction: "os-state".into(),
743 source: std::io::Error::new(
744 std::io::ErrorKind::NotFound,
745 "OS state directory not available on this platform",
746 ),
747 })?
748 .join(app_name);
749 Self::try_new_create(state_dir)
750 }
751
752 /// Creates a PathBoundary in a unique temporary directory with RAII cleanup.
753 ///
754 /// Returns a `StrictPath` pointing to the temp directory root. The directory
755 /// will be automatically cleaned up when the `StrictPath` is dropped.
756 ///
757 /// # Example
758 /// ```
759 /// # #[cfg(feature = "tempfile")] {
760 /// use strict_path::PathBoundary;
761 ///
762 /// // Get a validated temp directory path directly
763 /// let temp_root = PathBoundary::<()>::try_new_temp()?;
764 /// let user_input = "uploads/document.pdf";
765 /// let validated_path = temp_root.strict_join(user_input)?; // Returns StrictPath
766 /// // Ensure parent directories exist before writing
767 /// validated_path.create_parent_dir_all()?;
768 /// std::fs::write(validated_path.interop_path(), b"content")?; // Direct filesystem access
769 /// // temp_root is dropped here, directory gets cleaned up automatically
770 /// # }
771 /// # Ok::<(), Box<dyn std::error::Error>>(())
772 /// ```
773 #[cfg(feature = "tempfile")]
774 pub fn try_new_temp() -> Result<Self> {
775 let temp_dir =
776 tempfile::tempdir().map_err(|e| crate::StrictPathError::InvalidRestriction {
777 restriction: "temp".into(),
778 source: e,
779 })?;
780
781 let temp_path = temp_dir.path();
782 let raw = PathHistory::<Raw>::new(temp_path);
783 let canonicalized = raw.canonicalize()?;
784 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
785 crate::StrictPathError::InvalidRestriction {
786 restriction: "temp".into(),
787 source: std::io::Error::new(
788 std::io::ErrorKind::NotFound,
789 "Temp directory verification failed",
790 ),
791 }
792 })?;
793
794 Ok(Self::new_with_temp_dir(
795 Arc::new(verified_exists),
796 Some(Arc::new(temp_dir)),
797 ))
798 }
799
800 /// Creates a PathBoundary in a temporary directory with a custom prefix and RAII cleanup.
801 ///
802 /// Returns a `StrictPath` pointing to the temp directory root. The directory
803 /// will be automatically cleaned up when the `StrictPath` is dropped.
804 ///
805 /// # Example
806 /// ```
807 /// # #[cfg(feature = "tempfile")] {
808 /// use strict_path::PathBoundary;
809 ///
810 /// // Get a validated temp directory path with session prefix
811 /// let upload_root = PathBoundary::<()>::try_new_temp_with_prefix("upload_batch")?;
812 /// let user_file = upload_root.strict_join("user_document.pdf")?; // Validate path
813 /// // Process validated path with direct filesystem operations
814 /// // upload_root is dropped here, directory gets cleaned up automatically
815 /// # }
816 /// # Ok::<(), Box<dyn std::error::Error>>(())
817 /// ```
818 #[cfg(feature = "tempfile")]
819 pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
820 let temp_dir = tempfile::Builder::new()
821 .prefix(prefix)
822 .tempdir()
823 .map_err(|e| crate::StrictPathError::InvalidRestriction {
824 restriction: "temp".into(),
825 source: e,
826 })?;
827
828 let temp_path = temp_dir.path();
829 let raw = PathHistory::<Raw>::new(temp_path);
830 let canonicalized = raw.canonicalize()?;
831 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
832 crate::StrictPathError::InvalidRestriction {
833 restriction: "temp".into(),
834 source: std::io::Error::new(
835 std::io::ErrorKind::NotFound,
836 "Temp directory verification failed",
837 ),
838 }
839 })?;
840
841 Ok(Self::new_with_temp_dir(
842 Arc::new(verified_exists),
843 Some(Arc::new(temp_dir)),
844 ))
845 }
846
847 /// SUMMARY:
848 /// Create a boundary using `app-path` semantics (portable app-relative directory) with optional env override.
849 ///
850 /// PARAMETERS:
851 /// - `subdir` (`AsRef<Path>`): Subdirectory path relative to the executable (or override directory).
852 /// - `env_override` (Option<&str>): Optional environment variable name; when present and set,
853 /// its value is used as the base directory instead of the executable directory.
854 ///
855 /// RETURNS:
856 /// - `Result<PathBoundary<Marker>>`: Created/validated boundary at the resolved app-path location.
857 ///
858 /// ERRORS:
859 /// - `StrictPathError::InvalidRestriction`: If resolution fails or directory cannot be created/validated.
860 ///
861 /// EXAMPLE:
862 /// ```
863 /// # #[cfg(feature = "app-path")] {
864 /// use strict_path::PathBoundary;
865 ///
866 /// // Creates ./config/ relative to executable
867 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", None)?;
868 ///
869 /// // With environment override (checks MYAPP_CONFIG_DIR first)
870 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", Some("MYAPP_CONFIG_DIR"))?;
871 /// # }
872 /// # Ok::<(), Box<dyn std::error::Error>>(())
873 /// ```
874 #[cfg(feature = "app-path")]
875 pub fn try_new_app_path<P: AsRef<std::path::Path>>(
876 subdir: P,
877 env_override: Option<&str>,
878 ) -> Result<Self> {
879 let subdir_path = subdir.as_ref();
880 // Resolve the override environment variable name (if provided) to its value.
881 // app-path expects the override PATH value, not the variable name.
882 let override_value: Option<String> = env_override.and_then(|key| std::env::var(key).ok());
883 let app_path = app_path::AppPath::try_with_override(subdir_path, override_value.as_deref())
884 .map_err(|e| crate::StrictPathError::InvalidRestriction {
885 restriction: format!("app-path: {}", subdir_path.display()).into(),
886 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
887 })?;
888
889 Self::try_new_create(app_path)
890 }
891
892 /// SUMMARY:
893 /// Create a boundary using `app-path`, always consulting a specific environment variable first.
894 ///
895 /// PARAMETERS:
896 /// - `subdir` (`AsRef<Path>`): Subdirectory used with `app-path` resolution.
897 /// - `env_override` (&str): Environment variable name to check for a base directory.
898 ///
899 /// RETURNS:
900 /// - `Result<PathBoundary<Marker>>`: New boundary anchored using `app-path` semantics.
901 ///
902 /// ERRORS:
903 /// - `StrictPathError::InvalidRestriction`: If resolution fails or the directory can't be created/validated.
904 #[cfg(feature = "app-path")]
905 pub fn try_new_app_path_with_env<P: AsRef<std::path::Path>>(
906 subdir: P,
907 env_override: &str,
908 ) -> Result<Self> {
909 let subdir_path = subdir.as_ref();
910 Self::try_new_app_path(subdir_path, Some(env_override))
911 }
912}
913
914impl<Marker> AsRef<Path> for PathBoundary<Marker> {
915 #[inline]
916 fn as_ref(&self) -> &Path {
917 // PathHistory implements AsRef<Path>, so forward to it
918 self.path.as_ref()
919 }
920}
921
922impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
923 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
924 f.debug_struct("PathBoundary")
925 .field("path", &self.path.as_ref())
926 .field("marker", &std::any::type_name::<Marker>())
927 .finish()
928 }
929}
930
931impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
932 type Err = crate::StrictPathError;
933
934 /// Parse a PathBoundary from a string path for universal ergonomics.
935 ///
936 /// Creates the directory if it doesn't exist, enabling seamless integration
937 /// with any string-parsing context (clap, config files, environment variables, etc.):
938 /// ```rust
939 /// # use strict_path::PathBoundary;
940 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
941 /// let temp_dir = tempfile::tempdir()?;
942 /// let safe_path = temp_dir.path().join("safe_dir");
943 /// let boundary: PathBoundary<()> = safe_path.to_string_lossy().parse()?;
944 /// assert!(safe_path.exists());
945 /// # Ok(())
946 /// # }
947 /// ```
948 #[inline]
949 fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
950 Self::try_new_create(path)
951 }
952}
953//