1use std::error::Error;
7use std::fmt;
8use std::path::{Path, PathBuf};
9
10const MAX_ERROR_PATH_LEN: usize = 256;
14
15pub(crate) fn truncate_path_display(path: &Path, max_len: usize) -> String {
17 let path_str = path.to_string_lossy();
18 let char_count = path_str.chars().count();
19 if char_count <= max_len {
20 return path_str.into_owned();
21 }
22 let keep = max_len.saturating_sub(5) / 2;
23 let start: String = path_str.chars().take(keep).collect();
24 let mut tail_chars: Vec<char> = path_str.chars().rev().take(keep).collect();
25 tail_chars.reverse();
26 let end: String = tail_chars.into_iter().collect();
27 format!("{start}...{end}")
28}
29
30#[derive(Debug)]
36#[must_use = "this error indicates a path validation failure — handle it to detect path traversal attacks or invalid boundaries"]
37pub enum StrictPathError {
38 InvalidRestriction {
40 restriction: PathBuf,
41 source: std::io::Error,
42 },
43 PathEscapesBoundary {
45 attempted_path: PathBuf,
46 restriction_boundary: PathBuf,
47 },
48 PathResolutionError {
50 path: PathBuf,
51 source: std::io::Error,
52 },
53}
54
55impl StrictPathError {
56 #[inline]
58 pub(crate) fn invalid_restriction(restriction: PathBuf, source: std::io::Error) -> Self {
59 Self::InvalidRestriction {
60 restriction,
61 source,
62 }
63 }
64 #[inline]
66 pub(crate) fn path_escapes_boundary(
67 attempted_path: PathBuf,
68 restriction_boundary: PathBuf,
69 ) -> Self {
70 Self::PathEscapesBoundary {
71 attempted_path,
72 restriction_boundary,
73 }
74 }
75 #[inline]
77 pub(crate) fn path_resolution_error(path: PathBuf, source: std::io::Error) -> Self {
78 Self::PathResolutionError { path, source }
79 }
80}
81
82impl fmt::Display for StrictPathError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 StrictPathError::InvalidRestriction {
86 restriction,
87 source,
88 } => {
89 write!(
90 f,
91 "Invalid PathBoundary: '{}' is not a valid boundary directory ({source}). \
92 Ensure the path points to an existing directory, or use try_new_create() to auto-create it.",
93 truncate_path_display(restriction, MAX_ERROR_PATH_LEN)
94 )
95 }
96 StrictPathError::PathEscapesBoundary {
97 attempted_path,
98 restriction_boundary,
99 } => {
100 let truncated_attempted = truncate_path_display(attempted_path, MAX_ERROR_PATH_LEN);
101 let truncated_boundary =
102 truncate_path_display(restriction_boundary, MAX_ERROR_PATH_LEN);
103 write!(
104 f,
105 "Path escapes boundary: '{truncated_attempted}' resolves outside restriction boundary \
106 '{truncated_boundary}' — this path traversal attempt was blocked. \
107 Validate untrusted input through strict_join()/virtual_join() which prevents escapes."
108 )
109 }
110 StrictPathError::PathResolutionError { path, source } => {
111 write!(
112 f,
113 "Cannot resolve path: '{}' ({source}). \
114 Ensure the target exists and is accessible, or create parent directories first.",
115 truncate_path_display(path, MAX_ERROR_PATH_LEN)
116 )
117 }
118 }
119 }
120}
121
122impl Error for StrictPathError {
123 fn source(&self) -> Option<&(dyn Error + 'static)> {
124 match self {
125 StrictPathError::InvalidRestriction { source, .. }
126 | StrictPathError::PathResolutionError { source, .. } => Some(source),
127 StrictPathError::PathEscapesBoundary { .. } => None,
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests;