1use std::error::Error;
3use std::fmt;
4use std::path::{Path, PathBuf};
5
6const MAX_ERROR_PATH_LEN: usize = 256;
7
8pub(crate) fn truncate_path_display(path: &Path, max_len: usize) -> String {
9 let path_str = path.to_string_lossy();
10 let char_count = path_str.chars().count();
11 if char_count <= max_len {
12 return path_str.into_owned();
13 }
14 let keep = max_len.saturating_sub(5) / 2;
15 let start: String = path_str.chars().take(keep).collect();
16 let mut tail_chars: Vec<char> = path_str.chars().rev().take(keep).collect();
17 tail_chars.reverse();
18 let end: String = tail_chars.into_iter().collect();
19 format!("{start}...{end}")
20}
21
22#[derive(Debug)]
24pub enum StrictPathError {
25 InvalidRestriction {
27 restriction: PathBuf,
28 source: std::io::Error,
29 },
30 PathEscapesBoundary {
32 attempted_path: PathBuf,
33 restriction_boundary: PathBuf,
34 },
35 PathResolutionError {
37 path: PathBuf,
38 source: std::io::Error,
39 },
40 #[cfg(windows)]
41 WindowsShortName {
43 component: std::ffi::OsString,
44 original: PathBuf,
45 checked_at: PathBuf,
46 },
47}
48
49impl StrictPathError {
50 #[inline]
51 pub(crate) fn invalid_restriction(restriction: PathBuf, source: std::io::Error) -> Self {
52 Self::InvalidRestriction {
53 restriction,
54 source,
55 }
56 }
57 #[inline]
58 pub(crate) fn path_escapes_boundary(
59 attempted_path: PathBuf,
60 restriction_boundary: PathBuf,
61 ) -> Self {
62 Self::PathEscapesBoundary {
63 attempted_path,
64 restriction_boundary,
65 }
66 }
67 #[inline]
68 pub(crate) fn path_resolution_error(path: PathBuf, source: std::io::Error) -> Self {
69 Self::PathResolutionError { path, source }
70 }
71 #[cfg(windows)]
72 #[inline]
73 pub(crate) fn windows_short_name(
74 component: std::ffi::OsString,
75 original: PathBuf,
76 checked_at: PathBuf,
77 ) -> Self {
78 Self::WindowsShortName {
79 component,
80 original,
81 checked_at,
82 }
83 }
84}
85
86impl fmt::Display for StrictPathError {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 StrictPathError::InvalidRestriction { restriction, .. } => {
90 write!(
91 f,
92 "Invalid PathBoundary directory: {}",
93 restriction.display()
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 '{truncated_attempted}' escapes path restriction boundary '{truncated_boundary}'"
106 )
107 }
108 StrictPathError::PathResolutionError { path, .. } => {
109 write!(f, "Cannot resolve path: {}", path.display())
110 }
111 #[cfg(windows)]
112 StrictPathError::WindowsShortName {
113 component,
114 original,
115 checked_at,
116 } => {
117 let original_trunc = truncate_path_display(original, MAX_ERROR_PATH_LEN);
118 let checked_trunc = truncate_path_display(checked_at, MAX_ERROR_PATH_LEN);
119 write!(
120 f,
121 "Windows 8.3 short filename component '{}' rejected at '{}' for original '{}'",
122 component.to_string_lossy(),
123 checked_trunc,
124 original_trunc
125 )
126 }
127 }
128 }
129}
130
131impl Error for StrictPathError {
132 fn source(&self) -> Option<&(dyn Error + 'static)> {
133 match self {
134 StrictPathError::InvalidRestriction { source, .. }
135 | StrictPathError::PathResolutionError { source, .. } => Some(source),
136 StrictPathError::PathEscapesBoundary { .. } => None,
137 #[cfg(windows)]
138 StrictPathError::WindowsShortName { .. } => None,
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests;