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