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/// SUMMARY:
12/// Hold a validated, system-facing filesystem path guaranteed to be within a `PathBoundary`.
13///
14/// DETAILS:
15/// Use when you need system-facing I/O with safety proofs. For user-facing display and rooted
16/// virtual operations prefer `VirtualPath`. Operations like `strict_join` and
17/// `strictpath_parent` preserve guarantees. `Display` shows the real system path. String
18/// accessors are prefixed with `strictpath_` to avoid confusion.
19#[derive(Clone)]
20pub struct StrictPath<Marker = ()> {
21    path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
22    boundary: Arc<crate::PathBoundary<Marker>>,
23    _marker: PhantomData<Marker>,
24}
25
26impl<Marker> StrictPath<Marker> {
27    /// SUMMARY:
28    /// Create the base `StrictPath` anchored at the provided boundary directory.
29    ///
30    /// PARAMETERS:
31    /// - `dir_path` (`AsRef<Path>`): Boundary directory (must exist).
32    ///
33    /// RETURNS:
34    /// - `Result<StrictPath<Marker>>`: Base path ("" join) within the boundary.
35    ///
36    /// ERRORS:
37    /// - `StrictPathError::InvalidRestriction`: If the boundary cannot be created/validated.
38    ///
39    /// NOTE: Prefer passing `PathBoundary` in reusable flows.
40    pub fn with_boundary<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
41        let boundary = crate::PathBoundary::try_new(dir_path)?;
42        boundary.strict_join("")
43    }
44
45    /// SUMMARY:
46    /// Create the base `StrictPath`, creating the boundary directory if missing.
47    pub fn with_boundary_create<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
48        let boundary = crate::PathBoundary::try_new_create(dir_path)?;
49        boundary.strict_join("")
50    }
51    pub(crate) fn new(
52        boundary: Arc<crate::PathBoundary<Marker>>,
53        validated_path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
54    ) -> Self {
55        Self {
56            path: validated_path,
57            boundary,
58            _marker: PhantomData,
59        }
60    }
61
62    #[inline]
63    pub(crate) fn boundary(&self) -> &crate::PathBoundary<Marker> {
64        &self.boundary
65    }
66
67    #[inline]
68    pub(crate) fn path(&self) -> &Path {
69        &self.path
70    }
71
72    /// SUMMARY:
73    /// Return a lossy `String` view of the system path. Prefer `interop_path()` for interop.
74    #[inline]
75    pub fn strictpath_to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
76        self.path.to_string_lossy()
77    }
78
79    /// SUMMARY:
80    /// Return the underlying system path as `&str` if valid UTF‑8; otherwise `None`.
81    #[inline]
82    pub fn strictpath_to_str(&self) -> Option<&str> {
83        self.path.to_str()
84    }
85
86    /// SUMMARY:
87    /// Return the underlying system path as `&OsStr` for allocation‑free `AsRef<Path>` interop.
88    #[inline]
89    pub fn interop_path(&self) -> &OsStr {
90        self.path.as_os_str()
91    }
92
93    /// SUMMARY:
94    /// Return a `Display` wrapper that shows the real system path.
95    #[inline]
96    pub fn strictpath_display(&self) -> std::path::Display<'_> {
97        self.path.display()
98    }
99
100    /// SUMMARY:
101    /// Consume and return the inner `PathBuf` (escape hatch). Prefer `interop_path()` to borrow.
102    #[inline]
103    pub fn unstrict(self) -> PathBuf {
104        self.path.into_inner()
105    }
106
107    /// SUMMARY:
108    /// Convert this `StrictPath` into a user‑facing `VirtualPath`.
109    #[inline]
110    pub fn virtualize(self) -> crate::path::virtual_path::VirtualPath<Marker> {
111        crate::path::virtual_path::VirtualPath::new(self)
112    }
113
114    /// SUMMARY:
115    /// Consume and return the associated `PathBoundary` (infallible).
116    #[inline]
117    pub fn try_into_boundary(self) -> crate::PathBoundary<Marker> {
118        // Clone the underlying boundary reference (cheap, small struct)
119        self.boundary.as_ref().clone()
120    }
121
122    /// SUMMARY:
123    /// Consume and return the `PathBoundary`, creating the directory if missing (best‑effort).
124    #[inline]
125    pub fn try_into_boundary_create(self) -> crate::PathBoundary<Marker> {
126        let boundary = self.boundary.as_ref().clone();
127        if !boundary.exists() {
128            // Best-effort create; ignore error and let later operations surface it
129            let _ = std::fs::create_dir_all(boundary.as_ref());
130        }
131        boundary
132    }
133
134    /// SUMMARY:
135    /// Join a path segment and re-validate against the boundary.
136    ///
137    /// NOTE:
138    /// Never call `Path::join` on a leaked system path (e.g., from `interop_path()` or `unstrict()`); always re-validate through this method.
139    ///
140    /// PARAMETERS:
141    /// - `path` (`AsRef<Path>`): Segment or absolute path to validate.
142    ///
143    /// RETURNS:
144    /// - `Result<StrictPath<Marker>>`: Validated path inside the boundary.
145    ///
146    /// ERRORS:
147    /// - `StrictPathError::WindowsShortName` (windows), `StrictPathError::PathResolutionError`,
148    ///   `StrictPathError::PathEscapesBoundary`.
149    #[inline]
150    pub fn strict_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
151        let new_systempath = self.path.join(path);
152        self.boundary.strict_join(new_systempath)
153    }
154
155    /// SUMMARY:
156    /// Return the parent as a new `StrictPath`, or `None` at the boundary root.
157    pub fn strictpath_parent(&self) -> Result<Option<Self>> {
158        match self.path.parent() {
159            Some(p) => match self.boundary.strict_join(p) {
160                Ok(p) => Ok(Some(p)),
161                Err(e) => Err(e),
162            },
163            None => Ok(None),
164        }
165    }
166
167    /// SUMMARY:
168    /// Return a new path with file name changed, re‑validating against the boundary.
169    #[inline]
170    pub fn strictpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
171        let new_systempath = self.path.with_file_name(file_name);
172        self.boundary.strict_join(new_systempath)
173    }
174
175    /// SUMMARY:
176    /// Return a new path with extension changed; error at the boundary root.
177    pub fn strictpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
178        let system_path = &self.path;
179        if system_path.file_name().is_none() {
180            return Err(StrictPathError::path_escapes_boundary(
181                self.path.to_path_buf(),
182                self.boundary.path().to_path_buf(),
183            ));
184        }
185        let new_systempath = system_path.with_extension(extension);
186        self.boundary.strict_join(new_systempath)
187    }
188
189    /// Returns the file name component of the system path, if any.
190    #[inline]
191    pub fn strictpath_file_name(&self) -> Option<&OsStr> {
192        self.path.file_name()
193    }
194
195    /// Returns the file stem of the system path, if any.
196    #[inline]
197    pub fn strictpath_file_stem(&self) -> Option<&OsStr> {
198        self.path.file_stem()
199    }
200
201    /// Returns the extension of the system path, if any.
202    #[inline]
203    pub fn strictpath_extension(&self) -> Option<&OsStr> {
204        self.path.extension()
205    }
206
207    /// Returns `true` if the system path starts with the given prefix.
208    #[inline]
209    pub fn strictpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
210        self.path.starts_with(p.as_ref())
211    }
212
213    /// Returns `true` if the system path ends with the given suffix.
214    #[inline]
215    pub fn strictpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
216        self.path.ends_with(p.as_ref())
217    }
218
219    /// Returns `true` if the system path exists.
220    pub fn exists(&self) -> bool {
221        self.path.exists()
222    }
223
224    /// Returns `true` if the system path is a file.
225    pub fn is_file(&self) -> bool {
226        self.path.is_file()
227    }
228
229    /// Returns `true` if the system path is a directory.
230    pub fn is_dir(&self) -> bool {
231        self.path.is_dir()
232    }
233
234    /// Returns the metadata for the system path.
235    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
236        std::fs::metadata(&self.path)
237    }
238
239    /// SUMMARY:
240    /// Read directory entries at this path (discovery). Re‑join names through strict/virtual APIs before I/O.
241    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
242        std::fs::read_dir(&self.path)
243    }
244
245    /// Reads the file contents as `String`.
246    pub fn read_to_string(&self) -> std::io::Result<String> {
247        std::fs::read_to_string(&self.path)
248    }
249
250    /// Reads the file contents as raw bytes.
251    #[deprecated(since = "0.1.0-alpha.5", note = "Use read() instead")]
252    pub fn read_bytes(&self) -> std::io::Result<Vec<u8>> {
253        std::fs::read(&self.path)
254    }
255
256    /// Writes raw bytes to the file, creating it if it does not exist.
257    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
258    pub fn write_bytes(&self, data: &[u8]) -> std::io::Result<()> {
259        std::fs::write(&self.path, data)
260    }
261
262    /// Writes a UTF-8 string to the file, creating it if it does not exist.
263    #[deprecated(since = "0.1.0-alpha.5", note = "Use write(...) instead")]
264    pub fn write_string(&self, data: &str) -> std::io::Result<()> {
265        std::fs::write(&self.path, data)
266    }
267
268    /// Reads the file contents as raw bytes (replacement for `read_bytes`).
269    #[inline]
270    pub fn read(&self) -> std::io::Result<Vec<u8>> {
271        std::fs::read(&self.path)
272    }
273
274    /// SUMMARY:
275    /// Write bytes to the file (create if missing). Accepts any `AsRef<[u8]>` (e.g., `&str`, `&[u8]`).
276    #[inline]
277    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
278        std::fs::write(&self.path, contents)
279    }
280
281    /// Creates all directories in the system path if missing (like `std::fs::create_dir_all`).
282    pub fn create_dir_all(&self) -> std::io::Result<()> {
283        std::fs::create_dir_all(&self.path)
284    }
285
286    /// Creates the directory at the system path (non-recursive, like `std::fs::create_dir`).
287    ///
288    /// Fails if the parent directory does not exist. Use `create_dir_all` to
289    /// create missing parent directories recursively.
290    pub fn create_dir(&self) -> std::io::Result<()> {
291        std::fs::create_dir(&self.path)
292    }
293
294    /// SUMMARY:
295    /// Create only the immediate parent directory (non‑recursive). `Ok(())` at the boundary root.
296    pub fn create_parent_dir(&self) -> std::io::Result<()> {
297        match self.strictpath_parent() {
298            Ok(Some(parent)) => parent.create_dir(),
299            Ok(None) => Ok(()),
300            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
301            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
302        }
303    }
304
305    /// SUMMARY:
306    /// Recursively create all missing directories up to the immediate parent. `Ok(())` at root.
307    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
308        match self.strictpath_parent() {
309            Ok(Some(parent)) => parent.create_dir_all(),
310            Ok(None) => Ok(()),
311            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
312            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
313        }
314    }
315
316    /// SUMMARY:
317    /// Create a symbolic link at this location pointing to `target` (same boundary required).
318    /// On Windows, file vs directory symlink is selected by target metadata (or best‑effort when missing).
319    pub fn strict_symlink(&self, link_path: &Self) -> std::io::Result<()> {
320        if self.boundary.path() != link_path.boundary.path() {
321            let err = StrictPathError::path_escapes_boundary(
322                link_path.path().to_path_buf(),
323                self.boundary.path().to_path_buf(),
324            );
325            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
326        }
327
328        #[cfg(unix)]
329        {
330            std::os::unix::fs::symlink(self.path(), link_path.path())?;
331        }
332
333        #[cfg(windows)]
334        {
335            create_windows_symlink(self.path(), link_path.path())?;
336        }
337
338        Ok(())
339    }
340
341    /// SUMMARY:
342    /// Create a hard link at `link_path` pointing to this path (same boundary; caller creates parents).
343    pub fn strict_hard_link(&self, link_path: &Self) -> std::io::Result<()> {
344        if self.boundary.path() != link_path.boundary.path() {
345            let err = StrictPathError::path_escapes_boundary(
346                link_path.path().to_path_buf(),
347                self.boundary.path().to_path_buf(),
348            );
349            return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
350        }
351
352        std::fs::hard_link(self.path(), link_path.path())?;
353
354        Ok(())
355    }
356
357    /// SUMMARY:
358    /// Rename/move within the same boundary. Relative destinations are siblings; absolute are validated.
359    /// Parents are not created automatically.
360    pub fn strict_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
361        let dest_ref = dest.as_ref();
362
363        // Compute destination under the parent directory for relative paths; allow absolute too
364        let dest_path = if dest_ref.is_absolute() {
365            match self.boundary.strict_join(dest_ref) {
366                Ok(p) => p,
367                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
368            }
369        } else {
370            let parent = match self.strictpath_parent() {
371                Ok(Some(p)) => p,
372                Ok(None) => match self.boundary.strict_join("") {
373                    Ok(root) => root,
374                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
375                },
376                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
377            };
378            match parent.strict_join(dest_ref) {
379                Ok(p) => p,
380                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
381            }
382        };
383
384        std::fs::rename(self.path(), dest_path.path())
385    }
386
387    /// SUMMARY:
388    /// Copy within the same boundary. Relative destinations are siblings; absolute are validated.
389    /// Parents are not created automatically. Returns bytes copied.
390    pub fn strict_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
391        let dest_ref = dest.as_ref();
392
393        // Compute destination under the parent directory for relative paths; allow absolute too
394        let dest_path = if dest_ref.is_absolute() {
395            match self.boundary.strict_join(dest_ref) {
396                Ok(p) => p,
397                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
398            }
399        } else {
400            let parent = match self.strictpath_parent() {
401                Ok(Some(p)) => p,
402                Ok(None) => match self.boundary.strict_join("") {
403                    Ok(root) => root,
404                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
405                },
406                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
407            };
408            match parent.strict_join(dest_ref) {
409                Ok(p) => p,
410                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
411            }
412        };
413
414        std::fs::copy(self.path(), dest_path.path())
415    }
416
417    /// SUMMARY:
418    /// Remove the file at this path.
419    pub fn remove_file(&self) -> std::io::Result<()> {
420        std::fs::remove_file(&self.path)
421    }
422
423    /// SUMMARY:
424    /// Remove the directory at this path.
425    pub fn remove_dir(&self) -> std::io::Result<()> {
426        std::fs::remove_dir(&self.path)
427    }
428
429    /// SUMMARY:
430    /// Recursively remove the directory and its contents.
431    pub fn remove_dir_all(&self) -> std::io::Result<()> {
432        std::fs::remove_dir_all(&self.path)
433    }
434}
435
436#[cfg(feature = "serde")]
437impl<Marker> serde::Serialize for StrictPath<Marker> {
438    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
439    where
440        S: serde::Serializer,
441    {
442        serializer.serialize_str(self.strictpath_to_string_lossy().as_ref())
443    }
444}
445
446impl<Marker> fmt::Debug for StrictPath<Marker> {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        f.debug_struct("StrictPath")
449            .field("path", &self.path)
450            .field("boundary", &self.boundary.path())
451            .field("marker", &std::any::type_name::<Marker>())
452            .finish()
453    }
454}
455
456#[cfg(windows)]
457fn create_windows_symlink(src: &Path, link: &Path) -> std::io::Result<()> {
458    use std::os::windows::fs::{symlink_dir, symlink_file};
459
460    match std::fs::metadata(src) {
461        Ok(metadata) => {
462            if metadata.is_dir() {
463                symlink_dir(src, link)
464            } else {
465                symlink_file(src, link)
466            }
467        }
468        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
469            match symlink_file(src, link) {
470                Ok(()) => Ok(()),
471                Err(file_err) => {
472                    if let Some(code) = file_err.raw_os_error() {
473                        const ERROR_DIRECTORY: i32 = 267; // target resolved as directory
474                        if code == ERROR_DIRECTORY {
475                            return symlink_dir(src, link);
476                        }
477                    }
478                    Err(file_err)
479                }
480            }
481        }
482        Err(err) => Err(err),
483    }
484}
485
486impl<Marker> PartialEq for StrictPath<Marker> {
487    #[inline]
488    fn eq(&self, other: &Self) -> bool {
489        self.path.as_ref() == other.path.as_ref()
490    }
491}
492
493impl<Marker> Eq for StrictPath<Marker> {}
494
495impl<Marker> Hash for StrictPath<Marker> {
496    #[inline]
497    fn hash<H: Hasher>(&self, state: &mut H) {
498        self.path.hash(state);
499    }
500}
501
502impl<Marker> PartialOrd for StrictPath<Marker> {
503    #[inline]
504    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
505        Some(self.cmp(other))
506    }
507}
508
509impl<Marker> Ord for StrictPath<Marker> {
510    #[inline]
511    fn cmp(&self, other: &Self) -> Ordering {
512        self.path.cmp(&other.path)
513    }
514}
515
516impl<T: AsRef<Path>, Marker> PartialEq<T> for StrictPath<Marker> {
517    fn eq(&self, other: &T) -> bool {
518        self.path.as_ref() == other.as_ref()
519    }
520}
521
522impl<T: AsRef<Path>, Marker> PartialOrd<T> for StrictPath<Marker> {
523    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
524        Some(self.path.as_ref().cmp(other.as_ref()))
525    }
526}
527
528impl<Marker> PartialEq<crate::path::virtual_path::VirtualPath<Marker>> for StrictPath<Marker> {
529    #[inline]
530    fn eq(&self, other: &crate::path::virtual_path::VirtualPath<Marker>) -> bool {
531        self.path.as_ref() == other.interop_path()
532    }
533}