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