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}