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    /// This is sugar for `PathBoundary::try_new(root)?.strict_join("")`.
39    /// Prefer this in simple flows; use `PathBoundary` directly when you need
40    /// to reuse policy across many paths or pass it as a parameter.
41    pub fn with_boundary<P: AsRef<Path>>(root: P) -> Result<Self> {
42        let boundary = crate::PathBoundary::try_new(root)?;
43        boundary.strict_join("")
44    }
45
46    /// Create a root `StrictPath`, creating the boundary directory if missing.
47    ///
48    /// This is sugar for `PathBoundary::try_new_create(root)?.strict_join("")`.
49    pub fn with_boundary_create<P: AsRef<Path>>(root: P) -> Result<Self> {
50        let boundary = crate::PathBoundary::try_new_create(root)?;
51        boundary.strict_join("")
52    }
53    pub(crate) fn new(
54        boundary: Arc<crate::PathBoundary<Marker>>,
55        validated_path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
56    ) -> Self {
57        Self {
58            path: validated_path,
59            boundary,
60            _marker: PhantomData,
61        }
62    }
63
64    #[inline]
65    pub(crate) fn boundary(&self) -> &crate::PathBoundary<Marker> {
66        &self.boundary
67    }
68
69    #[inline]
70    pub(crate) fn path(&self) -> &Path {
71        &self.path
72    }
73
74    /// For interop with APIs that accept `AsRef<Path>`, prefer
75    /// `interop_path()` to avoid allocation.
76    #[inline]
77    pub fn strictpath_to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
78        self.path.to_string_lossy()
79    }
80
81    /// Returns the underlying system path as `&str` if valid UTF-8.
82    ///
83    /// For lossless interop on any platform, prefer `interop_path()`.
84    #[inline]
85    pub fn strictpath_to_str(&self) -> Option<&str> {
86        self.path.to_str()
87    }
88
89    /// Returns the underlying system path as `&OsStr` (lossless; implements `AsRef<Path>`).
90    ///
91    /// Use this when passing to external APIs that accept `AsRef<Path>`.
92    #[inline]
93    pub fn interop_path(&self) -> &OsStr {
94        self.path.as_os_str()
95    }
96
97    /// Returns a `Display` wrapper that shows the real system path.
98    #[inline]
99    pub fn strictpath_display(&self) -> std::path::Display<'_> {
100        self.path.display()
101    }
102
103    /// Consumes this `RestrictedPath` and returns the inner `PathBuf` (escape hatch).
104    ///
105    /// Prefer borrowing via `interop_path()` when possible.
106    #[inline]
107    pub fn unstrict(self) -> PathBuf {
108        self.path.into_inner()
109    }
110
111    /// Converts this `RestrictedPath` into a user-facing `VirtualPath`.
112    #[inline]
113    pub fn virtualize(self) -> crate::path::virtual_path::VirtualPath<Marker> {
114        crate::path::virtual_path::VirtualPath::new(self)
115    }
116
117    /// Safely joins a system path segment and re-validates against the restriction.
118    ///
119    /// Do not use `Path::join` on leaked paths. Always use this method to ensure
120    /// PathBoundary containment is preserved.
121    #[inline]
122    pub fn strict_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
123        let new_systempath = self.path.join(path);
124        self.boundary.strict_join(new_systempath)
125    }
126
127    /// Returns the parent directory as a new `StrictPath`, or `None` if at the PathBoundary root.
128    pub fn strictpath_parent(&self) -> Result<Option<Self>> {
129        match self.path.parent() {
130            Some(p) => match self.boundary.strict_join(p) {
131                Ok(p) => Ok(Some(p)),
132                Err(e) => Err(e),
133            },
134            None => Ok(None),
135        }
136    }
137
138    /// Returns a new `StrictPath` with the file name changed, re-validating against the restriction.
139    #[inline]
140    pub fn strictpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
141        let new_systempath = self.path.with_file_name(file_name);
142        self.boundary.strict_join(new_systempath)
143    }
144
145    /// Returns a new `StrictPath` with the extension changed, or an error if at PathBoundary root.
146    pub fn strictpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
147        let system_path = &self.path;
148        if system_path.file_name().is_none() {
149            return Err(StrictPathError::path_escapes_boundary(
150                self.path.to_path_buf(),
151                self.boundary.path().to_path_buf(),
152            ));
153        }
154        let new_systempath = system_path.with_extension(extension);
155        self.boundary.strict_join(new_systempath)
156    }
157
158    /// Returns the file name component of the system path, if any.
159    #[inline]
160    pub fn strictpath_file_name(&self) -> Option<&OsStr> {
161        self.path.file_name()
162    }
163
164    /// Returns the file stem of the system path, if any.
165    #[inline]
166    pub fn strictpath_file_stem(&self) -> Option<&OsStr> {
167        self.path.file_stem()
168    }
169
170    /// Returns the extension of the system path, if any.
171    #[inline]
172    pub fn strictpath_extension(&self) -> Option<&OsStr> {
173        self.path.extension()
174    }
175
176    /// Returns `true` if the system path starts with the given prefix.
177    #[inline]
178    pub fn strictpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
179        self.path.starts_with(p.as_ref())
180    }
181
182    /// Returns `true` if the system path ends with the given suffix.
183    #[inline]
184    pub fn strictpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
185        self.path.ends_with(p.as_ref())
186    }
187
188    /// Returns `true` if the system path exists.
189    pub fn exists(&self) -> bool {
190        self.path.exists()
191    }
192
193    /// Returns `true` if the system path is a file.
194    pub fn is_file(&self) -> bool {
195        self.path.is_file()
196    }
197
198    /// Returns `true` if the system path is a directory.
199    pub fn is_dir(&self) -> bool {
200        self.path.is_dir()
201    }
202
203    /// Returns the metadata for the system path.
204    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
205        std::fs::metadata(&self.path)
206    }
207
208    /// Reads the file contents as `String`.
209    pub fn read_to_string(&self) -> std::io::Result<String> {
210        std::fs::read_to_string(&self.path)
211    }
212
213    /// Reads the file contents as raw bytes.
214    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
215        std::fs::read(&self.path)
216    }
217
218    /// Writes raw bytes to the file, creating it if it does not exist.
219    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
220        std::fs::write(&self.path, data)
221    }
222
223    /// Writes a UTF-8 string to the file, creating it if it does not exist.
224    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
225        std::fs::write(&self.path, data)
226    }
227
228    /// Creates all directories in the system path if missing (like `std::fs::create_dir_all`).
229    pub fn create_dir_all(&self) -> std::io::Result<()> {
230        std::fs::create_dir_all(&self.path)
231    }
232
233    /// Creates the directory at the system path (non-recursive, like `std::fs::create_dir`).
234    ///
235    /// Fails if the parent directory does not exist. Use `create_dir_all` to
236    /// create missing parent directories recursively.
237    pub fn create_dir(&self) -> std::io::Result<()> {
238        std::fs::create_dir(&self.path)
239    }
240
241    /// Creates only the immediate parent directory of this system path (non-recursive).
242    ///
243    /// Returns `Ok(())` if at the restricted path root (no parent). Fails if the parent's
244    /// parent is missing. Use `create_parent_dir_all` to create the full chain.
245    pub fn create_parent_dir(&self) -> std::io::Result<()> {
246        match self.strictpath_parent() {
247            Ok(Some(parent)) => parent.create_dir(),
248            Ok(None) => Ok(()),
249            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
250            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
251        }
252    }
253
254    /// Recursively creates all missing directories up to the immediate parent of this system path.
255    ///
256    /// Returns `Ok(())` if at the restricted path root (no parent).
257    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
258        match self.strictpath_parent() {
259            Ok(Some(parent)) => parent.create_dir_all(),
260            Ok(None) => Ok(()),
261            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
262            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
263        }
264    }
265
266    /// Renames or moves this path to a new location within the same `PathBoundary`.
267    ///
268    /// Relative destinations are interpreted as siblings (resolved against this path's parent
269    /// directory), not children. Absolute destinations are validated against the `PathBoundary`.
270    /// No parent directories are created implicitly; call `create_parent_dir_all()` on the
271    /// desired destination path beforehand if needed. Returns the destination `StrictPath`.
272    pub fn strict_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<Self> {
273        let dest_ref = dest.as_ref();
274
275        // Compute destination under the parent directory for relative paths; allow absolute too
276        let dest_path = if dest_ref.is_absolute() {
277            match self.boundary.strict_join(dest_ref) {
278                Ok(p) => p,
279                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
280            }
281        } else {
282            let parent = match self.strictpath_parent() {
283                Ok(Some(p)) => p,
284                Ok(None) => match self.boundary.strict_join("") {
285                    Ok(root) => root,
286                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
287                },
288                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
289            };
290            match parent.strict_join(dest_ref) {
291                Ok(p) => p,
292                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
293            }
294        };
295
296        std::fs::rename(self.path(), dest_path.path())?;
297        Ok(dest_path)
298    }
299
300    /// Removes the file at the system path.
301    pub fn remove_file(&self) -> std::io::Result<()> {
302        std::fs::remove_file(&self.path)
303    }
304
305    /// Removes the directory at the system path.
306    pub fn remove_dir(&self) -> std::io::Result<()> {
307        std::fs::remove_dir(&self.path)
308    }
309
310    /// Recursively removes the directory and its contents.
311    pub fn remove_dir_all(&self) -> std::io::Result<()> {
312        std::fs::remove_dir_all(&self.path)
313    }
314}
315
316#[cfg(feature = "serde")]
317impl<Marker> serde::Serialize for StrictPath<Marker> {
318    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
319    where
320        S: serde::Serializer,
321    {
322        serializer.serialize_str(self.strictpath_to_string_lossy().as_ref())
323    }
324}
325
326impl<Marker> fmt::Debug for StrictPath<Marker> {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        f.debug_struct("StrictPath")
329            .field("path", &self.path)
330            .field("boundary", &self.boundary.path())
331            .field("marker", &std::any::type_name::<Marker>())
332            .finish()
333    }
334}
335
336impl<Marker> PartialEq for StrictPath<Marker> {
337    #[inline]
338    fn eq(&self, other: &Self) -> bool {
339        self.path.as_ref() == other.path.as_ref()
340    }
341}
342
343impl<Marker> Eq for StrictPath<Marker> {}
344
345impl<Marker> Hash for StrictPath<Marker> {
346    #[inline]
347    fn hash<H: Hasher>(&self, state: &mut H) {
348        self.path.hash(state);
349    }
350}
351
352impl<Marker> PartialOrd for StrictPath<Marker> {
353    #[inline]
354    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
355        Some(self.cmp(other))
356    }
357}
358
359impl<Marker> Ord for StrictPath<Marker> {
360    #[inline]
361    fn cmp(&self, other: &Self) -> Ordering {
362        self.path.cmp(&other.path)
363    }
364}
365
366impl<T: AsRef<Path>, Marker> PartialEq<T> for StrictPath<Marker> {
367    fn eq(&self, other: &T) -> bool {
368        self.path.as_ref() == other.as_ref()
369    }
370}
371
372impl<T: AsRef<Path>, Marker> PartialOrd<T> for StrictPath<Marker> {
373    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
374        Some(self.path.as_ref().cmp(other.as_ref()))
375    }
376}
377
378impl<Marker> PartialEq<crate::path::virtual_path::VirtualPath<Marker>> for StrictPath<Marker> {
379    #[inline]
380    fn eq(&self, other: &crate::path::virtual_path::VirtualPath<Marker>) -> bool {
381        self.path.as_ref() == other.interop_path()
382    }
383}