Skip to main content

scour_secrets/
error.rs

1//! Unified error types for the sanitization engine.
2//!
3//! All fallible operations in the crate return [`Result<T>`], which is an
4//! alias for `std::result::Result<T, SanitizeError>`.
5//!
6//! Errors are categorised by subsystem (`IoError`, `SecretsError`,
7//! `ArchiveError`, …) so callers can match on the variant to decide
8//! whether to retry, skip, or abort. The [`thiserror`] derive keeps
9//! display messages actionable and grep-friendly.
10
11use thiserror::Error;
12
13/// All errors that can occur within the sanitization engine.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum SanitizeError {
17    #[error("replacement store capacity exceeded: {current} mappings (limit: {limit})")]
18    CapacityExceeded { current: usize, limit: usize },
19
20    #[error("invalid seed length: expected 32 bytes, got {0}")]
21    InvalidSeedLength(usize),
22
23    #[error("I/O error: {0}")]
24    IoError(#[from] std::io::Error),
25
26    #[error("parse error ({format}): {message}")]
27    ParseError { format: String, message: String },
28
29    #[error("recursion depth exceeded: {0}")]
30    RecursionDepthExceeded(String),
31
32    #[error("input too large: {size} bytes (limit: {limit})")]
33    InputTooLarge { size: usize, limit: usize },
34
35    #[error("pattern compilation error: {0}")]
36    PatternCompileError(String),
37
38    #[error("invalid configuration: {0}")]
39    InvalidConfig(String),
40
41    #[error("secrets: empty password")]
42    SecretsEmptyPassword,
43
44    #[error("secrets: encrypted file too short (corrupt or truncated)")]
45    SecretsTooShort,
46
47    #[error("secrets: not a recognized encrypted secrets file (bad magic or unsupported version)")]
48    SecretsUnrecognizedFormat,
49
50    #[error("secrets: decryption failed — wrong password or corrupted file")]
51    SecretsDecryptFailed,
52
53    #[error("secrets: cipher error: {0}")]
54    SecretsCipherError(String),
55
56    #[error("secrets: {format} error: {message}")]
57    SecretsFormatError { format: String, message: String },
58
59    #[error("secrets: invalid UTF-8: {0}")]
60    SecretsInvalidUtf8(String),
61
62    #[error("secrets: no password provided — file appears encrypted but --encrypted-secrets was not specified")]
63    SecretsPasswordRequired,
64
65    #[error("archive error: {0}")]
66    ArchiveError(String),
67}
68
69pub type Result<T> = std::result::Result<T, SanitizeError>;
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn from_io_error_wraps_message() {
77        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
78        let err = SanitizeError::from(io_err);
79        assert!(matches!(err, SanitizeError::IoError(_)));
80        assert!(err.to_string().contains("file not found"));
81    }
82
83    #[test]
84    fn io_error_exposes_kind() {
85        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
86        if let SanitizeError::IoError(inner) = SanitizeError::from(io_err) {
87            assert_eq!(inner.kind(), std::io::ErrorKind::PermissionDenied);
88        } else {
89            panic!("expected IoError");
90        }
91    }
92
93    #[test]
94    fn display_variants_are_actionable() {
95        assert!(SanitizeError::CapacityExceeded {
96            current: 5,
97            limit: 3
98        }
99        .to_string()
100        .contains('5'));
101        assert!(SanitizeError::InputTooLarge {
102            size: 100,
103            limit: 50
104        }
105        .to_string()
106        .contains("100"));
107        assert!(SanitizeError::RecursionDepthExceeded("too deep".into())
108            .to_string()
109            .contains("too deep"));
110        assert!(SanitizeError::SecretsEmptyPassword
111            .to_string()
112            .contains("empty"));
113        assert!(SanitizeError::SecretsDecryptFailed
114            .to_string()
115            .contains("wrong password"));
116    }
117}