1use std::path::{Path, PathBuf};
2
3use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, RebeccaError>;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ScanFailureKind {
9 NotFound,
10 PermissionDenied,
11 InvalidInput,
12 DirectoryLoop,
13 MetadataUnavailable,
14 DirectoryTraversal,
15}
16
17impl ScanFailureKind {
18 fn label(self) -> &'static str {
19 match self {
20 Self::NotFound => "not-found",
21 Self::PermissionDenied => "permission-denied",
22 Self::InvalidInput => "invalid-input",
23 Self::DirectoryLoop => "directory-loop",
24 Self::MetadataUnavailable => "metadata-unavailable",
25 Self::DirectoryTraversal => "directory-traversal",
26 }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ScanFailurePhase {
32 RootMetadata,
33 DirectoryWalk,
34 EntryMetadata,
35}
36
37impl ScanFailurePhase {
38 fn label(self) -> &'static str {
39 match self {
40 Self::RootMetadata => "root-metadata",
41 Self::DirectoryWalk => "directory-walk",
42 Self::EntryMetadata => "entry-metadata",
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ScanFailure {
49 pub kind: ScanFailureKind,
50 pub phase: ScanFailurePhase,
51 pub path: PathBuf,
52 pub message: String,
53}
54
55impl ScanFailure {
56 pub fn from_io(path: &Path, phase: ScanFailurePhase, err: &std::io::Error) -> Self {
57 Self {
58 kind: classify_io_error(err),
59 phase,
60 path: path.to_path_buf(),
61 message: err.to_string(),
62 }
63 }
64
65 pub fn directory_walk(path: &Path, err: &ignore::Error) -> Self {
66 Self::from_ignore(path, ScanFailurePhase::DirectoryWalk, err)
67 }
68
69 pub fn from_ignore(path: &Path, phase: ScanFailurePhase, err: &ignore::Error) -> Self {
70 let kind = classify_ignore_error(err).unwrap_or(match phase {
71 ScanFailurePhase::DirectoryWalk => ScanFailureKind::DirectoryTraversal,
72 ScanFailurePhase::EntryMetadata | ScanFailurePhase::RootMetadata => {
73 ScanFailureKind::MetadataUnavailable
74 }
75 });
76
77 Self {
78 kind,
79 phase,
80 path: ignore_error_path(err).unwrap_or(path).to_path_buf(),
81 message: err.to_string(),
82 }
83 }
84}
85
86impl std::fmt::Display for ScanFailure {
87 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(
89 formatter,
90 "{} during {} at {}: {}",
91 self.kind.label(),
92 self.phase.label(),
93 self.path.display(),
94 self.message
95 )
96 }
97}
98
99fn classify_io_error(err: &std::io::Error) -> ScanFailureKind {
100 match err.kind() {
101 std::io::ErrorKind::NotFound => ScanFailureKind::NotFound,
102 std::io::ErrorKind::PermissionDenied => ScanFailureKind::PermissionDenied,
103 std::io::ErrorKind::InvalidInput => ScanFailureKind::InvalidInput,
104 _ => ScanFailureKind::MetadataUnavailable,
105 }
106}
107
108fn classify_ignore_error(err: &ignore::Error) -> Option<ScanFailureKind> {
109 match err {
110 ignore::Error::Partial(errors) if errors.len() == 1 => {
111 errors.first().and_then(classify_ignore_error)
112 }
113 ignore::Error::WithLineNumber { err, .. }
114 | ignore::Error::WithPath { err, .. }
115 | ignore::Error::WithDepth { err, .. } => classify_ignore_error(err),
116 ignore::Error::Loop { .. } => Some(ScanFailureKind::DirectoryLoop),
117 ignore::Error::Io(err) => Some(classify_io_error(err)),
118 _ => None,
119 }
120}
121
122fn ignore_error_path(err: &ignore::Error) -> Option<&Path> {
123 match err {
124 ignore::Error::Partial(errors) if errors.len() == 1 => {
125 errors.first().and_then(ignore_error_path)
126 }
127 ignore::Error::WithLineNumber { err, .. } | ignore::Error::WithDepth { err, .. } => {
128 ignore_error_path(err)
129 }
130 ignore::Error::WithPath { path, .. } => Some(path),
131 ignore::Error::Loop { child, .. } => Some(child),
132 _ => None,
133 }
134}
135
136#[derive(Debug, Error)]
137pub enum RebeccaError {
138 #[error("i/o error: {0}")]
139 Io(#[from] std::io::Error),
140
141 #[error("json error: {0}")]
142 Json(#[from] serde_json::Error),
143
144 #[error("config read failed at {path}: {message}")]
145 ConfigRead { path: PathBuf, message: String },
146
147 #[error("config parse failed at {path}: {message}")]
148 ConfigParse { path: PathBuf, message: String },
149
150 #[error("could not locate the current user's standard directories")]
151 UserDirsUnavailable,
152
153 #[error("invalid rule id: {0}")]
154 InvalidRuleId(String),
155
156 #[error("invalid category: {0}")]
157 InvalidCategory(String),
158
159 #[error("invalid project artifact selector: {0}")]
160 InvalidProjectArtifactSelector(String),
161
162 #[error("invalid catalog selector: {0}")]
163 InvalidCatalogSelector(String),
164
165 #[error("invalid rule catalog: {0}")]
166 RuleCatalogInvalid(String),
167
168 #[error("invalid safety catalog: {0}")]
169 SafetyCatalogInvalid(String),
170
171 #[error("path template expansion failed: {0}")]
172 PathExpansionFailed(String),
173
174 #[error("application discovery failed: {0}")]
175 ApplicationDiscoveryFailed(String),
176
177 #[error("cleanup target was blocked by safety policy: {0}")]
178 SafetyBlocked(String),
179
180 #[error("scan failed: {0}")]
181 ScanFailed(ScanFailure),
182
183 #[error("scan cache is unavailable: {0}")]
184 ScanCacheUnavailable(String),
185
186 #[error("operation cancelled: {0}")]
187 OperationCancelled(String),
188
189 #[error("platform feature is not available: {0}")]
190 PlatformUnavailable(String),
191
192 #[error("history is unavailable: {0}")]
193 HistoryUnavailable(String),
194
195 #[error("history record was corrupted: {0}")]
196 HistoryCorrupted(String),
197
198 #[error("cleanup execution failed: {0}")]
199 ExecutionFailed(String),
200}
201
202#[cfg(test)]
203mod tests {
204 use super::{ScanFailure, ScanFailureKind, ScanFailurePhase};
205
206 #[test]
207 fn ignore_error_failure_keeps_nested_path_and_io_kind() {
208 let root = std::path::Path::new("root");
209 let nested = std::path::PathBuf::from("root/cache");
210 let err = ignore::Error::WithPath {
211 path: nested.clone(),
212 err: Box::new(ignore::Error::Io(std::io::Error::new(
213 std::io::ErrorKind::PermissionDenied,
214 "denied",
215 ))),
216 };
217
218 let failure = ScanFailure::from_ignore(root, ScanFailurePhase::DirectoryWalk, &err);
219
220 assert_eq!(failure.kind, ScanFailureKind::PermissionDenied);
221 assert_eq!(failure.phase, ScanFailurePhase::DirectoryWalk);
222 assert_eq!(failure.path, nested);
223 }
224}