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    /// Creates a symbolic link at this location pointing to `target`.
316    ///
317    /// Both the link path (`self`) and the target must belong to the same `PathBoundary`. The caller
318    /// is responsible for ensuring the parent directory exists. On Windows the target's current
319    /// metadata determines whether a file or directory symbolic link is created; if the target does
320    /// not yet exist a file symbolic link is attempted first.
321    pub fn strict_symlink(&self, link_path: &Self) -> std::io::Result<()> {
322        if self.boundary.path() != link_path.boundary.path() {
323            let err = StrictPathError::path_escapes_boundary(
324                link_path.path().to_path_buf(),
325                self.boundary.path().to_path_buf(),
326            );
327            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
328        }
329
330        #[cfg(unix)]
331        {
332            std::os::unix::fs::symlink(self.path(), link_path.path())?;
333        }
334
335        #[cfg(windows)]
336        {
337            create_windows_symlink(self.path(), link_path.path())?;
338        }
339
340        Ok(())
341    }
342
343    /// Creates a hard link at `link_path` pointing to this path.
344    ///
345    /// Both paths must belong to the same `PathBoundary`. The caller is responsible for ensuring the
346    /// parent directory of `link_path` exists.
347    pub fn strict_hard_link(&self, link_path: &Self) -> std::io::Result<()> {
348        if self.boundary.path() != link_path.boundary.path() {
349            let err = StrictPathError::path_escapes_boundary(
350                link_path.path().to_path_buf(),
351                self.boundary.path().to_path_buf(),
352            );
353            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
354        }
355
356        std::fs::hard_link(self.path(), link_path.path())?;
357
358        Ok(())
359    }
360
361    /// Renames or moves this path to a new location within the same `PathBoundary`.
362    ///
363    /// Relative destinations are interpreted as siblings (resolved against this path's parent
364    /// directory), not children. Absolute destinations are validated against the `PathBoundary`.
365    /// No parent directories are created implicitly; call `create_parent_dir_all()` on the
366    /// desired destination path beforehand if needed.
367    pub fn strict_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
368        let dest_ref = dest.as_ref();
369
370        // Compute destination under the parent directory for relative paths; allow absolute too
371        let dest_path = if dest_ref.is_absolute() {
372            match self.boundary.strict_join(dest_ref) {
373                Ok(p) => p,
374                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
375            }
376        } else {
377            let parent = match self.strictpath_parent() {
378                Ok(Some(p)) => p,
379                Ok(None) => match self.boundary.strict_join("") {
380                    Ok(root) => root,
381                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
382                },
383                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
384            };
385            match parent.strict_join(dest_ref) {
386                Ok(p) => p,
387                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
388            }
389        };
390
391        std::fs::rename(self.path(), dest_path.path())
392    }
393
394    /// Copies this file to a new location within the same `PathBoundary`.
395    ///
396    /// Semantics mirror `strict_rename` for destination resolution:
397    /// - Relative destinations are interpreted as siblings (resolved against this path's parent).
398    /// - Absolute destinations are validated against the `PathBoundary`.
399    ///
400    /// No parent directories are created implicitly; call `create_parent_dir_all()` on the
401    /// desired destination path beforehand if needed. Equivalent to
402    /// `std::fs::copy(self.interop_path(), dest.interop_path())` but with restriction-aware
403    /// destination validation. Returns the number of bytes copied.
404    pub fn strict_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
405        let dest_ref = dest.as_ref();
406
407        // Compute destination under the parent directory for relative paths; allow absolute too
408        let dest_path = if dest_ref.is_absolute() {
409            match self.boundary.strict_join(dest_ref) {
410                Ok(p) => p,
411                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
412            }
413        } else {
414            let parent = match self.strictpath_parent() {
415                Ok(Some(p)) => p,
416                Ok(None) => match self.boundary.strict_join("") {
417                    Ok(root) => root,
418                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
419                },
420                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
421            };
422            match parent.strict_join(dest_ref) {
423                Ok(p) => p,
424                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
425            }
426        };
427
428        std::fs::copy(self.path(), dest_path.path())
429    }
430
431    /// Removes the file at the system path.
432    pub fn remove_file(&self) -> std::io::Result<()> {
433        std::fs::remove_file(&self.path)
434    }
435
436    /// Removes the directory at the system path.
437    pub fn remove_dir(&self) -> std::io::Result<()> {
438        std::fs::remove_dir(&self.path)
439    }
440
441    /// Recursively removes the directory and its contents.
442    pub fn remove_dir_all(&self) -> std::io::Result<()> {
443        std::fs::remove_dir_all(&self.path)
444    }
445}
446
447#[cfg(feature = "serde")]
448impl<Marker> serde::Serialize for StrictPath<Marker> {
449    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
450    where
451        S: serde::Serializer,
452    {
453        serializer.serialize_str(self.strictpath_to_string_lossy().as_ref())
454    }
455}
456
457impl<Marker> fmt::Debug for StrictPath<Marker> {
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459        f.debug_struct("StrictPath")
460            .field("path", &self.path)
461            .field("boundary", &self.boundary.path())
462            .field("marker", &std::any::type_name::<Marker>())
463            .finish()
464    }
465}
466
467#[cfg(windows)]
468fn create_windows_symlink(src: &Path, link: &Path) -> std::io::Result<()> {
469    use std::os::windows::fs::{symlink_dir, symlink_file};
470
471    match std::fs::metadata(src) {
472        Ok(metadata) => {
473            if metadata.is_dir() {
474                symlink_dir(src, link)
475            } else {
476                symlink_file(src, link)
477            }
478        }
479        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
480            match symlink_file(src, link) {
481                Ok(()) => Ok(()),
482                Err(file_err) => {
483                    if let Some(code) = file_err.raw_os_error() {
484                        const ERROR_DIRECTORY: i32 = 267; // target resolved as directory
485                        if code == ERROR_DIRECTORY {
486                            return symlink_dir(src, link);
487                        }
488                    }
489                    Err(file_err)
490                }
491            }
492        }
493        Err(err) => Err(err),
494    }
495}
496
497impl<Marker> PartialEq for StrictPath<Marker> {
498    #[inline]
499    fn eq(&self, other: &Self) -> bool {
500        self.path.as_ref() == other.path.as_ref()
501    }
502}
503
504impl<Marker> Eq for StrictPath<Marker> {}
505
506impl<Marker> Hash for StrictPath<Marker> {
507    #[inline]
508    fn hash<H: Hasher>(&self, state: &mut H) {
509        self.path.hash(state);
510    }
511}
512
513impl<Marker> PartialOrd for StrictPath<Marker> {
514    #[inline]
515    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
516        Some(self.cmp(other))
517    }
518}
519
520impl<Marker> Ord for StrictPath<Marker> {
521    #[inline]
522    fn cmp(&self, other: &Self) -> Ordering {
523        self.path.cmp(&other.path)
524    }
525}
526
527impl<T: AsRef<Path>, Marker> PartialEq<T> for StrictPath<Marker> {
528    fn eq(&self, other: &T) -> bool {
529        self.path.as_ref() == other.as_ref()
530    }
531}
532
533impl<T: AsRef<Path>, Marker> PartialOrd<T> for StrictPath<Marker> {
534    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
535        Some(self.path.as_ref().cmp(other.as_ref()))
536    }
537}
538
539impl<Marker> PartialEq<crate::path::virtual_path::VirtualPath<Marker>> for StrictPath<Marker> {
540    #[inline]
541    fn eq(&self, other: &crate::path::virtual_path::VirtualPath<Marker>) -> bool {
542        self.path.as_ref() == other.interop_path()
543    }
544}