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;