strict_path/path/
strict_path.rs

1// Content copied from original src/path/restricted_path.rs
2use crate::validator::path_history::{BoundaryChecked, Canonicalized, PathHistory, Raw};
3use crate::{Result, StrictPathError};
4use std::cmp::Ordering;
5use std::ffi::OsStr;
6use std::fmt;
7use std::hash::{Hash, Hasher};
8use std::marker::PhantomData;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12/// A validated, system-facing filesystem path that is mathematically proven to be within a
13/// `PathBoundary` boundary. If this value exists, the path is guaranteed safe.
14///
15/// Use this type when you need system-facing path display/operations with proof of safety.
16/// For user-facing display and virtual operations, consider using `VirtualPath` which provides
17/// a rooted view (PathBoundary becomes "/") and virtual joins/navigation.
18///
19/// Operations like `strict_join`, `strictpath_parent`, etc. preserve the boundary guarantees.
20/// Use `interop_path()` for I/O with external APIs. Both this type and `VirtualPath` support I/O.
21///
22/// Equality/ordering is based on the underlying system path (same as `Path::cmp`).
23/// `Display` shows the real system path.
24///
25/// All string accessors are prefixed with `strictpath_` to avoid confusion:
26/// `strictpath_*` accessors for strings/OS strings and the safe manipulation methods
27/// which re-validate against the restriction.
28#[derive(Clone)]
29pub struct StrictPath<Marker = ()> {
30    path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
31    boundary: Arc<crate::PathBoundary<Marker>>,
32    _marker: PhantomData<Marker>,
33}
34
35impl<Marker> StrictPath<Marker> {
36    /// Create a root `StrictPath` anchored at the provided boundary directory.
37    ///
38    /// Prefer this in simple flows; use `PathBoundary` directly when you need
39    /// to reuse policy across many paths or pass it as a parameter.
40    pub fn with_boundary<P: AsRef<Path>>(root: P) -> Result<Self> {
41        let boundary = crate::PathBoundary::try_new(root)?;
42        boundary.strict_join("")
43    }
44
45    /// Create a root `StrictPath`, creating the boundary directory if missing.
46    ///
47    /// Ensures the boundary directory exists by creating it when needed.
48    pub fn with_boundary_create<P: AsRef<Path>>(root: P) -> Result<Self> {
49        let boundary = crate::PathBoundary::try_new_create(root)?;
50        boundary.strict_join("")
51    }
52    pub(crate) fn new(
53        boundary: Arc<crate::PathBoundary<Marker>>,
54        validated_path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
55    ) -> Self {
56        Self {
57            path: validated_path,
58            boundary,
59            _marker: PhantomData,
60        }
61    }
62
63    #[inline]
64    pub(crate) fn boundary(&self) -> &crate::PathBoundary<Marker> {
65        &self.boundary
66    }
67
68    #[inline]
69    pub(crate) fn path(&self) -> &Path {
70        &self.path
71    }
72
73    /// For interop with APIs that accept `AsRef<Path>`, prefer
74    /// `interop_path()` to avoid allocation.
75    #[inline]
76    pub fn strictpath_to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
77        self.path.to_string_lossy()
78    }
79
80    /// Returns the underlying system path as `&str` if valid UTF-8.
81    ///
82    /// For lossless interop on any platform, prefer `interop_path()`.
83    #[inline]
84    pub fn strictpath_to_str(&self) -> Option<&str> {
85        self.path.to_str()
86    }
87
88    /// Returns the underlying system path as `&OsStr` (lossless; implements `AsRef<Path>`).
89    ///
90    /// Use this when passing to external APIs that accept `AsRef<Path>`.
91    #[inline]
92    pub fn interop_path(&self) -> &OsStr {
93        self.path.as_os_str()
94    }
95
96    /// Returns a `Display` wrapper that shows the real system path.
97    #[inline]
98    pub fn strictpath_display(&self) -> std::path::Display<'_> {
99        self.path.display()
100    }
101
102    /// Consumes this `RestrictedPath` and returns the inner `PathBuf` (escape hatch).
103    ///
104    /// Prefer borrowing via `interop_path()` when possible.
105    #[inline]
106    pub fn unstrict(self) -> PathBuf {
107        self.path.into_inner()
108    }
109
110    /// Converts this `RestrictedPath` into a user-facing `VirtualPath`.
111    #[inline]
112    pub fn virtualize(self) -> crate::path::virtual_path::VirtualPath<Marker> {
113        crate::path::virtual_path::VirtualPath::new(self)
114    }
115
116    /// Consumes this `StrictPath` and returns its associated `PathBoundary`.
117    ///
118    /// This is infallible because a `StrictPath` always carries a boundary reference.
119    /// Provided as ergonomic symmetry with other `try_*` constructors.
120    #[inline]
121    pub fn try_into_boundary(self) -> crate::PathBoundary<Marker> {
122        // Clone the underlying boundary reference (cheap, small struct)
123        self.boundary.as_ref().clone()
124    }
125
126    /// Consumes this `StrictPath` and returns its `PathBoundary`, creating the
127    /// underlying directory if it does not exist.
128    ///
129    /// This is typically a no-op since a `StrictPath` is constructed only from an
130    /// existing boundary, but this method is provided for API symmetry and robustness.
131    #[inline]
132    pub fn try_into_boundary_create(self) -> crate::PathBoundary<Marker> {
133        let boundary = self.boundary.as_ref().clone();
134        if !boundary.exists() {
135            // Best-effort create; ignore error and let later operations surface it
136            let _ = std::fs::create_dir_all(boundary.as_ref());
137        }
138        boundary
139    }
140
141    /// Safely joins a system path segment and re-validates against the restriction.
142    ///
143    /// Do not use `Path::join` on leaked paths. Always use this method to ensure
144    /// PathBoundary containment is preserved.
145    #[inline]
146    pub fn strict_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
147        let new_systempath = self.path.join(path);
148        self.boundary.strict_join(new_systempath)
149    }
150
151    /// Returns the parent directory as a new `StrictPath`, or `None` if at the PathBoundary root.
152    pub fn strictpath_parent(&self) -> Result<Option<Self>> {
153        match self.path.parent() {
154            Some(p) => match self.boundary.strict_join(p) {
155                Ok(p) => Ok(Some(p)),
156                Err(e) => Err(e),
157            },
158            None => Ok(None),
159        }
160    }
161
162    /// Returns a new `StrictPath` with the file name changed, re-validating against the restriction.
163    #[inline]
164    pub fn strictpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
165        let new_systempath = self.path.with_file_name(file_name);
166        self.boundary.strict_join(new_systempath)
167    }
168
169    /// Returns a new `StrictPath` with the extension changed, or an error if at PathBoundary root.
170    pub fn strictpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
171        let system_path = &self.path;
172        if system_path.file_name().is_none() {
173            return Err(StrictPathError::path_escapes_boundary(
174                self.path.to_path_buf(),
175                self.boundary.path().to_path_buf(),
176            ));
177        }
178        let new_systempath = system_path.with_extension(extension);
179        self.boundary.strict_join(new_systempath)
180    }
181
182    /// Returns the file name component of the system path, if any.
183    #[inline]
184    pub fn strictpath_file_name(&self) -> Option<&OsStr> {
185        self.path.file_name()
186    }
187
188    /// Returns the file stem of the system path, if any.
189    #[inline]
190    pub fn strictpath_file_stem(&self) -> Option<&OsStr> {
191        self.path.file_stem()
192    }
193
194    /// Returns the extension of the system path, if any.
195    #[inline]
196    pub fn strictpath_extension(&self) -> Option<&OsStr> {
197        self.path.extension()
198    }
199
200    /// Returns `true` if the system path starts with the given prefix.
201    #[inline]
202    pub fn strictpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
203        self.path.starts_with(p.as_ref())
204    }
205
206    /// Returns `true` if the system path ends with the given suffix.
207    #[inline]
208    pub fn strictpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
209        self.path.ends_with(p.as_ref())
210    }
211
212    /// Returns `true` if the system path exists.
213    pub fn exists(&self) -> bool {
214        self.path.exists()
215    }
216
217    /// Returns `true` if the system path is a file.
218    pub fn is_file(&self) -> bool {
219        self.path.is_file()
220    }
221
222    /// Returns `true` if the system path is a directory.
223    pub fn is_dir(&self) -> bool {
224        self.path.is_dir()
225    }
226
227    /// Returns the metadata for the system path.
228    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
229        std::fs::metadata(&self.path)
230    }
231
232    /// Reads the directory entries at the system path (like `std::fs::read_dir`).
233    ///
234    /// This is intended for discovery. Prefer collecting each entry's file name via
235    /// `entry.file_name()` and re‑joining it with `strict_join(...)` (or `virtual_join(...)`
236    /// when working from a `VirtualRoot`) before performing I/O.
237    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
238        std::fs::read_dir(&self.path)
239    }
240
241    /// Reads the file contents as `String`.
242    pub fn read_to_string(&self) -> std::io::Result<String> {
243        std::fs::read_to_string(&self.path)
244    }
245
246    /// Reads the file contents as raw bytes.
247    #[deprecated(since = "0.1.0-alpha.5", note = "Use read() instead")]
248    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
249        std::fs::read(&self.path)
250    }
251
252    /// Writes raw bytes to the file, creating it if it does not exist.
253    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
254    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
255        std::fs::write(&self.path, data)
256    }
257
258    /// Writes a UTF-8 string to the file, creating it if it does not exist.
259    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
260    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
261        std::fs::write(&self.path, data)
262    }
263
264    /// Reads the file contents as raw bytes (replacement for `read_bytes`).
265    #[inline]
266    pub fn read(&self) -> std::io::Result<Vec<u8>> {
267        std::fs::read(&self.path)
268    }
269
270    /// Writes data to the file, creating it if it does not exist.
271    /// Accepts any type that can be viewed as a byte slice (e.g., `&str`, `String`, `&[u8]`, `Vec<u8]`).
272    #[inline]
273    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
274        std::fs::write(&self.path, contents)
275    }
276
277    /// Creates all directories in the system path if missing (like `std::fs::create_dir_all`).
278    pub fn create_dir_all(&self) -> std::io::Result<()> {
279        std::fs::create_dir_all(&self.path)
280    }
281
282    /// Creates the directory at the system path (non-recursive, like `std::fs::create_dir`).
283    ///
284    /// Fails if the parent directory does not exist. Use `create_dir_all` to
285    /// create missing parent directories recursively.
286    pub fn create_dir(&self) -> std::io::Result<()> {
287        std::fs::create_dir(&self.path)
288    }
289
290    /// Creates only the immediate parent directory of this system path (non-recursive).
291    ///
292    /// Returns `Ok(())` if at the restricted path root (no parent). Fails if the parent's
293    /// parent is missing. Use `create_parent_dir_all` to create the full chain.
294    pub fn create_parent_dir(&self) -> std::io::Result<()> {
295        match self.strictpath_parent() {
296            Ok(Some(parent)) => parent.create_dir(),
297            Ok(None) => Ok(()),
298            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
299            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
300        }
301    }
302
303    /// Recursively creates all missing directories up to the immediate parent of this system path.
304    ///
305    /// Returns `Ok(())` if at the restricted path root (no parent).
306    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
307        match self.strictpath_parent() {
308            Ok(Some(parent)) => parent.create_dir_all(),
309            Ok(None) => Ok(()),
310            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
311            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
312        }
313    }
314
315    /// Renames or moves this path to a new location within the same `PathBoundary`.
316    ///
317    /// Relative destinations are interpreted as siblings (resolved against this path's parent
318    /// directory), not children. Absolute destinations are validated against the `PathBoundary`.
319    /// No parent directories are created implicitly; call `create_parent_dir_all()` on the
320    /// desired destination path beforehand if needed. Returns the destination `StrictPath`.
321    pub fn strict_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
322        let dest_ref = dest.as_ref();
323
324        // Compute destination under the parent directory for relative paths; allow absolute too
325        let dest_path = if dest_ref.is_absolute() {
326            match self.boundary.strict_join(dest_ref) {
327                Ok(p) => p,
328                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
329            }
330        } else {
331            let parent = match self.strictpath_parent() {
332                Ok(Some(p)) => p,
333                Ok(None) => match self.boundary.strict_join("") {
334                    Ok(root) => root,
335                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
336                },
337                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
338            };
339            match parent.strict_join(dest_ref) {
340                Ok(p) => p,
341                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
342            }
343        };
344
345        std::fs::rename(self.path(), dest_path.path())?;
346        Ok(dest_path)
347    }
348
349    /// Copies this file to a new location within the same `PathBoundary`.
350    ///
351    /// Semantics mirror `strict_rename` for destination resolution:
352    /// - Relative destinations are interpreted as siblings (resolved against this path's parent).
353    /// - Absolute destinations are validated against the `PathBoundary`.
354    ///
355    /// No parent directories are created implicitly; call `create_parent_dir_all()` on the
356    /// desired destination path beforehand if needed. Returns the destination `StrictPath` on
357    /// success. Equivalent to `std::fs::copy(self.interop_path(), dest.interop_path())` but with
358    /// restriction‑aware destination validation.
359    pub fn strict_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
360        let dest_ref = dest.as_ref();
361
362        // Compute destination under the parent directory for relative paths; allow absolute too
363        let dest_path = if dest_ref.is_absolute() {
364            match self.boundary.strict_join(dest_ref) {
365                Ok(p) => p,
366                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
367            }
368        } else {
369            let parent = match self.strictpath_parent() {
370                Ok(Some(p)) => p,
371                Ok(None) => match self.boundary.strict_join("") {
372                    Ok(root) => root,
373                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
374                },
375                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
376            };
377            match parent.strict_join(dest_ref) {
378                Ok(p) => p,
379                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
380            }
381        };
382
383        std::fs::copy(self.path(), dest_path.path())?;
384        Ok(dest_path)
385    }
386
387    /// Removes the file at the system path.
388    pub fn remove_file(&self) -> std::io::Result<()> {
389        std::fs::remove_file(&self.path)
390    }
391
392    /// Removes the directory at the system path.
393    pub fn remove_dir(&self) -> std::io::Result<()> {
394        std::fs::remove_dir(&self.path)
395    }
396
397    /// Recursively removes the directory and its contents.
398    pub fn remove_dir_all(&self) -> std::io::Result<()> {
399        std::fs::remove_dir_all(&self.path)
400    }
401}
402
403#[cfg(feature = "serde")]
404impl<Marker> serde::Serialize for StrictPath<Marker> {
405    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
406    where
407        S: serde::Serializer,
408    {
409        serializer.serialize_str(self.strictpath_to_string_lossy().as_ref())
410    }
411}
412
413impl<Marker> fmt::Debug for StrictPath<Marker> {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        f.debug_struct("StrictPath")
416            .field("path", &self.path)
417            .field("boundary", &self.boundary.path())
418            .field("marker", &std::any::type_name::<Marker>())
419            .finish()
420    }
421}
422
423impl<Marker> PartialEq for StrictPath<Marker> {
424    #[inline]
425    fn eq(&self, other: &Self) -> bool {
426        self.path.as_ref() == other.path.as_ref()
427    }
428}
429
430impl<Marker> Eq for StrictPath<Marker> {}
431
432impl<Marker> Hash for StrictPath<Marker> {
433    #[inline]
434    fn hash<H: Hasher>(&self, state: &mut H) {
435        self.path.hash(state);
436    }
437}
438
439impl<Marker> PartialOrd for StrictPath<Marker> {
440    #[inline]
441    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
442        Some(self.cmp(other))
443    }
444}
445
446impl<Marker> Ord for StrictPath<Marker> {
447    #[inline]
448    fn cmp(&self, other: &Self) -> Ordering {
449        self.path.cmp(&other.path)
450    }
451}
452
453impl<T: AsRef<Path>, Marker> PartialEq<T> for StrictPath<Marker> {
454    fn eq(&self, other: &T) -> bool {
455        self.path.as_ref() == other.as_ref()
456    }
457}
458
459impl<T: AsRef<Path>, Marker> PartialOrd<T> for StrictPath<Marker> {
460    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
461        Some(self.path.as_ref().cmp(other.as_ref()))
462    }
463}
464
465impl<Marker> PartialEq<crate::path::virtual_path::VirtualPath<Marker>> for StrictPath<Marker> {
466    #[inline]
467    fn eq(&self, other: &crate::path::virtual_path::VirtualPath<Marker>) -> bool {
468        self.path.as_ref() == other.interop_path()
469    }
470}