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/// Strip the Windows verbatim `\\?\` prefix from a path if present.
12///
13/// The `junction` crate does not handle verbatim prefix paths correctly - it creates
14/// broken junctions that return ERROR_INVALID_NAME (123) when accessed.
15/// This helper strips the prefix so junction creation works correctly.
16///
17/// See: <https://github.com/tesuji/junction/issues/30>
18#[cfg(all(windows, feature = "junctions"))]
19fn strip_verbatim_prefix(path: &Path) -> std::borrow::Cow<'_, Path> {
20    use std::borrow::Cow;
21    let s = path.as_os_str().to_string_lossy();
22    if let Some(rest) = s.strip_prefix(r"\\?\") {
23        Cow::Owned(PathBuf::from(rest))
24    } else {
25        Cow::Borrowed(path)
26    }
27}
28
29/// SUMMARY:
30/// Hold a validated, system-facing filesystem path guaranteed to be within a `PathBoundary`.
31///
32/// DETAILS:
33/// Use when you need system-facing I/O with safety proofs. For user-facing display and rooted
34/// virtual operations prefer `VirtualPath`. Operations like `strict_join` and
35/// `strictpath_parent` preserve guarantees. `Display` shows the real system path. String
36/// accessors are prefixed with `strictpath_` to avoid confusion.
37#[derive(Clone)]
38pub struct StrictPath<Marker = ()> {
39    path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
40    boundary: Arc<crate::PathBoundary<Marker>>,
41    _marker: PhantomData<Marker>,
42}
43
44impl<Marker> StrictPath<Marker> {
45    /// SUMMARY:
46    /// Create the base `StrictPath` anchored at the provided boundary directory.
47    ///
48    /// PARAMETERS:
49    /// - `dir_path` (`AsRef<Path>`): Boundary directory (must exist).
50    ///
51    /// RETURNS:
52    /// - `Result<StrictPath<Marker>>`: Base path ("" join) within the boundary.
53    ///
54    /// ERRORS:
55    /// - `StrictPathError::InvalidRestriction`: If the boundary cannot be created/validated.
56    ///
57    /// NOTE: Prefer passing `PathBoundary` in reusable flows.
58    pub fn with_boundary<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
59        let boundary = crate::PathBoundary::try_new(dir_path)?;
60        boundary.into_strictpath()
61    }
62
63    /// SUMMARY:
64    /// Create the base `StrictPath`, creating the boundary directory if missing.
65    pub fn with_boundary_create<P: AsRef<Path>>(dir_path: P) -> Result<Self> {
66        let boundary = crate::PathBoundary::try_new_create(dir_path)?;
67        boundary.into_strictpath()
68    }
69    pub(crate) fn new(
70        boundary: Arc<crate::PathBoundary<Marker>>,
71        validated_path: PathHistory<((Raw, Canonicalized), BoundaryChecked)>,
72    ) -> Self {
73        Self {
74            path: validated_path,
75            boundary,
76            _marker: PhantomData,
77        }
78    }
79
80    #[cfg(feature = "virtual-path")]
81    #[inline]
82    pub(crate) fn boundary(&self) -> &crate::PathBoundary<Marker> {
83        &self.boundary
84    }
85
86    #[inline]
87    pub(crate) fn path(&self) -> &Path {
88        &self.path
89    }
90
91    /// SUMMARY:
92    /// Return a lossy `String` view of the system path. Prefer `.interop_path()` only for unavoidable third-party interop.
93    #[inline]
94    pub fn strictpath_to_string_lossy(&self) -> std::borrow::Cow<'_, str> {
95        self.path.to_string_lossy()
96    }
97
98    /// SUMMARY:
99    /// Return the underlying system path as `&str` if valid UTF‑8; otherwise `None`.
100    #[inline]
101    pub fn strictpath_to_str(&self) -> Option<&str> {
102        self.path.to_str()
103    }
104
105    /// SUMMARY:
106    /// Return the underlying system path as `&OsStr` for unavoidable third-party `AsRef<Path>` interop.
107    #[inline]
108    pub fn interop_path(&self) -> &OsStr {
109        self.path.as_os_str()
110    }
111
112    /// SUMMARY:
113    /// Return a `Display` wrapper that shows the real system path.
114    #[inline]
115    pub fn strictpath_display(&self) -> std::path::Display<'_> {
116        self.path.display()
117    }
118
119    /// SUMMARY:
120    /// Consume and return the inner `PathBuf` (escape hatch). Prefer `.interop_path()` (third-party adapters only) to borrow.
121    #[inline]
122    pub fn unstrict(self) -> PathBuf {
123        self.path.into_inner()
124    }
125
126    /// SUMMARY:
127    /// Convert this `StrictPath` into a user‑facing `VirtualPath`.
128    #[cfg(feature = "virtual-path")]
129    #[inline]
130    pub fn virtualize(self) -> crate::path::virtual_path::VirtualPath<Marker> {
131        crate::path::virtual_path::VirtualPath::new(self)
132    }
133
134    /// SUMMARY:
135    /// Change the compile-time marker while reusing the validated strict path.
136    ///
137    /// WHEN TO USE:
138    /// - After authenticating/authorizing a user and granting them access to a path
139    /// - When escalating or downgrading permissions (e.g., ReadOnly → ReadWrite)
140    /// - When reinterpreting a path's domain (e.g., TempStorage → UserUploads)
141    ///
142    /// WHEN NOT TO USE:
143    /// - When converting between path types - conversions preserve markers automatically
144    /// - When the current marker already matches your needs - no transformation needed
145    /// - When you haven't verified authorization - NEVER change markers without checking permissions
146    ///
147    /// PARAMETERS:
148    /// - `_none_`
149    ///
150    /// RETURNS:
151    /// - `StrictPath<NewMarker>`: Same boundary-checked system path encoded with the new marker.
152    ///
153    /// ERRORS:
154    /// - `_none_`
155    ///
156    /// SECURITY:
157    /// The caller MUST ensure the new marker reflects real-world permissions. This method does not
158    /// perform any authorization checks.
159    ///
160    /// EXAMPLE:
161    /// ```rust
162    /// # use strict_path::{PathBoundary, StrictPath};
163    /// # use std::io;
164    /// # struct UserFiles;
165    /// # struct ReadOnly;
166    /// # struct ReadWrite;
167    /// # let boundary_dir = std::env::temp_dir().join("strict-path-change-marker-example");
168    /// # std::fs::create_dir_all(&boundary_dir.join("logs"))?;
169    /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
170    /// #
171    /// // Verify user can write before granting write access
172    /// fn authorize_write_access(
173    ///     user_id: &str,
174    ///     path: StrictPath<(UserFiles, ReadOnly)>
175    /// ) -> Result<StrictPath<(UserFiles, ReadWrite)>, &'static str> {
176    ///     if user_id == "admin" {
177    ///         Ok(path.change_marker())  // ✅ Transform after authorization check
178    ///     } else {
179    ///         Err("insufficient permissions")  // ❌ User lacks write permission
180    ///     }
181    /// }
182    ///
183    /// // Function requiring write permission - enforces type safety at compile time
184    /// fn write_log_entry(path: StrictPath<(UserFiles, ReadWrite)>, content: &str) -> io::Result<()> {
185    ///     path.write(content.as_bytes())
186    /// }
187    ///
188    /// // Start with read-only access from untrusted input
189    /// let requested_log = "logs/app.log"; // Untrusted input
190    /// let read_only_path: StrictPath<(UserFiles, ReadOnly)> =
191    ///     boundary.strict_join(requested_log)?.change_marker();
192    ///
193    /// // Elevate permissions after authorization
194    /// let read_write_path = authorize_write_access("admin", read_only_path)
195    ///     .expect("user must have sufficient permissions");
196    ///
197    /// // Now we can call functions requiring write access
198    /// write_log_entry(read_write_path, "Application started")?;
199    /// #
200    /// # std::fs::remove_dir_all(&boundary_dir)?;
201    /// # Ok::<_, Box<dyn std::error::Error>>(())
202    /// ```
203    ///
204    /// **Type Safety Guarantee:**
205    ///
206    /// The following code **fails to compile** because you cannot pass a path with one marker
207    /// type to a function expecting a different marker type. This compile-time check enforces
208    /// that permission changes are explicit and cannot be bypassed accidentally.
209    ///
210    /// ```compile_fail
211    /// # use strict_path::{PathBoundary, StrictPath};
212    /// # struct ReadOnly;
213    /// # struct WritePermission;
214    /// # let boundary_dir = std::env::temp_dir().join("strict-path-change-marker-deny");
215    /// # std::fs::create_dir_all(&boundary_dir).unwrap();
216    /// # let boundary: PathBoundary<ReadOnly> = PathBoundary::try_new(&boundary_dir).unwrap();
217    /// let read_only_path: StrictPath<ReadOnly> = boundary.strict_join("logs/app.log").unwrap();
218    /// fn require_write(_: StrictPath<WritePermission>) {}
219    /// // ❌ Compile error: expected `StrictPath<WritePermission>`, found `StrictPath<ReadOnly>`
220    /// require_write(read_only_path);
221    /// ```
222    #[inline]
223    pub fn change_marker<NewMarker>(self) -> StrictPath<NewMarker> {
224        let StrictPath { path, boundary, .. } = self;
225
226        // Try to unwrap the Arc (zero-cost if this is the only reference).
227        // If other references exist, clone the boundary (allocation needed).
228        let boundary_owned = Arc::try_unwrap(boundary).unwrap_or_else(|arc| (*arc).clone());
229        let new_boundary = Arc::new(boundary_owned.change_marker::<NewMarker>());
230
231        StrictPath {
232            path,
233            boundary: new_boundary,
234            _marker: PhantomData,
235        }
236    }
237
238    /// SUMMARY:
239    /// Consume and return a new `PathBoundary` anchored at this strict path.
240    ///
241    /// RETURNS:
242    /// - `Result<PathBoundary<Marker>>`: Boundary anchored at the strict path's
243    ///   system location (must already exist and be a directory).
244    ///
245    /// ERRORS:
246    /// - `StrictPathError::InvalidRestriction`: If the strict path does not exist
247    ///   or is not a directory.
248    #[inline]
249    pub fn try_into_boundary(self) -> Result<crate::PathBoundary<Marker>> {
250        let StrictPath { path, .. } = self;
251        crate::PathBoundary::try_new(path.into_inner())
252    }
253
254    /// SUMMARY:
255    /// Consume and return a `PathBoundary`, creating the directory if missing.
256    ///
257    /// RETURNS:
258    /// - `Result<PathBoundary<Marker>>`: Boundary anchored at the strict path's
259    ///   system location (created if necessary).
260    ///
261    /// ERRORS:
262    /// - `StrictPathError::InvalidRestriction`: If creation or canonicalization fails.
263    #[inline]
264    pub fn try_into_boundary_create(self) -> Result<crate::PathBoundary<Marker>> {
265        let StrictPath { path, .. } = self;
266        crate::PathBoundary::try_new_create(path.into_inner())
267    }
268
269    /// SUMMARY:
270    /// Join a path segment and re-validate against the boundary.
271    ///
272    /// NOTE:
273    /// Never wrap `.interop_path()` in `Path::new()` to use `Path::join()` — that defeats all security. Always use this method.
274    /// After `.unstrict()` (explicit escape hatch), you own a `PathBuf` and can do whatever you need.
275    ///
276    /// PARAMETERS:
277    /// - `path` (`AsRef<Path>`): Segment or absolute path to validate.
278    ///
279    /// RETURNS:
280    /// - `Result<StrictPath<Marker>>`: Validated path inside the boundary.
281    ///
282    /// ERRORS:
283    /// - `StrictPathError::PathResolutionError`, `StrictPathError::PathEscapesBoundary`.
284    #[inline]
285    pub fn strict_join<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
286        let new_systempath = self.path.join(path);
287        self.boundary.strict_join(new_systempath)
288    }
289
290    /// SUMMARY:
291    /// Return the parent as a new `StrictPath`, or `None` at the boundary root.
292    pub fn strictpath_parent(&self) -> Result<Option<Self>> {
293        match self.path.parent() {
294            Some(p) => match self.boundary.strict_join(p) {
295                Ok(p) => Ok(Some(p)),
296                Err(e) => Err(e),
297            },
298            None => Ok(None),
299        }
300    }
301
302    /// SUMMARY:
303    /// Return a new path with file name changed, re‑validating against the boundary.
304    #[inline]
305    pub fn strictpath_with_file_name<S: AsRef<OsStr>>(&self, file_name: S) -> Result<Self> {
306        let new_systempath = self.path.with_file_name(file_name);
307        self.boundary.strict_join(new_systempath)
308    }
309
310    /// SUMMARY:
311    /// Return a new path with extension changed; error at the boundary root.
312    pub fn strictpath_with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Result<Self> {
313        let system_path = &self.path;
314        if system_path.file_name().is_none() {
315            return Err(StrictPathError::path_escapes_boundary(
316                self.path.to_path_buf(),
317                self.boundary.path().to_path_buf(),
318            ));
319        }
320        let new_systempath = system_path.with_extension(extension);
321        self.boundary.strict_join(new_systempath)
322    }
323
324    /// Returns the file name component of the system path, if any.
325    #[inline]
326    pub fn strictpath_file_name(&self) -> Option<&OsStr> {
327        self.path.file_name()
328    }
329
330    /// Returns the file stem of the system path, if any.
331    #[inline]
332    pub fn strictpath_file_stem(&self) -> Option<&OsStr> {
333        self.path.file_stem()
334    }
335
336    /// Returns the extension of the system path, if any.
337    #[inline]
338    pub fn strictpath_extension(&self) -> Option<&OsStr> {
339        self.path.extension()
340    }
341
342    /// Returns `true` if the system path starts with the given prefix.
343    #[inline]
344    pub fn strictpath_starts_with<P: AsRef<Path>>(&self, p: P) -> bool {
345        self.path.starts_with(p.as_ref())
346    }
347
348    /// Returns `true` if the system path ends with the given suffix.
349    #[inline]
350    pub fn strictpath_ends_with<P: AsRef<Path>>(&self, p: P) -> bool {
351        self.path.ends_with(p.as_ref())
352    }
353
354    /// Returns `true` if the system path exists.
355    pub fn exists(&self) -> bool {
356        self.path.exists()
357    }
358
359    /// Returns `true` if the system path is a file.
360    pub fn is_file(&self) -> bool {
361        self.path.is_file()
362    }
363
364    /// Returns `true` if the system path is a directory.
365    pub fn is_dir(&self) -> bool {
366        self.path.is_dir()
367    }
368
369    /// Returns the metadata for the system path.
370    pub fn metadata(&self) -> std::io::Result<std::fs::Metadata> {
371        std::fs::metadata(&self.path)
372    }
373
374    /// SUMMARY:
375    /// Return the metadata for the system path without following symlinks (like `std::fs::symlink_metadata`).
376    ///
377    /// DETAILS:
378    /// This retrieves metadata about the path entry itself. On symlinks, this reports
379    /// information about the link, not the target.
380    #[inline]
381    pub fn symlink_metadata(&self) -> std::io::Result<std::fs::Metadata> {
382        std::fs::symlink_metadata(&self.path)
383    }
384
385    /// SUMMARY:
386    /// Read directory entries at this path (discovery). Re‑join names through strict/virtual APIs before I/O.
387    pub fn read_dir(&self) -> std::io::Result<std::fs::ReadDir> {
388        std::fs::read_dir(&self.path)
389    }
390
391    /// Reads the file contents as `String`.
392    pub fn read_to_string(&self) -> std::io::Result<String> {
393        std::fs::read_to_string(&self.path)
394    }
395
396    /// Reads the file contents as raw bytes.
397    #[inline]
398    pub fn read(&self) -> std::io::Result<Vec<u8>> {
399        std::fs::read(&self.path)
400    }
401
402    /// SUMMARY:
403    /// Write bytes to the file (create if missing). Accepts any `AsRef<[u8]>` (e.g., `&str`, `&[u8]`).
404    #[inline]
405    pub fn write<C: AsRef<[u8]>>(&self, contents: C) -> std::io::Result<()> {
406        std::fs::write(&self.path, contents)
407    }
408
409    /// SUMMARY:
410    /// Create or truncate the file at this strict path and return a writable handle.
411    ///
412    /// PARAMETERS:
413    /// - _none_
414    ///
415    /// RETURNS:
416    /// - `std::fs::File`: Writable handle scoped to this boundary.
417    ///
418    /// ERRORS:
419    /// - `std::io::Error`: Propagates OS errors when the parent directory is missing or file creation fails.
420    ///
421    /// EXAMPLE:
422    /// ```rust
423    /// # use strict_path::{PathBoundary, StrictPath};
424    /// # use std::io::Write;
425    /// # let boundary_dir = std::env::temp_dir().join("strict-path-create-file-example");
426    /// # std::fs::create_dir_all(&boundary_dir)?;
427    /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
428    /// // Untrusted input from request/CLI/config/etc.
429    /// let requested_file = "logs/app.log";
430    /// let log_path: StrictPath = boundary.strict_join(requested_file)?;
431    /// log_path.create_parent_dir_all()?;
432    /// let mut file = log_path.create_file()?;
433    /// file.write_all(b"session started")?;
434    /// # std::fs::remove_dir_all(&boundary_dir)?;
435    /// # Ok::<_, Box<dyn std::error::Error>>(())
436    /// ```
437    #[inline]
438    pub fn create_file(&self) -> std::io::Result<std::fs::File> {
439        std::fs::File::create(&self.path)
440    }
441
442    /// SUMMARY:
443    /// Open the file at this strict path in read-only mode.
444    ///
445    /// PARAMETERS:
446    /// - _none_
447    ///
448    /// RETURNS:
449    /// - `std::fs::File`: Read-only handle scoped to this boundary.
450    ///
451    /// ERRORS:
452    /// - `std::io::Error`: Propagates OS errors when the file is missing or inaccessible.
453    ///
454    /// EXAMPLE:
455    /// ```rust
456    /// # use strict_path::{PathBoundary, StrictPath};
457    /// # use std::io::{Read, Write};
458    /// # let boundary_dir = std::env::temp_dir().join("strict-path-open-file-example");
459    /// # std::fs::create_dir_all(&boundary_dir)?;
460    /// # let boundary: PathBoundary = PathBoundary::try_new(&boundary_dir)?;
461    /// // Untrusted input from request/CLI/config/etc.
462    /// let requested_file = "logs/session.log";
463    /// let transcript: StrictPath = boundary.strict_join(requested_file)?;
464    /// transcript.create_parent_dir_all()?;
465    /// transcript.write("session start")?;
466    /// let mut file = transcript.open_file()?;
467    /// let mut contents = String::new();
468    /// file.read_to_string(&mut contents)?;
469    /// assert_eq!(contents, "session start");
470    /// # std::fs::remove_dir_all(&boundary_dir)?;
471    /// # Ok::<_, Box<dyn std::error::Error>>(())
472    /// ```
473    #[inline]
474    pub fn open_file(&self) -> std::io::Result<std::fs::File> {
475        std::fs::File::open(&self.path)
476    }
477
478    /// Creates all directories in the system path if missing (like `std::fs::create_dir_all`).
479    pub fn create_dir_all(&self) -> std::io::Result<()> {
480        std::fs::create_dir_all(&self.path)
481    }
482
483    /// Creates the directory at the system path (non-recursive, like `std::fs::create_dir`).
484    ///
485    /// Fails if the parent directory does not exist. Use `create_dir_all` to
486    /// create missing parent directories recursively.
487    pub fn create_dir(&self) -> std::io::Result<()> {
488        std::fs::create_dir(&self.path)
489    }
490
491    /// SUMMARY:
492    /// Create only the immediate parent directory (non‑recursive). `Ok(())` at the boundary root.
493    pub fn create_parent_dir(&self) -> std::io::Result<()> {
494        match self.strictpath_parent() {
495            Ok(Some(parent)) => parent.create_dir(),
496            Ok(None) => Ok(()),
497            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
498            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
499        }
500    }
501
502    /// SUMMARY:
503    /// Recursively create all missing directories up to the immediate parent. `Ok(())` at boundary.
504    pub fn create_parent_dir_all(&self) -> std::io::Result<()> {
505        match self.strictpath_parent() {
506            Ok(Some(parent)) => parent.create_dir_all(),
507            Ok(None) => Ok(()),
508            Err(StrictPathError::PathEscapesBoundary { .. }) => Ok(()),
509            Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
510        }
511    }
512
513    /// SUMMARY:
514    /// Create a symbolic link at `link_path` pointing to this path (same boundary required).
515    /// On Windows, file vs directory symlink is selected by target metadata (or best‑effort when missing).
516    /// Relative paths are resolved as siblings; absolute paths are validated against the boundary.
517    pub fn strict_symlink<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
518        let link_ref = link_path.as_ref();
519
520        // Compute link path under the parent directory for relative paths; allow absolute too
521        let validated_link = if link_ref.is_absolute() {
522            match self.boundary.strict_join(link_ref) {
523                Ok(p) => p,
524                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
525            }
526        } else {
527            let parent = match self.strictpath_parent() {
528                Ok(Some(p)) => p,
529                Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
530                    Ok(root) => root,
531                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
532                },
533                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
534            };
535            match parent.strict_join(link_ref) {
536                Ok(p) => p,
537                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
538            }
539        };
540
541        #[cfg(unix)]
542        {
543            std::os::unix::fs::symlink(self.path(), validated_link.path())?;
544        }
545
546        #[cfg(windows)]
547        {
548            create_windows_symlink(self.path(), validated_link.path())?;
549        }
550
551        Ok(())
552    }
553
554    /// SUMMARY:
555    /// Create a hard link at `link_path` pointing to this path (same boundary; caller creates parents).
556    /// Relative paths are resolved as siblings; absolute paths are validated against the boundary.
557    pub fn strict_hard_link<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
558        let link_ref = link_path.as_ref();
559
560        // Compute link path under the parent directory for relative paths; allow absolute too
561        let validated_link = if link_ref.is_absolute() {
562            match self.boundary.strict_join(link_ref) {
563                Ok(p) => p,
564                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
565            }
566        } else {
567            let parent = match self.strictpath_parent() {
568                Ok(Some(p)) => p,
569                Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
570                    Ok(root) => root,
571                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
572                },
573                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
574            };
575            match parent.strict_join(link_ref) {
576                Ok(p) => p,
577                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
578            }
579        };
580
581        std::fs::hard_link(self.path(), validated_link.path())
582    }
583
584    /// SUMMARY:
585    /// Create a Windows NTFS directory junction at `link_path` pointing to this path.
586    ///
587    /// DETAILS:
588    /// - Windows-only and behind the `junctions` crate feature.
589    /// - Junctions are directory-only. This call will fail if the target is not a directory.
590    /// - Both `self` (target) and `link_path` must be within the same `PathBoundary`.
591    /// - Parents for `link_path` are not created automatically; call `create_parent_dir_all()` first.
592    ///
593    /// RETURNS:
594    /// - `io::Result<()>`: Mirrors OS semantics (and `junction` crate behavior).
595    ///
596    /// ERRORS:
597    /// - Returns an error if the target is not a directory, or the OS call fails.
598    #[cfg(all(windows, feature = "junctions"))]
599    pub fn strict_junction<P: AsRef<Path>>(&self, link_path: P) -> std::io::Result<()> {
600        let link_ref = link_path.as_ref();
601
602        // Compute link path under the parent directory for relative paths; allow absolute too
603        let validated_link = if link_ref.is_absolute() {
604            match self.boundary.strict_join(link_ref) {
605                Ok(p) => p,
606                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
607            }
608        } else {
609            let parent = match self.strictpath_parent() {
610                Ok(Some(p)) => p,
611                Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
612                    Ok(root) => root,
613                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
614                },
615                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
616            };
617            match parent.strict_join(link_ref) {
618                Ok(p) => p,
619                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
620            }
621        };
622
623        // Validate target is a directory (junctions are directory-only)
624        let meta = std::fs::metadata(self.path())?;
625        if !meta.is_dir() {
626            return Err(std::io::Error::new(
627                std::io::ErrorKind::Other,
628                "junction targets must be directories",
629            ));
630        }
631
632        // The junction crate does not handle verbatim `\\?\` prefix paths correctly.
633        // It creates broken junctions that return ERROR_INVALID_NAME (123) when accessed.
634        // Strip the prefix before passing to the junction crate.
635        // See: https://github.com/tesuji/junction/issues/30
636        let target_path = strip_verbatim_prefix(self.path());
637        let link_path = strip_verbatim_prefix(validated_link.path());
638
639        junction::create(target_path.as_ref(), link_path.as_ref())
640    }
641
642    /// SUMMARY:
643    /// Rename/move within the same boundary. Relative destinations are siblings; absolute are validated.
644    /// Parents are not created automatically.
645    pub fn strict_rename<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<()> {
646        let dest_ref = dest.as_ref();
647
648        // Compute destination under the parent directory for relative paths; allow absolute too
649        let dest_path = if dest_ref.is_absolute() {
650            match self.boundary.strict_join(dest_ref) {
651                Ok(p) => p,
652                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
653            }
654        } else {
655            let parent = match self.strictpath_parent() {
656                Ok(Some(p)) => p,
657                Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
658                    Ok(root) => root,
659                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
660                },
661                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
662            };
663            match parent.strict_join(dest_ref) {
664                Ok(p) => p,
665                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
666            }
667        };
668
669        std::fs::rename(self.path(), dest_path.path())
670    }
671
672    /// SUMMARY:
673    /// Copy within the same boundary. Relative destinations are siblings; absolute are validated.
674    /// Parents are not created automatically. Returns bytes copied.
675    pub fn strict_copy<P: AsRef<Path>>(&self, dest: P) -> std::io::Result<u64> {
676        let dest_ref = dest.as_ref();
677
678        // Compute destination under the parent directory for relative paths; allow absolute too
679        let dest_path = if dest_ref.is_absolute() {
680            match self.boundary.strict_join(dest_ref) {
681                Ok(p) => p,
682                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
683            }
684        } else {
685            let parent = match self.strictpath_parent() {
686                Ok(Some(p)) => p,
687                Ok(None) => match self.boundary.as_ref().clone().into_strictpath() {
688                    Ok(root) => root,
689                    Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
690                },
691                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
692            };
693            match parent.strict_join(dest_ref) {
694                Ok(p) => p,
695                Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
696            }
697        };
698
699        std::fs::copy(self.path(), dest_path.path())
700    }
701
702    /// SUMMARY:
703    /// Remove the file at this path.
704    pub fn remove_file(&self) -> std::io::Result<()> {
705        std::fs::remove_file(&self.path)
706    }
707
708    /// SUMMARY:
709    /// Remove the directory at this path.
710    pub fn remove_dir(&self) -> std::io::Result<()> {
711        std::fs::remove_dir(&self.path)
712    }
713
714    /// SUMMARY:
715    /// Recursively remove the directory and its contents.
716    pub fn remove_dir_all(&self) -> std::io::Result<()> {
717        std::fs::remove_dir_all(&self.path)
718    }
719}
720
721impl<Marker> fmt::Debug for StrictPath<Marker> {
722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723        f.debug_struct("StrictPath")
724            .field("path", &self.path)
725            .field("boundary", &self.boundary.path())
726            .field("marker", &std::any::type_name::<Marker>())
727            .finish()
728    }
729}
730
731#[cfg(windows)]
732fn create_windows_symlink(src: &Path, link: &Path) -> std::io::Result<()> {
733    use std::os::windows::fs::{symlink_dir, symlink_file};
734
735    match std::fs::metadata(src) {
736        Ok(metadata) => {
737            if metadata.is_dir() {
738                symlink_dir(src, link)
739            } else {
740                symlink_file(src, link)
741            }
742        }
743        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
744            match symlink_file(src, link) {
745                Ok(()) => Ok(()),
746                Err(file_err) => {
747                    if let Some(code) = file_err.raw_os_error() {
748                        const ERROR_DIRECTORY: i32 = 267; // target resolved as directory
749                        if code == ERROR_DIRECTORY {
750                            return symlink_dir(src, link);
751                        }
752                    }
753                    Err(file_err)
754                }
755            }
756        }
757        Err(err) => Err(err),
758    }
759}
760
761// Note: No separate helper for junction creation by design — keep surface minimal
762
763impl<Marker> PartialEq for StrictPath<Marker> {
764    #[inline]
765    fn eq(&self, other: &Self) -> bool {
766        self.path.as_ref() == other.path.as_ref()
767    }
768}
769
770impl<Marker> Eq for StrictPath<Marker> {}
771
772impl<Marker> Hash for StrictPath<Marker> {
773    #[inline]
774    fn hash<H: Hasher>(&self, state: &mut H) {
775        self.path.hash(state);
776    }
777}
778
779impl<Marker> PartialOrd for StrictPath<Marker> {
780    #[inline]
781    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
782        Some(self.cmp(other))
783    }
784}
785
786impl<Marker> Ord for StrictPath<Marker> {
787    #[inline]
788    fn cmp(&self, other: &Self) -> Ordering {
789        self.path.cmp(&other.path)
790    }
791}
792
793impl<T: AsRef<Path>, Marker> PartialEq<T> for StrictPath<Marker> {
794    fn eq(&self, other: &T) -> bool {
795        self.path.as_ref() == other.as_ref()
796    }
797}
798
799impl<T: AsRef<Path>, Marker> PartialOrd<T> for StrictPath<Marker> {
800    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
801        Some(self.path.as_ref().cmp(other.as_ref()))
802    }
803}
804
805#[cfg(feature = "virtual-path")]
806impl<Marker> PartialEq<crate::path::virtual_path::VirtualPath<Marker>> for StrictPath<Marker> {
807    #[inline]
808    fn eq(&self, other: &crate::path::virtual_path::VirtualPath<Marker>) -> bool {
809        self.path.as_ref() == other.interop_path()
810    }
811}