Skip to main content

rebecca_core/
error.rs

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}