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/// Canonicalize a candidate path and enforce the PathBoundary boundary, returning a `StrictPath`.
34///
35/// What this does:
36/// - Windows prefilter: rejects DOS 8.3 short-name segments (e.g., `PROGRA~1`) in relative inputs
37/// to avoid aliasing-based escapes before any filesystem calls.
38/// - Input interpretation: absolute inputs are validated as-is; relative inputs are joined under
39/// the PathBoundary root.
40/// - Resolution: canonicalizes the composed path, fully resolving `.`/`..`, symlinks/junctions,
41/// and platform prefixes.
42/// - Boundary enforcement: verifies the canonicalized result is strictly within the PathBoundary's
43/// canonicalized root; rejects any resolution that would escape the boundary.
44/// - Returns: a `StrictPath<Marker>` that borrows the PathBoundary and holds the validated system path.
45pub(crate) fn canonicalize_and_enforce_restriction_boundary<Marker>(
46 path: impl AsRef<Path>,
47 restriction: &PathBoundary<Marker>,
48) -> Result<StrictPath<Marker>> {
49 #[cfg(windows)]
50 {
51 let original_user_path = path.as_ref().to_path_buf();
52 if !path.as_ref().is_absolute() {
53 let mut probe = restriction.path().to_path_buf();
54 for comp in path.as_ref().components() {
55 match comp {
56 Component::CurDir | Component::ParentDir => continue,
57 Component::RootDir | Component::Prefix(_) => continue,
58 Component::Normal(name) => {
59 if is_potential_83_short_name(name) {
60 return Err(StrictPathError::windows_short_name(
61 name.to_os_string(),
62 original_user_path,
63 probe.clone(),
64 ));
65 }
66 probe.push(name);
67 }
68 }
69 }
70 }
71 }
72
73 let target_path = if path.as_ref().is_absolute() {
74 path.as_ref().to_path_buf()
75 } else {
76 restriction.path().join(path.as_ref())
77 };
78
79 let validated_path = PathHistory::<Raw>::new(target_path)
80 .canonicalize()?
81 .boundary_check(&restriction.path)?;
82
83 Ok(StrictPath::new(
84 Arc::new(restriction.clone()),
85 validated_path,
86 ))
87}
88
89/// A path boundary that serves as the secure foundation for validated path operations.
90///
91/// `PathBoundary` represents the trusted starting point (like `/home/users/alice`) from which
92/// all path operations begin. When you call `path_boundary.strict_join("documents/file.txt")`,
93/// you're building outward from this secure boundary with validated path construction.
94pub struct PathBoundary<Marker = ()> {
95 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
96 #[cfg(feature = "tempfile")]
97 _temp_dir: Option<Arc<TempDir>>,
98 _marker: PhantomData<Marker>,
99}
100
101impl<Marker> Clone for PathBoundary<Marker> {
102 fn clone(&self) -> Self {
103 Self {
104 path: self.path.clone(),
105 #[cfg(feature = "tempfile")]
106 _temp_dir: self._temp_dir.clone(),
107 _marker: PhantomData,
108 }
109 }
110}
111
112impl<Marker> PartialEq for PathBoundary<Marker> {
113 #[inline]
114 fn eq(&self, other: &Self) -> bool {
115 self.path() == other.path()
116 }
117}
118
119impl<Marker> Eq for PathBoundary<Marker> {}
120
121impl<Marker> std::hash::Hash for PathBoundary<Marker> {
122 #[inline]
123 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
124 self.path().hash(state);
125 }
126}
127
128impl<Marker> PartialOrd for PathBoundary<Marker> {
129 #[inline]
130 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
131 Some(self.cmp(other))
132 }
133}
134
135impl<Marker> Ord for PathBoundary<Marker> {
136 #[inline]
137 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
138 self.path().cmp(other.path())
139 }
140}
141
142impl<Marker> PartialEq<crate::validator::virtual_root::VirtualRoot<Marker>>
143 for PathBoundary<Marker>
144{
145 #[inline]
146 fn eq(&self, other: &crate::validator::virtual_root::VirtualRoot<Marker>) -> bool {
147 self.path() == other.path()
148 }
149}
150
151impl<Marker> PartialEq<Path> for PathBoundary<Marker> {
152 #[inline]
153 fn eq(&self, other: &Path) -> bool {
154 self.path() == other
155 }
156}
157
158impl<Marker> PartialEq<std::path::PathBuf> for PathBoundary<Marker> {
159 #[inline]
160 fn eq(&self, other: &std::path::PathBuf) -> bool {
161 self.eq(other.as_path())
162 }
163}
164
165impl<Marker> PartialEq<&std::path::Path> for PathBoundary<Marker> {
166 #[inline]
167 fn eq(&self, other: &&std::path::Path) -> bool {
168 self.eq(*other)
169 }
170}
171
172impl<Marker> PathBoundary<Marker> {
173 /// Private constructor that allows setting the temp_dir during construction
174 #[cfg(feature = "tempfile")]
175 fn new_with_temp_dir(
176 path: Arc<PathHistory<((Raw, Canonicalized), Exists)>>,
177 temp_dir: Option<Arc<TempDir>>,
178 ) -> Self {
179 Self {
180 path,
181 _temp_dir: temp_dir,
182 _marker: PhantomData,
183 }
184 }
185
186 /// Creates a new `PathBoundary` rooted at `restriction_path` (which must already exist and be a directory).
187 ///
188 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
189 /// ```rust
190 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
191 /// use strict_path::PathBoundary;
192 /// let tmp_dir = tempfile::tempdir()?;
193 /// let tmp_dir = PathBoundary::<()>::try_new(tmp_dir)?; // Clean variable shadowing
194 /// # Ok(())
195 /// # }
196 /// ```
197 #[inline]
198 pub fn try_new<P: AsRef<Path>>(restriction_path: P) -> Result<Self> {
199 let restriction_path = restriction_path.as_ref();
200 let raw = PathHistory::<Raw>::new(restriction_path);
201
202 let canonicalized = raw.canonicalize()?;
203
204 let verified_exists = match canonicalized.verify_exists() {
205 Some(path) => path,
206 None => {
207 let io = IoError::new(
208 ErrorKind::NotFound,
209 "The specified PathBoundary path does not exist.",
210 );
211 return Err(StrictPathError::invalid_restriction(
212 restriction_path.to_path_buf(),
213 io,
214 ));
215 }
216 };
217
218 if !verified_exists.is_dir() {
219 let error = IoError::new(
220 ErrorKind::InvalidInput,
221 "The specified PathBoundary path exists but is not a directory.",
222 );
223 return Err(StrictPathError::invalid_restriction(
224 restriction_path.to_path_buf(),
225 error,
226 ));
227 }
228
229 #[cfg(feature = "tempfile")]
230 {
231 Ok(Self::new_with_temp_dir(Arc::new(verified_exists), None))
232 }
233 #[cfg(not(feature = "tempfile"))]
234 {
235 Ok(Self {
236 path: Arc::new(verified_exists),
237 _marker: PhantomData,
238 })
239 }
240 }
241
242 /// Creates the directory if missing, then constructs a new `PathBoundary`.
243 ///
244 /// Uses `AsRef<Path>` for maximum ergonomics, including direct `TempDir` support for clean shadowing patterns:
245 /// ```rust
246 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
247 /// use strict_path::PathBoundary;
248 /// let tmp_dir = tempfile::tempdir()?;
249 /// let tmp_dir = PathBoundary::<()>::try_new_create(tmp_dir)?; // Clean variable shadowing
250 /// # Ok(())
251 /// # }
252 /// ```
253 pub fn try_new_create<P: AsRef<Path>>(root: P) -> Result<Self> {
254 let root_path = root.as_ref();
255 if !root_path.exists() {
256 std::fs::create_dir_all(root_path)
257 .map_err(|e| StrictPathError::invalid_restriction(root_path.to_path_buf(), e))?;
258 }
259 Self::try_new(root_path)
260 }
261
262 /// Joins a path to this restrictor root and validates it remains within the restriction boundary.
263 ///
264 /// Accepts absolute or relative inputs; ensures the resulting path remains within the restriction.
265 #[inline]
266 pub fn strict_join(&self, candidate_path: impl AsRef<Path>) -> Result<StrictPath<Marker>> {
267 canonicalize_and_enforce_restriction_boundary(candidate_path, self)
268 }
269
270 /// Returns the canonicalized PathBoundary root path. Kept crate-private to avoid leaking raw path.
271 #[inline]
272 pub(crate) fn path(&self) -> &Path {
273 self.path.as_ref()
274 }
275
276 /// Internal: returns the canonicalized PathHistory of the PathBoundary root for boundary checks.
277 #[inline]
278 pub(crate) fn stated_path(&self) -> &PathHistory<((Raw, Canonicalized), Exists)> {
279 &self.path
280 }
281
282 /// Returns true if the PathBoundary root exists.
283 ///
284 /// This is always true for a constructed PathBoundary, but we query the filesystem for robustness.
285 #[inline]
286 pub fn exists(&self) -> bool {
287 self.path.exists()
288 }
289
290 /// Returns the PathBoundary root path for interop with `AsRef<Path>` APIs.
291 ///
292 /// This provides allocation-free, OS-native string access to the PathBoundary root
293 /// for use with standard library APIs that accept `AsRef<Path>`.
294 #[inline]
295 pub fn interop_path(&self) -> &std::ffi::OsStr {
296 self.path.as_os_str()
297 }
298
299 /// Returns a Display wrapper that shows the PathBoundary root system path.
300 #[inline]
301 pub fn strictpath_display(&self) -> std::path::Display<'_> {
302 self.path().display()
303 }
304
305 /// Internal helper: exposes the tempfile RAII handle so `VirtualRoot` constructors can mirror cleanup semantics when constructed from temporary directories.
306 #[cfg(feature = "tempfile")]
307 #[inline]
308 pub(crate) fn temp_dir_arc(&self) -> Option<Arc<TempDir>> {
309 self._temp_dir.clone()
310 }
311
312 /// Returns filesystem metadata for the PathBoundary root path.
313 #[inline]
314 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
315 std::fs::metadata(self.path())
316 }
317
318 /// Creates a symbolic link at `link_path` that points to this PathBoundary's root.
319 pub fn strict_symlink(
320 &self,
321 link_path: &crate::path::strict_path::StrictPath<Marker>,
322 ) -> std::io::Result<()> {
323 let root = self
324 .strict_join("")
325 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
326
327 root.strict_symlink(link_path)
328 }
329
330 /// Creates a hard link at `link_path` that points to this PathBoundary's root.
331 pub fn strict_hard_link(
332 &self,
333 link_path: &crate::path::strict_path::StrictPath<Marker>,
334 ) -> std::io::Result<()> {
335 let root = self
336 .strict_join("")
337 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
338
339 root.strict_hard_link(link_path)
340 }
341
342 /// Reads the directory entries under this PathBoundary root (like `std::fs::read_dir`).
343 ///
344 /// This is intended for discovery. Prefer collecting entry names and joining via
345 /// `strict_join`/`virtual_join` before performing I/O.
346 #[inline]
347 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
348 std::fs::read_dir(self.path())
349 }
350
351 /// Removes this PathBoundary root directory (non-recursive).
352 ///
353 /// Equivalent to `std::fs::remove_dir(root)`. Fails if the directory is not empty.
354 #[inline]
355 pub fn remove_dir(&self) -> std::io::Result<()> {
356 std::fs::remove_dir(self.path())
357 }
358
359 /// Recursively removes this PathBoundary root directory and all its contents.
360 ///
361 /// Equivalent to `std::fs::remove_dir_all(root)`.
362 #[inline]
363 pub fn remove_dir_all(&self) -> std::io::Result<()> {
364 std::fs::remove_dir_all(self.path())
365 }
366
367 /// Converts this `PathBoundary` into a `VirtualRoot`.
368 ///
369 /// This creates a virtual root view of the PathBoundary, allowing virtual path operations
370 /// that treat the PathBoundary root as the virtual filesystem root "/".
371 #[inline]
372 pub fn virtualize(self) -> crate::VirtualRoot<Marker> {
373 crate::VirtualRoot {
374 root: self,
375 #[cfg(feature = "tempfile")]
376 _temp_dir: None,
377 _marker: PhantomData,
378 }
379 }
380
381 // Note: Do not add new crate-private helpers unless necessary; use existing flows.
382
383 // OS Standard Directory Constructors
384 //
385 // These constructors provide secure access to operating system standard directories
386 // following platform-specific conventions (XDG on Linux, Known Folder API on Windows,
387 // Apple Standard Directories on macOS). Each creates an app-specific subdirectory
388 // and enforces path boundaries for secure file operations.
389
390 /// Creates a PathBoundary in the OS standard config directory for the given application.
391 ///
392 /// **Cross-Platform Behavior:**
393 /// - **Linux**: `~/.config/{app_name}` (XDG Base Directory Specification)
394 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
395 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
396 ///
397 /// Respects environment variables like `$XDG_CONFIG_HOME` on Linux systems.
398 #[cfg(feature = "dirs")]
399 pub fn try_new_os_config(app_name: &str) -> Result<Self> {
400 let config_dir = dirs::config_dir()
401 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
402 restriction: "os-config".into(),
403 source: std::io::Error::new(
404 std::io::ErrorKind::NotFound,
405 "OS config directory not available",
406 ),
407 })?
408 .join(app_name);
409 Self::try_new_create(config_dir)
410 }
411
412 /// Creates a PathBoundary in the OS standard data directory for the given application.
413 ///
414 /// **Cross-Platform Behavior:**
415 /// - **Linux**: `~/.local/share/{app_name}` (XDG Base Directory Specification)
416 /// - **Windows**: `%APPDATA%\{app_name}` (Known Folder API - Roaming AppData)
417 /// - **macOS**: `~/Library/Application Support/{app_name}` (Apple Standard Directories)
418 ///
419 /// Respects environment variables like `$XDG_DATA_HOME` on Linux systems.
420 #[cfg(feature = "dirs")]
421 pub fn try_new_os_data(app_name: &str) -> Result<Self> {
422 let data_dir = dirs::data_dir()
423 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
424 restriction: "os-data".into(),
425 source: std::io::Error::new(
426 std::io::ErrorKind::NotFound,
427 "OS data directory not available",
428 ),
429 })?
430 .join(app_name);
431 Self::try_new_create(data_dir)
432 }
433
434 /// Creates a PathBoundary in the OS standard cache directory for the given application.
435 ///
436 /// **Cross-Platform Behavior:**
437 /// - **Linux**: `~/.cache/{app_name}` (XDG Base Directory Specification)
438 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
439 /// - **macOS**: `~/Library/Caches/{app_name}` (Apple Standard Directories)
440 ///
441 /// Respects environment variables like `$XDG_CACHE_HOME` on Linux systems.
442 #[cfg(feature = "dirs")]
443 pub fn try_new_os_cache(app_name: &str) -> Result<Self> {
444 let cache_dir = dirs::cache_dir()
445 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
446 restriction: "os-cache".into(),
447 source: std::io::Error::new(
448 std::io::ErrorKind::NotFound,
449 "OS cache directory not available",
450 ),
451 })?
452 .join(app_name);
453 Self::try_new_create(cache_dir)
454 }
455
456 /// Creates a PathBoundary in the OS local config directory (non-roaming on Windows).
457 ///
458 /// **Cross-Platform Behavior:**
459 /// - **Linux**: `~/.config/{app_name}` (same as config_dir)
460 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
461 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as config_dir)
462 #[cfg(feature = "dirs")]
463 pub fn try_new_os_config_local(app_name: &str) -> Result<Self> {
464 let config_dir = dirs::config_local_dir()
465 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
466 restriction: "os-config-local".into(),
467 source: std::io::Error::new(
468 std::io::ErrorKind::NotFound,
469 "OS local config directory not available",
470 ),
471 })?
472 .join(app_name);
473 Self::try_new_create(config_dir)
474 }
475
476 /// Creates a PathBoundary in the OS local data directory.
477 ///
478 /// **Cross-Platform Behavior:**
479 /// - **Linux**: `~/.local/share/{app_name}` (same as data_dir)
480 /// - **Windows**: `%LOCALAPPDATA%\{app_name}` (Known Folder API - Local AppData)
481 /// - **macOS**: `~/Library/Application Support/{app_name}` (same as data_dir)
482 #[cfg(feature = "dirs")]
483 pub fn try_new_os_data_local(app_name: &str) -> Result<Self> {
484 let data_dir = dirs::data_local_dir()
485 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
486 restriction: "os-data-local".into(),
487 source: std::io::Error::new(
488 std::io::ErrorKind::NotFound,
489 "OS local data directory not available",
490 ),
491 })?
492 .join(app_name);
493 Self::try_new_create(data_dir)
494 }
495
496 /// Creates a PathBoundary in the user's home directory.
497 ///
498 /// **Cross-Platform Behavior:**
499 /// - **Linux**: `$HOME`
500 /// - **Windows**: `%USERPROFILE%` (e.g., `C:\Users\Username`)
501 /// - **macOS**: `$HOME`
502 #[cfg(feature = "dirs")]
503 pub fn try_new_os_home() -> Result<Self> {
504 let home_dir =
505 dirs::home_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
506 restriction: "os-home".into(),
507 source: std::io::Error::new(
508 std::io::ErrorKind::NotFound,
509 "OS home directory not available",
510 ),
511 })?;
512 Self::try_new(home_dir)
513 }
514
515 /// Creates a PathBoundary in the user's desktop directory.
516 ///
517 /// **Cross-Platform Behavior:**
518 /// - **Linux**: `$HOME/Desktop` or XDG_DESKTOP_DIR
519 /// - **Windows**: `%USERPROFILE%\Desktop`
520 /// - **macOS**: `$HOME/Desktop`
521 #[cfg(feature = "dirs")]
522 pub fn try_new_os_desktop() -> Result<Self> {
523 let desktop_dir =
524 dirs::desktop_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
525 restriction: "os-desktop".into(),
526 source: std::io::Error::new(
527 std::io::ErrorKind::NotFound,
528 "OS desktop directory not available",
529 ),
530 })?;
531 Self::try_new(desktop_dir)
532 }
533
534 /// Creates a PathBoundary in the user's documents directory.
535 ///
536 /// **Cross-Platform Behavior:**
537 /// - **Linux**: `$HOME/Documents` or XDG_DOCUMENTS_DIR
538 /// - **Windows**: `%USERPROFILE%\Documents`
539 /// - **macOS**: `$HOME/Documents`
540 #[cfg(feature = "dirs")]
541 pub fn try_new_os_documents() -> Result<Self> {
542 let docs_dir =
543 dirs::document_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
544 restriction: "os-documents".into(),
545 source: std::io::Error::new(
546 std::io::ErrorKind::NotFound,
547 "OS documents directory not available",
548 ),
549 })?;
550 Self::try_new(docs_dir)
551 }
552
553 /// Creates a PathBoundary in the user's downloads directory.
554 ///
555 /// **Cross-Platform Behavior:**
556 /// - **Linux**: `$HOME/Downloads` or XDG_DOWNLOAD_DIR
557 /// - **Windows**: `%USERPROFILE%\Downloads`
558 /// - **macOS**: `$HOME/Downloads`
559 #[cfg(feature = "dirs")]
560 pub fn try_new_os_downloads() -> Result<Self> {
561 let downloads_dir =
562 dirs::download_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
563 restriction: "os-downloads".into(),
564 source: std::io::Error::new(
565 std::io::ErrorKind::NotFound,
566 "OS downloads directory not available",
567 ),
568 })?;
569 Self::try_new(downloads_dir)
570 }
571
572 /// Creates a PathBoundary in the user's pictures directory.
573 ///
574 /// **Cross-Platform Behavior:**
575 /// - **Linux**: `$HOME/Pictures` or XDG_PICTURES_DIR
576 /// - **Windows**: `%USERPROFILE%\Pictures`
577 /// - **macOS**: `$HOME/Pictures`
578 #[cfg(feature = "dirs")]
579 pub fn try_new_os_pictures() -> Result<Self> {
580 let pictures_dir =
581 dirs::picture_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
582 restriction: "os-pictures".into(),
583 source: std::io::Error::new(
584 std::io::ErrorKind::NotFound,
585 "OS pictures directory not available",
586 ),
587 })?;
588 Self::try_new(pictures_dir)
589 }
590
591 /// Creates a PathBoundary in the user's music/audio directory.
592 ///
593 /// **Cross-Platform Behavior:**
594 /// - **Linux**: `$HOME/Music` or XDG_MUSIC_DIR
595 /// - **Windows**: `%USERPROFILE%\Music`
596 /// - **macOS**: `$HOME/Music`
597 #[cfg(feature = "dirs")]
598 pub fn try_new_os_audio() -> Result<Self> {
599 let audio_dir =
600 dirs::audio_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
601 restriction: "os-audio".into(),
602 source: std::io::Error::new(
603 std::io::ErrorKind::NotFound,
604 "OS audio directory not available",
605 ),
606 })?;
607 Self::try_new(audio_dir)
608 }
609
610 /// Creates a PathBoundary in the user's videos directory.
611 ///
612 /// **Cross-Platform Behavior:**
613 /// - **Linux**: `$HOME/Videos` or XDG_VIDEOS_DIR
614 /// - **Windows**: `%USERPROFILE%\Videos`
615 /// - **macOS**: `$HOME/Movies`
616 #[cfg(feature = "dirs")]
617 pub fn try_new_os_videos() -> Result<Self> {
618 let videos_dir =
619 dirs::video_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
620 restriction: "os-videos".into(),
621 source: std::io::Error::new(
622 std::io::ErrorKind::NotFound,
623 "OS videos directory not available",
624 ),
625 })?;
626 Self::try_new(videos_dir)
627 }
628
629 /// Creates a PathBoundary in the OS executable directory (Linux only).
630 ///
631 /// **Platform Availability:**
632 /// - **Linux**: `~/.local/bin` or $XDG_BIN_HOME
633 /// - **Windows**: Returns error (not available)
634 /// - **macOS**: Returns error (not available)
635 #[cfg(feature = "dirs")]
636 pub fn try_new_os_executables() -> Result<Self> {
637 let exec_dir =
638 dirs::executable_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
639 restriction: "os-executables".into(),
640 source: std::io::Error::new(
641 std::io::ErrorKind::NotFound,
642 "OS executables directory not available on this platform",
643 ),
644 })?;
645 Self::try_new(exec_dir)
646 }
647
648 /// Creates a PathBoundary in the OS runtime directory (Linux only).
649 ///
650 /// **Platform Availability:**
651 /// - **Linux**: `$XDG_RUNTIME_DIR` (session-specific, user-only access)
652 /// - **Windows**: Returns error (not available)
653 /// - **macOS**: Returns error (not available)
654 #[cfg(feature = "dirs")]
655 pub fn try_new_os_runtime() -> Result<Self> {
656 let runtime_dir =
657 dirs::runtime_dir().ok_or_else(|| crate::StrictPathError::InvalidRestriction {
658 restriction: "os-runtime".into(),
659 source: std::io::Error::new(
660 std::io::ErrorKind::NotFound,
661 "OS runtime directory not available on this platform",
662 ),
663 })?;
664 Self::try_new(runtime_dir)
665 }
666
667 /// Creates a PathBoundary in the OS state directory (Linux only).
668 ///
669 /// **Platform Availability:**
670 /// - **Linux**: `~/.local/state/{app_name}` or $XDG_STATE_HOME/{app_name}
671 /// - **Windows**: Returns error (not available)
672 /// - **macOS**: Returns error (not available)
673 #[cfg(feature = "dirs")]
674 pub fn try_new_os_state(app_name: &str) -> Result<Self> {
675 let state_dir = dirs::state_dir()
676 .ok_or_else(|| crate::StrictPathError::InvalidRestriction {
677 restriction: "os-state".into(),
678 source: std::io::Error::new(
679 std::io::ErrorKind::NotFound,
680 "OS state directory not available on this platform",
681 ),
682 })?
683 .join(app_name);
684 Self::try_new_create(state_dir)
685 }
686
687 /// Creates a PathBoundary in a unique temporary directory with RAII cleanup.
688 ///
689 /// Returns a `StrictPath` pointing to the temp directory root. The directory
690 /// will be automatically cleaned up when the `StrictPath` is dropped.
691 ///
692 /// # Example
693 /// ```
694 /// # #[cfg(feature = "tempfile")] {
695 /// use strict_path::PathBoundary;
696 ///
697 /// // Get a validated temp directory path directly
698 /// let temp_root = PathBoundary::<()>::try_new_temp()?;
699 /// let user_input = "uploads/document.pdf";
700 /// let validated_path = temp_root.strict_join(user_input)?; // Returns StrictPath
701 /// // Ensure parent directories exist before writing
702 /// validated_path.create_parent_dir_all()?;
703 /// std::fs::write(validated_path.interop_path(), b"content")?; // Direct filesystem access
704 /// // temp_root is dropped here, directory gets cleaned up automatically
705 /// # }
706 /// # Ok::<(), Box<dyn std::error::Error>>(())
707 /// ```
708 #[cfg(feature = "tempfile")]
709 pub fn try_new_temp() -> Result<Self> {
710 let temp_dir =
711 tempfile::tempdir().map_err(|e| crate::StrictPathError::InvalidRestriction {
712 restriction: "temp".into(),
713 source: e,
714 })?;
715
716 let temp_path = temp_dir.path();
717 let raw = PathHistory::<Raw>::new(temp_path);
718 let canonicalized = raw.canonicalize()?;
719 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
720 crate::StrictPathError::InvalidRestriction {
721 restriction: "temp".into(),
722 source: std::io::Error::new(
723 std::io::ErrorKind::NotFound,
724 "Temp directory verification failed",
725 ),
726 }
727 })?;
728
729 Ok(Self::new_with_temp_dir(
730 Arc::new(verified_exists),
731 Some(Arc::new(temp_dir)),
732 ))
733 }
734
735 /// Creates a PathBoundary in a temporary directory with a custom prefix and RAII cleanup.
736 ///
737 /// Returns a `StrictPath` pointing to the temp directory root. The directory
738 /// will be automatically cleaned up when the `StrictPath` is dropped.
739 ///
740 /// # Example
741 /// ```
742 /// # #[cfg(feature = "tempfile")] {
743 /// use strict_path::PathBoundary;
744 ///
745 /// // Get a validated temp directory path with session prefix
746 /// let upload_root = PathBoundary::<()>::try_new_temp_with_prefix("upload_batch")?;
747 /// let user_file = upload_root.strict_join("user_document.pdf")?; // Validate path
748 /// // Process validated path with direct filesystem operations
749 /// // upload_root is dropped here, directory gets cleaned up automatically
750 /// # }
751 /// # Ok::<(), Box<dyn std::error::Error>>(())
752 /// ```
753 #[cfg(feature = "tempfile")]
754 pub fn try_new_temp_with_prefix(prefix: &str) -> Result<Self> {
755 let temp_dir = tempfile::Builder::new()
756 .prefix(prefix)
757 .tempdir()
758 .map_err(|e| crate::StrictPathError::InvalidRestriction {
759 restriction: "temp".into(),
760 source: e,
761 })?;
762
763 let temp_path = temp_dir.path();
764 let raw = PathHistory::<Raw>::new(temp_path);
765 let canonicalized = raw.canonicalize()?;
766 let verified_exists = canonicalized.verify_exists().ok_or_else(|| {
767 crate::StrictPathError::InvalidRestriction {
768 restriction: "temp".into(),
769 source: std::io::Error::new(
770 std::io::ErrorKind::NotFound,
771 "Temp directory verification failed",
772 ),
773 }
774 })?;
775
776 Ok(Self::new_with_temp_dir(
777 Arc::new(verified_exists),
778 Some(Arc::new(temp_dir)),
779 ))
780 }
781
782 /// Creates a PathBoundary using app-path for portable applications.
783 ///
784 /// Creates a directory relative to the executable location, with optional
785 /// environment variable override support for deployment flexibility.
786 ///
787 /// # Example
788 /// ```
789 /// # #[cfg(feature = "app-path")] {
790 /// use strict_path::PathBoundary;
791 ///
792 /// // Creates ./config/ relative to executable
793 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", None)?;
794 ///
795 /// // With environment override (checks MYAPP_CONFIG_DIR first)
796 /// let config_restriction = PathBoundary::<()>::try_new_app_path("config", Some("MYAPP_CONFIG_DIR"))?;
797 /// # }
798 /// # Ok::<(), Box<dyn std::error::Error>>(())
799 /// ```
800 #[cfg(feature = "app-path")]
801 pub fn try_new_app_path(subdir: &str, env_override: Option<&str>) -> Result<Self> {
802 let app_path = app_path::AppPath::try_with_override(subdir, env_override).map_err(|e| {
803 crate::StrictPathError::InvalidRestriction {
804 restriction: format!("app-path: {subdir}").into(),
805 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, e),
806 }
807 })?;
808
809 Self::try_new_create(app_path)
810 }
811}
812
813impl<Marker> AsRef<Path> for PathBoundary<Marker> {
814 #[inline]
815 fn as_ref(&self) -> &Path {
816 // PathHistory implements AsRef<Path>, so forward to it
817 self.path.as_ref()
818 }
819}
820
821impl<Marker> std::fmt::Debug for PathBoundary<Marker> {
822 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
823 f.debug_struct("PathBoundary")
824 .field("path", &self.path.as_ref())
825 .field("marker", &std::any::type_name::<Marker>())
826 .finish()
827 }
828}
829
830impl<Marker: Default> std::str::FromStr for PathBoundary<Marker> {
831 type Err = crate::StrictPathError;
832
833 /// Parse a PathBoundary from a string path for universal ergonomics.
834 ///
835 /// Creates the directory if it doesn't exist, enabling seamless integration
836 /// with any string-parsing context (clap, config files, environment variables, etc.):
837 /// ```rust
838 /// # use strict_path::PathBoundary;
839 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
840 /// let temp_dir = tempfile::tempdir()?;
841 /// let safe_path = temp_dir.path().join("safe_dir");
842 /// let boundary: PathBoundary<()> = safe_path.to_string_lossy().parse()?;
843 /// assert!(safe_path.exists());
844 /// # Ok(())
845 /// # }
846 /// ```
847 #[inline]
848 fn from_str(path: &str) -> std::result::Result<Self, Self::Err> {
849 Self::try_new_create(path)
850 }
851}
852// hi