strict_path/path/strict_path.rs
1use crate::validator::path_history::{BoundaryChecked, Canonicalized, PathHistory, Raw};
2use crate::{Result, StrictPathError};
3use std::cmp::Ordering;
4use std::ffi::OsStr;
5use std::fmt;
6use std::hash::{Hash, Hasher};
7use std::marker::PhantomData;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11/// Strip the Windows verbatim `\\?\` prefix from a path if present.
12///
13/// The `junction` crate does not handle verbatim prefix paths correctly - it creates
14/// broken junctions that return ERROR_INVALID_NAME (123) when accessed.
15/// This helper strips the prefix so junction creation works correctly.
16///
17/// See: <https://github.com/tesuji/junction/issues/30>
18#[cfg(all(windows, feature = "junctions"))]
19fn strip_verbatim_prefix(path: &Path) -> std::borrow::Cow<'_, Path> {
20 use std::borrow::Cow;
21 let s = path.as_os_str().to_string_lossy();
22 if let Some(rest) = s.strip_prefix(r"\\?\") {
23 Cow::Owned(PathBuf::from(rest))
24 } else {
25 Cow::Borrowed(path)
26 }
27}
28
29/// SUMMARY:
30/// Hold a validated, system-facing filesystem path guaranteed to be within a `PathBoundary`.
31///
32/// DETAILS:
33/// Use when you need system-facing I/O with safety proofs. For user-facing display and rooted
34/// virtual operations prefer `VirtualPath`. Operations like `strict_join` and
35/// `strictpath_parent` preserve guarantees. `Display` shows the real system path. String
36/// accessors are prefixed with `strictpath_` to avoid confusion.
37#[derive(Clone)]
38pub struct StrictPath<Marker = ()> {
39 path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
40 boundary: Arc<crate::PathBoundary<Marker>>,
41 _marker: PhantomData<Marker>,
42}
43
44impl<Marker> StrictPath<Marker> {
45 /// SUMMARY:
46 /// Create the base `StrictPath` anchored at the provided boundary directory.
47 ///
48 /// PARAMETERS:
49 /// - `dir_path` (`AsRef<Path>`): Boundary directory (must exist).
50 ///
51 /// RETURNS:
52 /// - `Result<StrictPath<Marker>>`: Base path ("" join) within the boundary.
53 ///
54 /// ERRORS:
55 /// - `StrictPathError::InvalidRestriction`: If the boundary cannot be created/validated.
56 ///
57 /// NOTE: Prefer passing `PathBoundary` in reusable flows.
58 pub fn with_boundary<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
59 let boundary = crate::PathBoundary::try_new(dir_path)?;
60 boundary.into_strictpath()
61 }
62
63 /// SUMMARY:
64 /// Create the base `StrictPath`, creating the boundary directory if missing.
65 pub fn with_boundary_create<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
66 let boundary = crate::PathBoundary::try_new_create(dir_path)?;
67 boundary.into_strictpath()
68 }
69 pub(crate) fn new(
70 boundary: Arc<crate::PathBoundary<Marker>>,
71 validated_path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
72 ) -> Self {
73 Self {
74 path: validated_path,
75 boundary,
76 _marker: PhantomData,
77 }
78 }
79
80 #[cfg(feature = "virtual-path")]
81 #[inline]
82 pub(crate) fn boundary(&self) -> &crate::PathBoundary<Marker> {
83 &self.boundary
84 }
85
86 #[inline]
87 pub(crate) fn path(&self) -> &Path {
88 &self.path
89 }
90
91 /// SUMMARY:
92 /// Return a lossy `String` view of the system path. Prefer `.interop_path()` only for unavoidable third-party interop.
93 #[inline]
94 pub fn strictpath_to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
95 self.path.to_string_lossy()
96 }
97
98 /// SUMMARY:
99 /// Return the underlying system path as `&str` if valid UTF‑8; otherwise `None`.
100 #[inline]
101 pub fn strictpath_to_str(&self) -> Option<&str> {
102 self.path.to_str()
103 }
104
105 /// SUMMARY:
106 /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
107 #[inline]
108 pub fn interop_path(&self) -> &OsStr {
109 self.path.as_os_str()
110 }
111
112 /// SUMMARY:
113 /// Return a `Display` wrapper that shows the real system path.
114 #[inline]
115 pub fn strictpath_display(&self) -> std::path::Display<'_> {
116 self.path.display()
117 }
118
119 /// SUMMARY:
120 /// Consume and return the inner `PathBuf` (escape hatch). Prefer `.interop_path()` (third-party adapters only) to borrow.
121 #[inline]
122 pub fn unstrict(self) -> PathBuf {
123 self.path.into_inner()
124 }
125
126 /// SUMMARY:
127 /// Convert this `StrictPath` into a user‑facing `VirtualPath`.
128 #[cfg(feature = "virtual-path")]
129 #[inline]
130 pub fn virtualize(self) -> crate::path::virtual_path::VirtualPath<Marker> {
131 crate::path::virtual_path::VirtualPath::new(self)
132 }
133
134 /// SUMMARY:
135 /// Change the compile-time marker while reusing the validated strict path.
136 ///
137 /// WHEN TO USE:
138 /// - After authenticating/authorizing a user and granting them access to a path
139 /// - When escalating or downgrading permissions (e.g., ReadOnly → ReadWrite)
140 /// - When reinterpreting a path's domain (e.g., TempStorage → UserUploads)
141 ///
142 /// WHEN NOT TO USE:
143 /// - When converting between path types - conversions preserve markers automatically
144 /// - When the current marker already matches your needs - no transformation needed
145 /// - When you haven't verified authorization - NEVER change markers without checking permissions
146 ///
147 /// PARAMETERS:
148 /// - `_none_`
149 ///
150 /// RETURNS:
151 /// - `StrictPath<NewMarker>`: Same boundary-checked system path encoded with the new marker.
152 ///
153 /// ERRORS:
154 /// - `_none_`
155 ///
156 /// SECURITY:
157 /// The caller MUST ensure the new marker reflects real-world permissions. This method does not
158 /// perform any authorization checks.
159 ///
160 /// EXAMPLE:
161 /// ```rust
162 /// # use strict_path::{PathBoundary, StrictPath};
163 /// # use std::io;
164 /// # struct UserFiles;
165 /// # struct ReadOnly;
166 /// # struct ReadWrite;
167 /// # let boundary_dir = std::env::temp_dir().join("strict-path-change-marker-example");
168 /// # std::fs::create_dir_all(&boundary_dir.join("logs"))?;
169 /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
170 /// #
171 /// // Verify user can write before granting write access
172 /// fn authorize_write_access(
173 /// user_id: &str,
174 /// path: StrictPath<(UserFiles, ReadOnly)>
175 /// ) -> Result<StrictPath<(UserFiles, ReadWrite)>, &'static str> {
176 /// if user_id == "admin" {
177 /// Ok(path.change_marker()) // ✅ Transform after authorization check
178 /// } else {
179 /// Err("insufficient permissions") // ❌ User lacks write permission
180 /// }
181 /// }
182 ///
183 /// // Function requiring write permission - enforces type safety at compile time
184 /// fn write_log_entry(path: StrictPath<(UserFiles, ReadWrite)>, content: &str) -> io::Result<()> {
185 /// path.write(content.as_bytes())
186 /// }
187 ///
188 /// // Start with read-only access from untrusted input
189 /// let requested_log = "logs/app.log"; // Untrusted input
190 /// let read_only_path: StrictPath<(UserFiles, ReadOnly)> =
191 /// boundary.strict_join(requested_log)?.change_marker();
192 ///
193 /// // Elevate permissions after authorization
194 /// let read_write_path = authorize_write_access("admin", read_only_path)
195 /// .expect("user must have sufficient permissions");
196 ///
197 /// // Now we can call functions requiring write access
198 /// write_log_entry(read_write_path, "Application started")?;
199 /// #
200 /// # std::fs::remove_dir_all(&boundary_dir)?;
201 /// # Ok::<_, Box<dyn std::error::Error>>(())
202 /// ```
203 ///
204 /// **Type Safety Guarantee:**
205 ///
206 /// The following code **fails to compile** because you cannot pass a path with one marker
207 /// type to a function expecting a different marker type. This compile-time check enforces
208 /// that permission changes are explicit and cannot be bypassed accidentally.
209 ///
210 /// ```compile_fail
211 /// # use strict_path::{PathBoundary, StrictPath};
212 /// # struct ReadOnly;
213 /// # struct WritePermission;
214 /// # let boundary_dir = std::env::temp_dir().join("strict-path-change-marker-deny");
215 /// # std::fs::create_dir_all(&boundary_dir).unwrap();
216 /// # let boundary: PathBoundary<ReadOnly> = PathBoundary::try_new(&boundary_dir).unwrap();
217 /// let read_only_path: StrictPath<ReadOnly> = boundary.strict_join("logs/app.log").unwrap();
218 /// fn require_write(_: StrictPath<WritePermission>) {}
219 /// // ❌ Compile error: expected `StrictPath<WritePermission>`, found `StrictPath<ReadOnly>`
220 /// require_write(read_only_path);
221 /// ```
222 #[inline]
223 pub fn change_marker<NewMarker>(self) -> StrictPath<NewMarker> {
224 let StrictPath { path, boundary, .. } = self;
225
226 // Try to unwrap the Arc (zero-cost if this is the only reference).
227 // If other references exist, clone the boundary (allocation needed).
228 let boundary_owned = Arc::try_unwrap(boundary).unwrap_or_else(|arc| (*arc).clone());
229 let new_boundary = Arc::new(boundary_owned.change_marker::<NewMarker>());
230
231 StrictPath {
232 path,
233 boundary: new_boundary,
234 _marker: PhantomData,
235 }
236 }
237
238 /// SUMMARY:
239 /// Consume and return a new `PathBoundary` anchored at this strict path.
240 ///
241 /// RETURNS:
242 /// - `Result<PathBoundary<Marker>>`: Boundary anchored at the strict path's
243 /// system location (must already exist and be a directory).
244 ///
245 /// ERRORS:
246 /// - `StrictPathError::InvalidRestriction`: If the strict path does not exist
247 /// or is not a directory.
248 #[inline]
249 pub fn try_into_boundary(self) -> Result<crate::PathBoundary<Marker>> {
250 let StrictPath { path, .. } = self;
251 crate::PathBoundary::try_new(path.into_inner())
252 }
253
254 /// SUMMARY:
255 /// Consume and return a `PathBoundary`, creating the directory if missing.
256 ///
257 /// RETURNS:
258 /// - `Result<PathBoundary<Marker>>`: Boundary anchored at the strict path's
259 /// system location (created if necessary).
260 ///
261 /// ERRORS:
262 /// - `StrictPathError::InvalidRestriction`: If creation or canonicalization fails.
263 #[inline]
264 pub fn try_into_boundary_create(self) -> Result<crate::PathBoundary<Marker>> {
265 let StrictPath { path, .. } = self;
266 crate::PathBoundary::try_new_create(path.into_inner())
267 }
268
269 /// SUMMARY:
270 /// Join a path segment and re-validate against the boundary.
271 ///
272 /// NOTE:
273 /// Never wrap `.interop_path()` in `Path::new()` to use `Path::join()` — that defeats all security. Always use this method.
274 /// After `.unstrict()` (explicit escape hatch), you own a `PathBuf` and can do whatever you need.
275 ///
276 /// PARAMETERS:
277 /// - `path` (`AsRef<Path>`): Segment or absolute path to validate.
278 ///
279 /// RETURNS:
280 /// - `Result<StrictPath<Marker>>`: Validated path inside the boundary.
281 ///
282 /// ERRORS:
283 /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
284 #[inline]
285 pub fn strict_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
286 let new_systempath = self.path.join(path);
287 self.boundary.strict_join(new_systempath)
288 }
289
290 /// SUMMARY:
291 /// Return the parent as a new `StrictPath`, or `None` at the boundary root.
292 pub fn strictpath_parent(&self) -> Result<Option<Self>> {
293 match self.path.parent() {
294 Some(p) => match self.boundary.strict_join(p) {
295 Ok(p) => Ok(Some(p)),
296 Err(e) => Err(e),
297 },
298 None => Ok(None),
299 }
300 }
301
302 /// SUMMARY:
303 /// Return a new path with file name changed, re‑validating against the boundary.
304 #[inline]
305 pub fn strictpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
306 let new_systempath = self.path.with_file_name(file_name);
307 self.boundary.strict_join(new_systempath)
308 }
309
310 /// SUMMARY:
311 /// Return a new path with extension changed; error at the boundary root.
312 pub fn strictpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
313 let system_path = &self.path;
314 if system_path.file_name().is_none() {
315 return Err(StrictPathError::path_escapes_boundary(
316 self.path.to_path_buf(),
317 self.boundary.path().to_path_buf(),
318 ));
319 }
320 let new_systempath = system_path.with_extension(extension);
321 self.boundary.strict_join(new_systempath)
322 }
323
324 /// Returns the file name component of the system path, if any.
325 #[inline]
326 pub fn strictpath_file_name(&self) -> Option<&OsStr> {
327 self.path.file_name()
328 }
329
330 /// Returns the file stem of the system path, if any.
331 #[inline]
332 pub fn strictpath_file_stem(&self) -> Option<&OsStr> {
333 self.path.file_stem()
334 }
335
336 /// Returns the extension of the system path, if any.
337 #[inline]
338 pub fn strictpath_extension(&self) -> Option<&OsStr> {
339 self.path.extension()
340 }
341
342 /// Returns `true` if the system path starts with the given prefix.
343 #[inline]
344 pub fn strictpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
345 self.path.starts_with(p.as_ref())
346 }
347
348 /// Returns `true` if the system path ends with the given suffix.
349 #[inline]
350 pub fn strictpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
351 self.path.ends_with(p.as_ref())
352 }
353
354 /// Returns `true` if the system path exists.
355 pub fn exists(&self) -> bool {
356 self.path.exists()
357 }
358
359 /// Returns `true` if the system path is a file.
360 pub fn is_file(&self) -> bool {
361 self.path.is_file()
362 }
363
364 /// Returns `true` if the system path is a directory.
365 pub fn is_dir(&self) -> bool {
366 self.path.is_dir()
367 }
368
369 /// Returns the metadata for the system path.
370 pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
371 std::fs::metadata(&self.path)
372 }
373
374 /// SUMMARY:
375 /// Return the metadata for the system path without following symlinks (like `std::fs::symlink_metadata`).
376 ///
377 /// DETAILS:
378 /// This retrieves metadata about the path entry itself. On symlinks, this reports
379 /// information about the link, not the target.
380 #[inline]
381 pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
382 std::fs::symlink_metadata(&self.path)
383 }
384
385 /// SUMMARY:
386 /// Set permissions on the file or directory at this path.
387 ///
388 /// PARAMETERS:
389 /// - `perm` (`std::fs::Permissions`): The permissions to set.
390 ///
391 /// RETURNS:
392 /// - `io::Result<()>`: Success or I/O error.
393 ///
394 /// EXAMPLE:
395 /// ```rust
396 /// use strict_path::PathBoundary;
397 ///
398 /// let temp = tempfile::tempdir()?;
399 /// let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
400 /// let file = boundary.strict_join("script.sh")?;
401 /// file.write("#!/bin/bash\necho hello")?;
402 ///
403 /// // Make executable (Unix) or read-only (cross-platform)
404 /// let mut perms = file.metadata()?.permissions();
405 /// perms.set_readonly(true);
406 /// file.set_permissions(perms)?;
407 ///
408 /// assert!(file.metadata()?.permissions().readonly());
409 /// # Ok::<(), Box<dyn std::error::Error>>(())
410 /// ```
411 #[inline]
412 pub fn set_permissions(&self, perm: std::fs::Permissions) -> std::io::Result<()> {
413 std::fs::set_permissions(&self.path, perm)
414 }
415
416 /// SUMMARY:
417 /// Check if the path exists, returning an error on permission issues.
418 ///
419 /// DETAILS:
420 /// Unlike `exists()` which returns `false` on permission errors, this method
421 /// distinguishes between "path does not exist" (`Ok(false)`) and "cannot check
422 /// due to permission error" (`Err(...)`).
423 ///
424 /// RETURNS:
425 /// - `Ok(true)`: Path exists
426 /// - `Ok(false)`: Path does not exist
427 /// - `Err(...)`: Permission or other I/O error prevented the check
428 ///
429 /// EXAMPLE:
430 /// ```rust
431 /// use strict_path::PathBoundary;
432 ///
433 /// let temp = tempfile::tempdir()?;
434 /// let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
435 ///
436 /// let existing = boundary.strict_join("exists.txt")?;
437 /// existing.write("content")?;
438 /// assert_eq!(existing.try_exists()?, true);
439 ///
440 /// let missing = boundary.strict_join("missing.txt")?;
441 /// assert_eq!(missing.try_exists()?, false);
442 /// # Ok::<(), Box<dyn std::error::Error>>(())
443 /// ```
444 #[inline]
445 pub fn try_exists(&self) -> std::io::Result<bool> {
446 self.path.try_exists()
447 }
448
449 /// SUMMARY:
450 /// Create an empty file if it doesn't exist, or update the modification time if it does.
451 ///
452 /// DETAILS:
453 /// This is a convenience method combining file creation and mtime update.
454 /// Uses `OpenOptions` with `create(true).write(true)` which creates the file
455 /// if missing or opens it for writing if it exists, updating mtime on close.
456 ///
457 /// RETURNS:
458 /// - `io::Result<()>`: Success or I/O error.
459 ///
460 /// EXAMPLE:
461 /// ```rust
462 /// use strict_path::PathBoundary;
463 ///
464 /// let temp = tempfile::tempdir()?;
465 /// let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
466 ///
467 /// let file = boundary.strict_join("marker.txt")?;
468 /// assert!(!file.exists());
469 ///
470 /// file.touch()?;
471 /// assert!(file.exists());
472 /// assert_eq!(file.read_to_string()?, ""); // Empty file
473 /// # Ok::<(), Box<dyn std::error::Error>>(())
474 /// ```
475 pub fn touch(&self) -> std::io::Result<()> {
476 // Using truncate(false) to preserve existing content - touch only updates mtime
477 std::fs::OpenOptions::new()
478 .create(true)
479 .write(true)
480 .truncate(false)
481 .open(&self.path)?;
482 Ok(())
483 }
484
485 /// SUMMARY:
486 /// Read directory entries at this path (discovery). Re‑join names through strict/virtual APIs before I/O.
487 pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
488 std::fs::read_dir(&self.path)
489 }
490
491 /// SUMMARY:
492 /// Read directory entries as validated `StrictPath` values (auto re-joins each entry).
493 ///
494 /// DETAILS:
495 /// Unlike `read_dir()` which returns raw `std::fs::DirEntry`, this method automatically
496 /// validates each directory entry through `strict_join()`, returning an iterator of
497 /// `Result<StrictPath<Marker>>`. This eliminates the need for manual re-validation loops.
498 ///
499 /// PARAMETERS:
500 /// - _none_
501 ///
502 /// RETURNS:
503 /// - `io::Result<StrictReadDir<Marker>>`: Iterator yielding validated `StrictPath` entries.
504 ///
505 /// ERRORS:
506 /// - `std::io::Error`: If the directory cannot be read.
507 /// - Each yielded item may also be `Err` if validation fails for that entry.
508 ///
509 /// EXAMPLE:
510 /// ```rust
511 /// # use strict_path::{PathBoundary, StrictPath};
512 /// # let temp = tempfile::tempdir()?;
513 /// # let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
514 /// # let dir = boundary.strict_join("data")?;
515 /// # dir.create_dir_all()?;
516 /// # boundary.strict_join("data/file1.txt")?.write("a")?;
517 /// # boundary.strict_join("data/file2.txt")?.write("b")?;
518 /// // Iterate with automatic validation
519 /// for entry in dir.strict_read_dir()? {
520 /// let child: StrictPath = entry?;
521 /// println!("{}", child.strictpath_display());
522 /// }
523 /// # Ok::<_, Box<dyn std::error::Error>>(())
524 /// ```
525 pub fn strict_read_dir(&self) -> std::io::Result<StrictReadDir<'_, Marker>> {
526 let inner = std::fs::read_dir(&self.path)?;
527 Ok(StrictReadDir {
528 inner,
529 parent: self,
530 })
531 }
532
533 /// Reads the file contents as `String`.
534 pub fn read_to_string(&self) -> std::io::Result<String> {
535 std::fs::read_to_string(&self.path)
536 }
537
538 /// Reads the file contents as raw bytes.
539 #[inline]
540 pub fn read(&self) -> std::io::Result<Vec<u8>> {
541 std::fs::read(&self.path)
542 }
543
544 /// SUMMARY:
545 /// Write bytes to the file (create if missing). Accepts any `AsRef<[u8]>` (e.g., `&str`, `&[u8]`).
546 #[inline]
547 pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
548 std::fs::write(&self.path, contents)
549 }
550
551 /// SUMMARY:
552 /// Append bytes to the file (create if missing). Accepts any `AsRef<[u8]>` (e.g., `&str`, `&[u8]`).
553 ///
554 /// PARAMETERS:
555 /// - `data` (`AsRef<[u8]>`): Bytes to append to the file.
556 ///
557 /// RETURNS:
558 /// - `()`: Returns nothing on success.
559 ///
560 /// ERRORS:
561 /// - `std::io::Error`: Propagates OS errors when the file cannot be opened or written.
562 ///
563 /// EXAMPLE:
564 /// ```rust
565 /// # use strict_path::{PathBoundary, StrictPath};
566 /// # let boundary_dir = std::env::temp_dir().join("strict-path-append-example");
567 /// # std::fs::create_dir_all(&boundary_dir)?;
568 /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
569 /// // Untrusted input from request/CLI/config/etc.
570 /// let log_file = "logs/audit.log";
571 /// let log_path: StrictPath = boundary.strict_join(log_file)?;
572 /// log_path.create_parent_dir_all()?;
573 /// log_path.append("[2025-01-01] Session started\n")?;
574 /// log_path.append("[2025-01-01] User logged in\n")?;
575 /// let contents = log_path.read_to_string()?;
576 /// assert!(contents.contains("Session started"));
577 /// assert!(contents.contains("User logged in"));
578 /// # std::fs::remove_dir_all(&boundary_dir)?;
579 /// # Ok::<_, Box<dyn std::error::Error>>(())
580 /// ```
581 #[inline]
582 pub fn append<C: AsRef<[u8]>>(&self, data: C) -> std::io::Result<()> {
583 use std::io::Write;
584 let mut file = std::fs::OpenOptions::new()
585 .create(true)
586 .append(true)
587 .open(&self.path)?;
588 file.write_all(data.as_ref())
589 }
590
591 /// SUMMARY:
592 /// Create or truncate the file at this strict path and return a writable handle.
593 ///
594 /// PARAMETERS:
595 /// - _none_
596 ///
597 /// RETURNS:
598 /// - `std::fs::File`: Writable handle scoped to this boundary.
599 ///
600 /// ERRORS:
601 /// - `std::io::Error`: Propagates OS errors when the parent directory is missing or file creation fails.
602 ///
603 /// EXAMPLE:
604 /// ```rust
605 /// # use strict_path::{PathBoundary, StrictPath};
606 /// # use std::io::Write;
607 /// # let boundary_dir = std::env::temp_dir().join("strict-path-create-file-example");
608 /// # std::fs::create_dir_all(&boundary_dir)?;
609 /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
610 /// // Untrusted input from request/CLI/config/etc.
611 /// let requested_file = "logs/app.log";
612 /// let log_path: StrictPath = boundary.strict_join(requested_file)?;
613 /// log_path.create_parent_dir_all()?;
614 /// let mut file = log_path.create_file()?;
615 /// file.write_all(b"session started")?;
616 /// # std::fs::remove_dir_all(&boundary_dir)?;
617 /// # Ok::<_, Box<dyn std::error::Error>>(())
618 /// ```
619 #[inline]
620 pub fn create_file(&self) -> std::io::Result<std::fs::File> {
621 std::fs::File::create(&self.path)
622 }
623
624 /// SUMMARY:
625 /// Open the file at this strict path in read-only mode.
626 ///
627 /// PARAMETERS:
628 /// - _none_
629 ///
630 /// RETURNS:
631 /// - `std::fs::File`: Read-only handle scoped to this boundary.
632 ///
633 /// ERRORS:
634 /// - `std::io::Error`: Propagates OS errors when the file is missing or inaccessible.
635 ///
636 /// EXAMPLE:
637 /// ```rust
638 /// # use strict_path::{PathBoundary, StrictPath};
639 /// # use std::io::{Read, Write};
640 /// # let boundary_dir = std::env::temp_dir().join("strict-path-open-file-example");
641 /// # std::fs::create_dir_all(&boundary_dir)?;
642 /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
643 /// // Untrusted input from request/CLI/config/etc.
644 /// let requested_file = "logs/session.log";
645 /// let transcript: StrictPath = boundary.strict_join(requested_file)?;
646 /// transcript.create_parent_dir_all()?;
647 /// transcript.write("session start")?;
648 /// let mut file = transcript.open_file()?;
649 /// let mut contents = String::new();
650 /// file.read_to_string(&mut contents)?;
651 /// assert_eq!(contents, "session start");
652 /// # std::fs::remove_dir_all(&boundary_dir)?;
653 /// # Ok::<_, Box<dyn std::error::Error>>(())
654 /// ```
655 #[inline]
656 pub fn open_file(&self) -> std::io::Result<std::fs::File> {
657 std::fs::File::open(&self.path)
658 }
659
660 /// SUMMARY:
661 /// Return an options builder for advanced file opening (read+write, append, exclusive create, etc.).
662 ///
663 /// PARAMETERS:
664 /// - _none_
665 ///
666 /// RETURNS:
667 /// - `StrictOpenOptions<Marker>`: Builder to configure file opening options.
668 ///
669 /// EXAMPLE:
670 /// ```rust
671 /// # use strict_path::{PathBoundary, StrictPath};
672 /// # use std::io::{Read, Write, Seek, SeekFrom};
673 /// # let boundary_dir = std::env::temp_dir().join("strict-path-open-with-example");
674 /// # std::fs::create_dir_all(&boundary_dir)?;
675 /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
676 /// // Untrusted input from request/CLI/config/etc.
677 /// let data_file = "data/records.bin";
678 /// let file_path: StrictPath = boundary.strict_join(data_file)?;
679 /// file_path.create_parent_dir_all()?;
680 ///
681 /// // Open with read+write access, create if missing
682 /// let mut file = file_path.open_with()
683 /// .read(true)
684 /// .write(true)
685 /// .create(true)
686 /// .open()?;
687 /// file.write_all(b"header")?;
688 /// file.seek(SeekFrom::Start(0))?;
689 /// let mut buf = [0u8; 6];
690 /// file.read_exact(&mut buf)?;
691 /// assert_eq!(&buf, b"header");
692 /// # std::fs::remove_dir_all(&boundary_dir)?;
693 /// # Ok::<_, Box<dyn std::error::Error>>(())
694 /// ```
695 #[inline]
696 pub fn open_with(&self) -> StrictOpenOptions<'_, Marker> {
697 StrictOpenOptions::new(self)
698 }
699
700 /// Creates all directories in the system path if missing (like `std::fs::create_dir_all`).
701 pub fn create_dir_all(&self) -> std::io::Result<()> {
702 std::fs::create_dir_all(&self.path)
703 }
704
705 /// Creates the directory at the system path (non-recursive, like `std::fs::create_dir`).
706 ///
707 /// Fails if the parent directory does not exist. Use `create_dir_all` to
708 /// create missing parent directories recursively.
709 pub fn create_dir(&self) -> std::io::Result<()> {
710 std::fs::create_dir(&self.path)
711 }
712
713 /// SUMMARY:
714 /// Create only the immediate parent directory (non‑recursive). `Ok(())` at the boundary root.
715 pub fn create_parent_dir(&self) -> std::io::Result<()> {
716 match self.strictpath_parent() {
717 Ok(Some(parent)) => parent.create_dir(),
718 Ok(None) => Ok(()),
719 Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
720 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
721 }
722 }
723
724 /// SUMMARY:
725 /// Recursively create all missing directories up to the immediate parent. `Ok(())` at boundary.
726 pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
727 match self.strictpath_parent() {
728 Ok(Some(parent)) => parent.create_dir_all(),
729 Ok(None) => Ok(()),
730 Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
731 Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
732 }
733 }
734
735 /// SUMMARY:
736 /// Create a symbolic link at `link_path` pointing to this path (same boundary required).
737 /// On Windows, file vs directory symlink is selected by target metadata (or best‑effort when missing).
738 /// Relative paths are resolved as siblings; absolute paths are validated against the boundary.
739 pub fn strict_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
740 let link_ref = link_path.as_ref();
741
742 // Compute link path under the parent directory for relative paths; allow absolute too
743 let validated_link = if link_ref.is_absolute() {
744 match self.boundary.strict_join(link_ref) {
745 Ok(p) => p,
746 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
747 }
748 } else {
749 let parent = match self.strictpath_parent() {
750 Ok(Some(p)) => p,
751 Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
752 Ok(root) => root,
753 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
754 },
755 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
756 };
757 match parent.strict_join(link_ref) {
758 Ok(p) => p,
759 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
760 }
761 };
762
763 #[cfg(unix)]
764 {
765 std::os::unix::fs::symlink(self.path(), validated_link.path())?;
766 }
767
768 #[cfg(windows)]
769 {
770 create_windows_symlink(self.path(), validated_link.path())?;
771 }
772
773 Ok(())
774 }
775
776 /// SUMMARY:
777 /// Read the target of a symbolic link and validate it is within the boundary.
778 ///
779 /// DESIGN NOTE:
780 /// This method has limited practical use because `strict_join` resolves symlinks
781 /// during canonicalization. A `StrictPath` obtained via `strict_join("link")` already
782 /// points to the symlink's target, not the symlink itself.
783 ///
784 /// To read a symlink target before validation, use `std::fs::read_link` on the raw
785 /// path, then validate the target with `strict_join`:
786 ///
787 /// EXAMPLE:
788 /// ```rust
789 /// use strict_path::PathBoundary;
790 ///
791 /// let temp = tempfile::tempdir()?;
792 /// let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
793 ///
794 /// // Create a target file
795 /// let target = boundary.strict_join("target.txt")?;
796 /// target.write("secret")?;
797 ///
798 /// // Create symlink (may fail on Windows without Developer Mode)
799 /// if target.strict_symlink("link.txt").is_ok() {
800 /// // WRONG: strict_join("link.txt") resolves to target.txt
801 /// let resolved = boundary.strict_join("link.txt")?;
802 /// assert_eq!(resolved.strictpath_file_name(), Some("target.txt".as_ref()));
803 ///
804 /// // RIGHT: read symlink target from raw path, then validate
805 /// let link_raw_path = temp.path().join("link.txt");
806 /// let symlink_target = std::fs::read_link(&link_raw_path)?;
807 /// let validated = boundary.strict_join(&symlink_target)?;
808 /// assert_eq!(validated.strictpath_file_name(), Some("target.txt".as_ref()));
809 /// }
810 /// # Ok::<(), Box<dyn std::error::Error>>(())
811 /// ```
812 pub fn strict_read_link(&self) -> std::io::Result<Self> {
813 // Read the raw symlink target
814 let raw_target = std::fs::read_link(&self.path)?;
815
816 // If the target is relative, resolve it relative to the symlink's parent
817 let resolved_target = if raw_target.is_relative() {
818 match self.path.parent() {
819 Some(parent) => parent.join(&raw_target),
820 None => raw_target,
821 }
822 } else {
823 raw_target
824 };
825
826 // Validate the resolved target against the boundary
827 self.boundary
828 .strict_join(resolved_target)
829 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
830 }
831
832 /// SUMMARY:
833 /// Create a hard link at `link_path` pointing to this path (same boundary; caller creates parents).
834 /// Relative paths are resolved as siblings; absolute paths are validated against the boundary.
835 pub fn strict_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
836 let link_ref = link_path.as_ref();
837
838 // Compute link path under the parent directory for relative paths; allow absolute too
839 let validated_link = if link_ref.is_absolute() {
840 match self.boundary.strict_join(link_ref) {
841 Ok(p) => p,
842 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
843 }
844 } else {
845 let parent = match self.strictpath_parent() {
846 Ok(Some(p)) => p,
847 Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
848 Ok(root) => root,
849 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
850 },
851 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
852 };
853 match parent.strict_join(link_ref) {
854 Ok(p) => p,
855 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
856 }
857 };
858
859 std::fs::hard_link(self.path(), validated_link.path())
860 }
861
862 /// SUMMARY:
863 /// Create a Windows NTFS directory junction at `link_path` pointing to this path.
864 ///
865 /// DETAILS:
866 /// - Windows-only and behind the `junctions` crate feature.
867 /// - Junctions are directory-only. This call will fail if the target is not a directory.
868 /// - Both `self` (target) and `link_path` must be within the same `PathBoundary`.
869 /// - Parents for `link_path` are not created automatically; call `create_parent_dir_all()` first.
870 ///
871 /// RETURNS:
872 /// - `io::Result<()>`: Mirrors OS semantics (and `junction` crate behavior).
873 ///
874 /// ERRORS:
875 /// - Returns an error if the target is not a directory, or the OS call fails.
876 #[cfg(all(windows, feature = "junctions"))]
877 pub fn strict_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
878 let link_ref = link_path.as_ref();
879
880 // Compute link path under the parent directory for relative paths; allow absolute too
881 let validated_link = if link_ref.is_absolute() {
882 match self.boundary.strict_join(link_ref) {
883 Ok(p) => p,
884 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
885 }
886 } else {
887 let parent = match self.strictpath_parent() {
888 Ok(Some(p)) => p,
889 Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
890 Ok(root) => root,
891 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
892 },
893 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
894 };
895 match parent.strict_join(link_ref) {
896 Ok(p) => p,
897 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
898 }
899 };
900
901 // Validate target is a directory (junctions are directory-only)
902 let meta = std::fs::metadata(self.path())?;
903 if !meta.is_dir() {
904 return Err(std::io::Error::new(
905 std::io::ErrorKind::Other,
906 "junction targets must be directories",
907 ));
908 }
909
910 // The junction crate does not handle verbatim `\\?\` prefix paths correctly.
911 // It creates broken junctions that return ERROR_INVALID_NAME (123) when accessed.
912 // Strip the prefix before passing to the junction crate.
913 // See: https://github.com/tesuji/junction/issues/30
914 let target_path = strip_verbatim_prefix(self.path());
915 let link_path = strip_verbatim_prefix(validated_link.path());
916
917 junction::create(target_path.as_ref(), link_path.as_ref())
918 }
919
920 /// SUMMARY:
921 /// Rename/move within the same boundary. Relative destinations are siblings; absolute are validated.
922 /// Parents are not created automatically.
923 pub fn strict_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
924 let dest_ref = dest.as_ref();
925
926 // Compute destination under the parent directory for relative paths; allow absolute too
927 let dest_path = if dest_ref.is_absolute() {
928 match self.boundary.strict_join(dest_ref) {
929 Ok(p) => p,
930 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
931 }
932 } else {
933 let parent = match self.strictpath_parent() {
934 Ok(Some(p)) => p,
935 Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
936 Ok(root) => root,
937 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
938 },
939 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
940 };
941 match parent.strict_join(dest_ref) {
942 Ok(p) => p,
943 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
944 }
945 };
946
947 std::fs::rename(self.path(), dest_path.path())
948 }
949
950 /// SUMMARY:
951 /// Copy within the same boundary. Relative destinations are siblings; absolute are validated.
952 /// Parents are not created automatically. Returns bytes copied.
953 pub fn strict_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
954 let dest_ref = dest.as_ref();
955
956 // Compute destination under the parent directory for relative paths; allow absolute too
957 let dest_path = if dest_ref.is_absolute() {
958 match self.boundary.strict_join(dest_ref) {
959 Ok(p) => p,
960 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
961 }
962 } else {
963 let parent = match self.strictpath_parent() {
964 Ok(Some(p)) => p,
965 Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
966 Ok(root) => root,
967 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
968 },
969 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
970 };
971 match parent.strict_join(dest_ref) {
972 Ok(p) => p,
973 Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
974 }
975 };
976
977 std::fs::copy(self.path(), dest_path.path())
978 }
979
980 /// SUMMARY:
981 /// Remove the file at this path.
982 pub fn remove_file(&self) -> std::io::Result<()> {
983 std::fs::remove_file(&self.path)
984 }
985
986 /// SUMMARY:
987 /// Remove the directory at this path.
988 pub fn remove_dir(&self) -> std::io::Result<()> {
989 std::fs::remove_dir(&self.path)
990 }
991
992 /// SUMMARY:
993 /// Recursively remove the directory and its contents.
994 pub fn remove_dir_all(&self) -> std::io::Result<()> {
995 std::fs::remove_dir_all(&self.path)
996 }
997}
998
999impl<Marker> fmt::Debug for StrictPath<Marker> {
1000 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1001 f.debug_struct("StrictPath")
1002 .field("path", &self.path)
1003 .field("boundary", &self.boundary.path())
1004 .field("marker", &std::any::type_name::<Marker>())
1005 .finish()
1006 }
1007}
1008
1009#[cfg(windows)]
1010fn create_windows_symlink(src: &Path, link: &Path) -> std::io::Result<()> {
1011 use std::os::windows::fs::{symlink_dir, symlink_file};
1012
1013 match std::fs::metadata(src) {
1014 Ok(metadata) => {
1015 if metadata.is_dir() {
1016 symlink_dir(src, link)
1017 } else {
1018 symlink_file(src, link)
1019 }
1020 }
1021 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1022 match symlink_file(src, link) {
1023 Ok(()) => Ok(()),
1024 Err(file_err) => {
1025 if let Some(code) = file_err.raw_os_error() {
1026 const ERROR_DIRECTORY: i32 = 267; // target resolved as directory
1027 if code == ERROR_DIRECTORY {
1028 return symlink_dir(src, link);
1029 }
1030 }
1031 Err(file_err)
1032 }
1033 }
1034 }
1035 Err(err) => Err(err),
1036 }
1037}
1038
1039// Note: No separate helper for junction creation by design — keep surface minimal
1040
1041impl<Marker> PartialEq for StrictPath<Marker> {
1042 #[inline]
1043 fn eq(&self, other: &Self) -> bool {
1044 self.path.as_ref() == other.path.as_ref()
1045 }
1046}
1047
1048impl<Marker> Eq for StrictPath<Marker> {}
1049
1050impl<Marker> Hash for StrictPath<Marker> {
1051 #[inline]
1052 fn hash<H: Hasher>(&self, state: &mut H) {
1053 self.path.hash(state);
1054 }
1055}
1056
1057impl<Marker> PartialOrd for StrictPath<Marker> {
1058 #[inline]
1059 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1060 Some(self.cmp(other))
1061 }
1062}
1063
1064impl<Marker> Ord for StrictPath<Marker> {
1065 #[inline]
1066 fn cmp(&self, other: &Self) -> Ordering {
1067 self.path.cmp(&other.path)
1068 }
1069}
1070
1071impl<T: AsRef<Path>, Marker> PartialEq<T> for StrictPath<Marker> {
1072 fn eq(&self, other: &T) -> bool {
1073 self.path.as_ref() == other.as_ref()
1074 }
1075}
1076
1077impl<T: AsRef<Path>, Marker> PartialOrd<T> for StrictPath<Marker> {
1078 fn partial_cmp(&self, other: &T) -> Option<Ordering> {
1079 Some(self.path.as_ref().cmp(other.as_ref()))
1080 }
1081}
1082
1083#[cfg(feature = "virtual-path")]
1084impl<Marker> PartialEq<crate::path::virtual_path::VirtualPath<Marker>> for StrictPath<Marker> {
1085 #[inline]
1086 fn eq(&self, other: &crate::path::virtual_path::VirtualPath<Marker>) -> bool {
1087 self.path.as_ref() == other.interop_path()
1088 }
1089}
1090
1091// ============================================================
1092// StrictOpenOptions — Builder for advanced file opening
1093// ============================================================
1094
1095/// SUMMARY:
1096/// Builder for opening files with custom options (read, write, append, create, truncate, create_new).
1097///
1098/// DETAILS:
1099/// Use `StrictPath::open_with()` to get an instance. Chain builder methods to configure
1100/// options, then call `.open()` to obtain the file handle. This mirrors `std::fs::OpenOptions`
1101/// but operates on a validated `StrictPath`, so the path is guaranteed to be within its boundary.
1102///
1103/// EXAMPLE:
1104/// ```rust
1105/// # use strict_path::{PathBoundary, StrictPath};
1106/// # use std::io::Write;
1107/// # let temp = tempfile::tempdir()?;
1108/// # let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
1109/// let log_path: StrictPath = boundary.strict_join("app.log")?;
1110/// let mut file = log_path.open_with()
1111/// .create(true)
1112/// .append(true)
1113/// .open()?;
1114/// file.write_all(b"log entry\n")?;
1115/// # Ok::<_, Box<dyn std::error::Error>>(())
1116/// ```
1117pub struct StrictOpenOptions<'a, Marker> {
1118 path: &'a StrictPath<Marker>,
1119 options: std::fs::OpenOptions,
1120}
1121
1122impl<'a, Marker> StrictOpenOptions<'a, Marker> {
1123 /// Create a new builder with default options (all flags false).
1124 #[inline]
1125 fn new(path: &'a StrictPath<Marker>) -> Self {
1126 Self {
1127 path,
1128 options: std::fs::OpenOptions::new(),
1129 }
1130 }
1131
1132 /// Sets the option for read access.
1133 ///
1134 /// When `true`, the file will be readable after opening.
1135 #[inline]
1136 pub fn read(mut self, read: bool) -> Self {
1137 self.options.read(read);
1138 self
1139 }
1140
1141 /// Sets the option for write access.
1142 ///
1143 /// When `true`, the file will be writable after opening.
1144 /// If the file exists, writes will overwrite existing content starting at the beginning
1145 /// unless `.append(true)` is also set.
1146 #[inline]
1147 pub fn write(mut self, write: bool) -> Self {
1148 self.options.write(write);
1149 self
1150 }
1151
1152 /// Sets the option for append mode.
1153 ///
1154 /// When `true`, all writes will append to the end of the file instead of overwriting.
1155 /// Implies `.write(true)`.
1156 #[inline]
1157 pub fn append(mut self, append: bool) -> Self {
1158 self.options.append(append);
1159 self
1160 }
1161
1162 /// Sets the option for truncating the file.
1163 ///
1164 /// When `true`, the file will be truncated to zero length upon opening.
1165 /// Requires `.write(true)`.
1166 #[inline]
1167 pub fn truncate(mut self, truncate: bool) -> Self {
1168 self.options.truncate(truncate);
1169 self
1170 }
1171
1172 /// Sets the option to create the file if it doesn't exist.
1173 ///
1174 /// When `true`, the file will be created if missing. Requires `.write(true)` or `.append(true)`.
1175 #[inline]
1176 pub fn create(mut self, create: bool) -> Self {
1177 self.options.create(create);
1178 self
1179 }
1180
1181 /// Sets the option for exclusive creation (fail if file exists).
1182 ///
1183 /// When `true`, the file must not exist; opening will fail with `AlreadyExists` if it does.
1184 /// Requires `.write(true)` and implies `.create(true)`.
1185 #[inline]
1186 pub fn create_new(mut self, create_new: bool) -> Self {
1187 self.options.create_new(create_new);
1188 self
1189 }
1190
1191 /// Open the file with the configured options.
1192 ///
1193 /// RETURNS:
1194 /// - `std::io::Result<std::fs::File>`: The opened file handle.
1195 ///
1196 /// ERRORS:
1197 /// - `std::io::Error`: Propagates OS errors (file not found, permission denied, already exists, etc.).
1198 #[inline]
1199 pub fn open(self) -> std::io::Result<std::fs::File> {
1200 self.options.open(&self.path.path)
1201 }
1202}
1203
1204// ============================================================
1205// StrictReadDir — Iterator for validated directory entries
1206// ============================================================
1207
1208/// SUMMARY:
1209/// Iterator over directory entries that yields validated `StrictPath` values.
1210///
1211/// DETAILS:
1212/// Created by `StrictPath::strict_read_dir()`. Each iteration automatically validates
1213/// the directory entry through `strict_join()`, so you get `StrictPath` values directly
1214/// instead of raw `std::fs::DirEntry` that would require manual re-validation.
1215///
1216/// EXAMPLE:
1217/// ```rust
1218/// # use strict_path::{PathBoundary, StrictPath};
1219/// # let temp = tempfile::tempdir()?;
1220/// # let boundary: PathBoundary = PathBoundary::try_new(temp.path())?;
1221/// # let dir = boundary.strict_join("docs")?;
1222/// # dir.create_dir_all()?;
1223/// # boundary.strict_join("docs/readme.md")?.write("# Docs")?;
1224/// for entry in dir.strict_read_dir()? {
1225/// let child: StrictPath = entry?;
1226/// if child.is_file() {
1227/// println!("File: {}", child.strictpath_display());
1228/// }
1229/// }
1230/// # Ok::<_, Box<dyn std::error::Error>>(())
1231/// ```
1232pub struct StrictReadDir<'a, Marker> {
1233 inner: std::fs::ReadDir,
1234 parent: &'a StrictPath<Marker>,
1235}
1236
1237impl<Marker> std::fmt::Debug for StrictReadDir<'_, Marker> {
1238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1239 f.debug_struct("StrictReadDir")
1240 .field("parent", &self.parent.strictpath_display())
1241 .finish_non_exhaustive()
1242 }
1243}
1244
1245impl<Marker: Clone> Iterator for StrictReadDir<'_, Marker> {
1246 type Item = std::io::Result<StrictPath<Marker>>;
1247
1248 fn next(&mut self) -> Option<Self::Item> {
1249 match self.inner.next()? {
1250 Ok(entry) => {
1251 let file_name = entry.file_name();
1252 match self.parent.strict_join(file_name) {
1253 Ok(strict_path) => Some(Ok(strict_path)),
1254 Err(e) => Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e))),
1255 }
1256 }
1257 Err(e) => Some(Err(e)),
1258 }
1259 }
1260}