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}