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}