strict_path/error/mod.rs
1//! SUMMARY:
2//! Define error types and helpers for boundary creation and strict/virtual path validation.
3//!
4//! OVERVIEW:
5//! This module exposes the crate-wide error enum `StrictPathError`, which captures
6//! boundary creation failures, path resolution errors, boundary escape attempts,
7//! and (on Windows) 8.3 short-name rejections. These errors are surfaced by
8//! public constructors and join operations throughout the crate.
9//!
10//! STYLE:
11//! All items follow the standardized doc format with explicit sections to keep
12//! behavior unambiguous for both humans and LLMs.
13// Content copied from original src/error/mod.rs
14use std::error::Error;
15use std::fmt;
16use std::path::{Path, PathBuf};
17
18const MAX_ERROR_PATH_LEN: usize = 256;
19
20// Internal helper: render error-friendly path display (truncate long values).
21pub(crate) fn truncate_path_display(path: &Path, max_len: usize) -> String {
22 let path_str = path.to_string_lossy();
23 let char_count = path_str.chars().count();
24 if char_count <= max_len {
25 return path_str.into_owned();
26 }
27 let keep = max_len.saturating_sub(5) / 2;
28 let start: String = path_str.chars().take(keep).collect();
29 let mut tail_chars: Vec<char> = path_str.chars().rev().take(keep).collect();
30 tail_chars.reverse();
31 let end: String = tail_chars.into_iter().collect();
32 format!("{start}...{end}")
33}
34
35/// SUMMARY:
36/// Represent errors produced by boundary creation and strict/virtual path validation.
37///
38/// DETAILS:
39/// This error type is returned by operations that construct `PathBoundary`
40///`VirtualRoot` or that compose `StrictPath`/`VirtualPath` via joins. Each
41/// variant carries enough context for actionable diagnostics while avoiding
42/// leaking unbounded path data into messages (we truncate long displays).
43///
44/// VARIANTS:
45/// - `InvalidRestriction`: The boundary directory is missing, not a directory, or failed I/O checks.
46/// - `PathEscapesBoundary`: A candidate path would resolve outside the boundary.
47/// - `PathResolutionError`: Canonicalization or resolution failed (I/O error).
48#[derive(Debug)]
49pub enum StrictPathError {
50 /// SUMMARY:
51 /// The boundary directory is invalid (missing, not a directory, or I/O error).
52 ///
53 /// FIELDS:
54 /// - `restriction` (`PathBuf`): The attempted boundary path.
55 /// - `source` (`std::io::Error`): Underlying OS error that explains why the
56 /// restriction is invalid.
57 InvalidRestriction {
58 restriction: PathBuf,
59 source: std::io::Error,
60 },
61 /// SUMMARY:
62 /// The attempted path would resolve outside the PathBoundary boundary.
63 ///
64 /// FIELDS:
65 /// - `attempted_path` (`PathBuf`): The user-supplied or composed candidate.
66 /// - `restriction_boundary` (`PathBuf`): The effective boundary root.
67 PathEscapesBoundary {
68 attempted_path: PathBuf,
69 restriction_boundary: PathBuf,
70 },
71 /// SUMMARY:
72 /// Canonicalization/resolution failed for the given path.
73 ///
74 /// FIELDS:
75 /// - `path` (`PathBuf`): The path whose resolution failed.
76 /// - `source` (`std::io::Error`): Underlying I/O cause.
77 PathResolutionError {
78 path: PathBuf,
79 source: std::io::Error,
80 },
81}
82
83impl StrictPathError {
84 // Internal helper: construct `InvalidRestriction`.
85 #[inline]
86 pub(crate) fn invalid_restriction(restriction: PathBuf, source: std::io::Error) -> Self {
87 Self::InvalidRestriction {
88 restriction,
89 source,
90 }
91 }
92 // Internal helper: construct `PathEscapesBoundary`.
93 #[inline]
94 pub(crate) fn path_escapes_boundary(
95 attempted_path: PathBuf,
96 restriction_boundary: PathBuf,
97 ) -> Self {
98 Self::PathEscapesBoundary {
99 attempted_path,
100 restriction_boundary,
101 }
102 }
103 // Internal helper: construct `PathResolutionError`.
104 #[inline]
105 pub(crate) fn path_resolution_error(path: PathBuf, source: std::io::Error) -> Self {
106 Self::PathResolutionError { path, source }
107 }
108}
109
110impl fmt::Display for StrictPathError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 StrictPathError::InvalidRestriction { restriction, .. } => {
114 write!(
115 f,
116 "Invalid PathBoundary directory: {}",
117 restriction.display()
118 )
119 }
120 StrictPathError::PathEscapesBoundary {
121 attempted_path,
122 restriction_boundary,
123 } => {
124 let truncated_attempted = truncate_path_display(attempted_path, MAX_ERROR_PATH_LEN);
125 let truncated_boundary =
126 truncate_path_display(restriction_boundary, MAX_ERROR_PATH_LEN);
127 write!(
128 f,
129 "Path '{truncated_attempted}' escapes path restriction boundary '{truncated_boundary}'"
130 )
131 }
132 StrictPathError::PathResolutionError { path, .. } => {
133 write!(f, "Cannot resolve path: {}", path.display())
134 }
135 }
136 }
137}
138
139impl Error for StrictPathError {
140 fn source(&self) -> Option<&(dyn Error + 'static)> {
141 match self {
142 StrictPathError::InvalidRestriction { source, .. }
143 | StrictPathError::PathResolutionError { source, .. } => Some(source),
144 StrictPathError::PathEscapesBoundary { .. } => None,
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests;