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 root 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/// - `WindowsShortName` (windows): A segment resembles a DOS 8.3 short name.
49#[derive(Debug)]
50pub enum StrictPathError {
51 /// SUMMARY:
52 /// The PathBoundary root is invalid (missing, not a directory, or I/O error).
53 ///
54 /// FIELDS:
55 /// - `restriction` (`PathBuf`): The attempted root path.
56 /// - `source` (`std::io::Error`): Underlying OS error that explains why the
57 /// restriction is invalid.
58 InvalidRestriction {
59 restriction: PathBuf,
60 source: std::io::Error,
61 },
62 /// SUMMARY:
63 /// The attempted path would resolve outside the PathBoundary boundary.
64 ///
65 /// FIELDS:
66 /// - `attempted_path` (`PathBuf`): The user-supplied or composed candidate.
67 /// - `restriction_boundary` (`PathBuf`): The effective boundary root.
68 PathEscapesBoundary {
69 attempted_path: PathBuf,
70 restriction_boundary: PathBuf,
71 },
72 /// SUMMARY:
73 /// Canonicalization/resolution failed for the given path.
74 ///
75 /// FIELDS:
76 /// - `path` (`PathBuf`): The path whose resolution failed.
77 /// - `source` (`std::io::Error`): Underlying I/O cause.
78 PathResolutionError {
79 path: PathBuf,
80 source: std::io::Error,
81 },
82 #[cfg(windows)]
83 /// SUMMARY:
84 /// A component resembles a Windows 8.3 short name (potential ambiguity).
85 ///
86 /// FIELDS:
87 /// - `component` (`std::ffi::OsString`): The suspicious segment (e.g., `"PROGRA~1"`).
88 /// - `original` (`PathBuf`): The original input path.
89 /// - `checked_at` (`PathBuf`): Boundary or anchor context used during the check.
90 WindowsShortName {
91 component: std::ffi::OsString,
92 original: PathBuf,
93 checked_at: PathBuf,
94 },
95}
96
97impl StrictPathError {
98 // Internal helper: construct `InvalidRestriction`.
99 #[inline]
100 pub(crate) fn invalid_restriction(restriction: PathBuf, source: std::io::Error) -> Self {
101 Self::InvalidRestriction {
102 restriction,
103 source,
104 }
105 }
106 // Internal helper: construct `PathEscapesBoundary`.
107 #[inline]
108 pub(crate) fn path_escapes_boundary(
109 attempted_path: PathBuf,
110 restriction_boundary: PathBuf,
111 ) -> Self {
112 Self::PathEscapesBoundary {
113 attempted_path,
114 restriction_boundary,
115 }
116 }
117 // Internal helper: construct `PathResolutionError`.
118 #[inline]
119 pub(crate) fn path_resolution_error(path: PathBuf, source: std::io::Error) -> Self {
120 Self::PathResolutionError { path, source }
121 }
122 #[cfg(windows)]
123 // Internal helper: construct `WindowsShortName`.
124 #[inline]
125 pub(crate) fn windows_short_name(
126 component: std::ffi::OsString,
127 original: PathBuf,
128 checked_at: PathBuf,
129 ) -> Self {
130 Self::WindowsShortName {
131 component,
132 original,
133 checked_at,
134 }
135 }
136}
137
138impl fmt::Display for StrictPathError {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 StrictPathError::InvalidRestriction { restriction, .. } => {
142 write!(
143 f,
144 "Invalid PathBoundary directory: {}",
145 restriction.display()
146 )
147 }
148 StrictPathError::PathEscapesBoundary {
149 attempted_path,
150 restriction_boundary,
151 } => {
152 let truncated_attempted = truncate_path_display(attempted_path, MAX_ERROR_PATH_LEN);
153 let truncated_boundary =
154 truncate_path_display(restriction_boundary, MAX_ERROR_PATH_LEN);
155 write!(
156 f,
157 "Path '{truncated_attempted}' escapes path restriction boundary '{truncated_boundary}'"
158 )
159 }
160 StrictPathError::PathResolutionError { path, .. } => {
161 write!(f, "Cannot resolve path: {}", path.display())
162 }
163 #[cfg(windows)]
164 StrictPathError::WindowsShortName {
165 component,
166 original,
167 checked_at,
168 } => {
169 let original_trunc = truncate_path_display(original, MAX_ERROR_PATH_LEN);
170 let checked_trunc = truncate_path_display(checked_at, MAX_ERROR_PATH_LEN);
171 write!(
172 f,
173 "Windows 8.3 short filename component '{}' rejected at '{}' for original '{}'",
174 component.to_string_lossy(),
175 checked_trunc,
176 original_trunc
177 )
178 }
179 }
180 }
181}
182
183impl Error for StrictPathError {
184 fn source(&self) -> Option<&(dyn Error + 'static)> {
185 match self {
186 StrictPathError::InvalidRestriction { source, .. }
187 | StrictPathError::PathResolutionError { source, .. } => Some(source),
188 StrictPathError::PathEscapesBoundary { .. } => None,
189 #[cfg(windows)]
190 StrictPathError::WindowsShortName { .. } => None,
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests;