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