1use std::fmt;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Error, Serialize)]
11pub enum ScanError {
12 #[error("Permission denied: {}: {}", crate::escape_filename(.path.as_os_str()), serialize_io_error_display(source))]
14 PermissionDenied {
15 #[serde(serialize_with = "crate::path_serde::serialize")]
16 path: PathBuf,
17 #[source]
18 #[serde(serialize_with = "serialize_io_error")]
19 source: std::io::Error,
20 },
21
22 #[error("Path not found: {}: {}", crate::escape_filename(.path.as_os_str()), serialize_io_error_display(source))]
24 NotFound {
25 #[serde(serialize_with = "crate::path_serde::serialize")]
26 path: PathBuf,
27 #[source]
28 #[serde(serialize_with = "serialize_io_error")]
29 source: std::io::Error,
30 },
31
32 #[error("I/O error at {}: {}", crate::escape_filename(.path.as_os_str()), serialize_io_error_display(source))]
34 Io {
35 #[serde(serialize_with = "crate::path_serde::serialize")]
36 path: PathBuf,
37 #[source]
38 #[serde(serialize_with = "serialize_io_error")]
39 source: std::io::Error,
40 },
41
42 #[error("Operation interrupted")]
44 Interrupted,
45
46 #[error("Too many errors ({count}), aborting")]
48 TooManyErrors { count: usize },
49
50 #[error("Scan entry limit exceeded ({limit} descendant entries)")]
52 EntryLimitExceeded { limit: u64 },
53
54 #[error("Invalid configuration: {}", crate::escape_terminal_text(message))]
56 InvalidConfig { message: String },
57
58 #[error("Root path is not a directory: {}", crate::escape_filename(.path.as_os_str()))]
60 NotADirectory {
61 #[serde(serialize_with = "crate::path_serde::serialize")]
62 path: PathBuf,
63 },
64
65 #[error("{}", crate::escape_terminal_text(message))]
67 Other { message: String },
68}
69
70fn serialize_io_error<S: serde::Serializer>(
71 error: &std::io::Error,
72 s: S,
73) -> Result<S::Ok, S::Error> {
74 s.serialize_str(&error.to_string())
75}
76
77fn serialize_io_error_display(error: &std::io::Error) -> String {
78 crate::escape_terminal_text(&error.to_string()).into_owned()
79}
80
81impl ScanError {
82 pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
84 let path = path.into();
85 match source.kind() {
86 std::io::ErrorKind::PermissionDenied => Self::PermissionDenied { path, source },
87 std::io::ErrorKind::NotFound => Self::NotFound { path, source },
88 _ => Self::Io { path, source },
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95pub enum WarningKind {
96 PermissionDenied,
98 BrokenSymlink,
100 ReadError,
102 MetadataError,
104 CrossFilesystem,
106 Omitted { count: u64 },
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
112pub struct ScanWarning {
113 #[serde(with = "crate::path_serde")]
115 pub path: PathBuf,
116 pub message: String,
118 pub kind: WarningKind,
120}
121
122impl fmt::Display for ScanWarning {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(f, "{}", crate::escape_terminal_text(&self.message))
125 }
126}
127
128impl ScanWarning {
129 pub fn represented_count(&self) -> u64 {
131 match self.kind {
132 WarningKind::Omitted { count } => count.max(1),
133 _ => 1,
134 }
135 }
136
137 pub fn new(path: impl Into<PathBuf>, kind: WarningKind, message: impl Into<String>) -> Self {
139 Self {
140 path: path.into(),
141 message: message.into(),
142 kind,
143 }
144 }
145
146 pub fn permission_denied(path: impl Into<PathBuf>) -> Self {
148 let path = path.into();
149 Self {
150 message: format!(
151 "Permission denied: {}",
152 crate::escape_filename(path.as_os_str())
153 ),
154 path,
155 kind: WarningKind::PermissionDenied,
156 }
157 }
158
159 pub fn broken_symlink(path: impl Into<PathBuf>, target: &str) -> Self {
161 let path = path.into();
162 Self {
163 message: format!(
164 "Broken symlink: {} -> {}",
165 crate::escape_filename(path.as_os_str()),
166 crate::escape_filename(std::ffi::OsStr::new(target))
167 ),
168 path,
169 kind: WarningKind::BrokenSymlink,
170 }
171 }
172
173 pub fn read_error(path: impl Into<PathBuf>, error: &std::io::Error) -> Self {
175 let path = path.into();
176 Self {
177 message: format!("Read error: {error}"),
178 path,
179 kind: WarningKind::ReadError,
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn displayed_errors_escape_paths_and_messages_but_preserve_sources() {
190 let path = PathBuf::from("line\n\u{1b}\\n");
191 for kind in [
192 std::io::ErrorKind::PermissionDenied,
193 std::io::ErrorKind::NotFound,
194 std::io::ErrorKind::Other,
195 ] {
196 let error = ScanError::io(&path, std::io::Error::new(kind, "bad\n\u{1b}source"));
197 let displayed = error.to_string();
198 let expected = if cfg!(windows) {
200 r"line\n\u{1b}\n"
201 } else {
202 r"line\n\u{1b}\\n"
203 };
204 assert!(displayed.contains(expected), "{displayed}");
205 assert!(displayed.contains(r"bad\n\u{1b}source"));
206 assert!(!displayed.contains(['\n', '\u{1b}']));
207 assert_eq!(
208 std::error::Error::source(&error).unwrap().to_string(),
209 "bad\n\u{1b}source"
210 );
211 }
212 for error in [
213 ScanError::NotADirectory { path },
214 ScanError::InvalidConfig {
215 message: "bad\nconfig".into(),
216 },
217 ScanError::Other {
218 message: "bad\nother".into(),
219 },
220 ] {
221 assert!(!error.to_string().contains(['\n', '\u{1b}']));
222 }
223 let warning = ScanWarning::new("root", WarningKind::ReadError, "bad\nwarning");
224 assert_eq!(warning.to_string(), r"bad\nwarning");
225 assert_eq!(warning.message, "bad\nwarning");
226 }
227
228 #[test]
229 fn test_scan_error_io() {
230 let err = ScanError::io(
231 "/test/path",
232 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
233 );
234 assert!(matches!(err, ScanError::PermissionDenied { .. }));
235 }
236
237 #[test]
238 fn test_scan_error_preserves_source() {
239 let err = ScanError::io(
240 "/test/path",
241 std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
242 );
243 match err {
244 ScanError::NotFound { source, .. } => {
245 assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
246 }
247 _ => panic!("Expected NotFound variant"),
248 }
249 }
250
251 #[test]
252 fn test_scan_warning_creation() {
253 let warning = ScanWarning::permission_denied("/test/path");
254 assert_eq!(warning.kind, WarningKind::PermissionDenied);
255 assert!(warning.message.contains("Permission denied"));
256 assert_eq!(format!("{warning}"), warning.message);
257 }
258}