Skip to main content

cargocrypt/
error.rs

1//! Error types for CargoCrypt
2//!
3//! This module provides comprehensive error handling with actionable error messages
4//! that help developers understand and fix issues quickly.
5
6// use std::fmt; // Currently unused
7
8/// Error severity levels
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ErrorSeverity {
11    Info,
12    Warning,
13    Critical,
14}
15
16/// Result type alias for CargoCrypt operations
17pub type CryptoResult<T> = Result<T, CargoCryptError>;
18
19/// Main error type for CargoCrypt operations
20#[derive(Debug, thiserror::Error)]
21pub enum CargoCryptError {
22    /// I/O errors (file operations, network, etc.)
23    #[error("File operation failed: {message}")]
24    Io {
25        message: String,
26        #[source]
27        source: std::io::Error,
28    },
29
30    /// Cryptographic operation errors
31    #[error("Cryptographic operation failed: {message}")]
32    Crypto {
33        message: String,
34        kind: CryptoErrorKind,
35    },
36
37    /// Configuration errors
38    #[error("Configuration error: {message}")]
39    Config {
40        message: String,
41        suggestion: Option<String>,
42    },
43
44    /// Project structure errors
45    #[error("Project structure error: {message}")]
46    Project {
47        message: String,
48        suggestion: Option<String>,
49    },
50
51    /// Authentication/Authorization errors
52    #[error("Authentication failed: {message}")]
53    Auth {
54        message: String,
55        retry_suggestion: Option<String>,
56    },
57
58    /// Key management errors
59    #[error("Key management error: {message}")]
60    KeyManagement {
61        message: String,
62        recovery_suggestion: Option<String>,
63    },
64
65    /// Serialization/Deserialization errors
66    #[error("Serialization error: {message}")]
67    Serialization {
68        message: String,
69        #[source]
70        source: Box<dyn std::error::Error + Send + Sync>,
71    },
72
73    /// Network-related errors
74    #[error("Network error: {message}")]
75    Network {
76        message: String,
77        #[source]
78        source: Box<dyn std::error::Error + Send + Sync>,
79    },
80
81    /// Git operations errors
82    #[error("Git operation failed: {message}")]
83    Git {
84        message: String,
85        #[source]
86        source: Option<git2::Error>,
87    },
88
89    /// Validation errors
90    #[error("Validation failed: {message}")]
91    Validation {
92        message: String,
93        errors: Vec<String>,
94        warnings: Vec<String>,
95    },
96}
97
98/// Specific kinds of cryptographic errors
99#[derive(Debug, Clone, PartialEq)]
100pub enum CryptoErrorKind {
101    /// Key derivation failed
102    KeyDerivation,
103    /// Encryption failed
104    Encryption,
105    /// Decryption failed
106    Decryption,
107    /// Invalid key format or length
108    InvalidKey,
109    /// Invalid nonce or IV
110    InvalidNonce,
111    /// Authentication tag verification failed
112    AuthenticationFailed,
113    /// Unsupported algorithm
114    UnsupportedAlgorithm,
115    /// Random number generation failed
116    RandomGenerationFailed,
117}
118
119/// Commonly used error constructors for better ergonomics
120impl CargoCryptError {
121    /// Get the severity level of this error
122    pub fn severity(&self) -> ErrorSeverity {
123        match self {
124            CargoCryptError::Crypto { kind, .. } => match kind {
125                CryptoErrorKind::AuthenticationFailed |
126                CryptoErrorKind::Decryption |
127                CryptoErrorKind::InvalidKey => ErrorSeverity::Critical,
128                _ => ErrorSeverity::Warning,
129            },
130            CargoCryptError::Validation { .. } => ErrorSeverity::Warning,
131            CargoCryptError::Auth { .. } => ErrorSeverity::Critical,
132            CargoCryptError::KeyManagement { .. } => ErrorSeverity::Critical,
133            CargoCryptError::Network { .. } => ErrorSeverity::Warning,
134            CargoCryptError::Git { .. } => ErrorSeverity::Warning,
135            CargoCryptError::Io { .. } => ErrorSeverity::Warning,
136            CargoCryptError::Config { .. } => ErrorSeverity::Info,
137            CargoCryptError::Project { .. } => ErrorSeverity::Info,
138            CargoCryptError::Serialization { .. } => ErrorSeverity::Warning,
139        }
140    }
141    /// Create a project not found error with helpful suggestion
142    pub fn project_not_found() -> Self {
143        Self::Project {
144            message: "Could not find Cargo.toml in current directory or any parent directories".to_string(),
145            suggestion: Some("Run this command from within a Rust project directory, or use 'cargo new' to create a new project".to_string()),
146        }
147    }
148
149    /// Create a configuration file not found error
150    pub fn config_not_found() -> Self {
151        Self::Config {
152            message: "CargoCrypt configuration file not found".to_string(),
153            suggestion: Some("Run 'cargo crypt init' to create a new configuration".to_string()),
154        }
155    }
156
157    /// Create an invalid password error
158    pub fn invalid_password() -> Self {
159        Self::Auth {
160            message: "Password verification failed".to_string(),
161            retry_suggestion: Some("Please check your password and try again".to_string()),
162        }
163    }
164
165    /// Create a file not found error with context
166    pub fn file_not_found(path: &std::path::Path) -> Self {
167        Self::Io {
168            message: format!("File not found: {}", path.display()),
169            source: std::io::Error::new(
170                std::io::ErrorKind::NotFound,
171                format!("File '{}' does not exist", path.display()),
172            ),
173        }
174    }
175
176    /// Create a decryption failure error
177    pub fn decryption_failed(details: &str) -> Self {
178        Self::Crypto {
179            message: format!("Decryption failed: {}", details),
180            kind: CryptoErrorKind::Decryption,
181        }
182    }
183
184    /// Create an encryption failure error
185    pub fn encryption_failed(details: &str) -> Self {
186        Self::Crypto {
187            message: format!("Encryption failed: {}", details),
188            kind: CryptoErrorKind::Encryption,
189        }
190    }
191
192    /// Create a key derivation failure error
193    pub fn key_derivation_failed(details: &str) -> Self {
194        Self::Crypto {
195            message: format!("Key derivation failed: {}", details),
196            kind: CryptoErrorKind::KeyDerivation,
197        }
198    }
199
200    /// Create an invalid key error
201    pub fn invalid_key(details: &str) -> Self {
202        Self::Crypto {
203            message: format!("Invalid key: {}", details),
204            kind: CryptoErrorKind::InvalidKey,
205        }
206    }
207
208    /// Create an authentication failure error
209    pub fn authentication_failed() -> Self {
210        Self::Crypto {
211            message: "Authentication tag verification failed - data may be corrupted or tampered with".to_string(),
212            kind: CryptoErrorKind::AuthenticationFailed,
213        }
214    }
215
216    /// Create a random generation failure error
217    pub fn random_generation_failed() -> Self {
218        Self::Crypto {
219            message: "Failed to generate cryptographically secure random data".to_string(),
220            kind: CryptoErrorKind::RandomGenerationFailed,
221        }
222    }
223
224    /// Create a detection error
225    pub fn detection_error(message: &str) -> Self {
226        Self::Config {
227            message: format!("Detection error: {}", message),
228            suggestion: Some("Check detection configuration and patterns".to_string()),
229        }
230    }
231
232    /// Get the error kind if this is a crypto error
233    pub fn crypto_kind(&self) -> Option<&CryptoErrorKind> {
234        match self {
235            CargoCryptError::Crypto { kind, .. } => Some(kind),
236            _ => None,
237        }
238    }
239
240    /// Check if this error is recoverable (user can retry)
241    pub fn is_recoverable(&self) -> bool {
242        match self {
243            CargoCryptError::Auth { .. } => true,
244            CargoCryptError::Network { .. } => true,
245            CargoCryptError::Io { source, .. } => matches!(
246                source.kind(),
247                std::io::ErrorKind::NotFound
248                    | std::io::ErrorKind::PermissionDenied
249                    | std::io::ErrorKind::ConnectionRefused
250                    | std::io::ErrorKind::TimedOut
251            ),
252            CargoCryptError::Crypto { kind, .. } => matches!(
253                kind,
254                CryptoErrorKind::RandomGenerationFailed
255            ),
256            _ => false,
257        }
258    }
259
260    /// Get a user-friendly suggestion for resolving this error
261    pub fn suggestion(&self) -> Option<&str> {
262        match self {
263            CargoCryptError::Config { suggestion, .. } => suggestion.as_deref(),
264            CargoCryptError::Project { suggestion, .. } => suggestion.as_deref(),
265            CargoCryptError::Auth { retry_suggestion, .. } => retry_suggestion.as_deref(),
266            CargoCryptError::KeyManagement { recovery_suggestion, .. } => recovery_suggestion.as_deref(),
267            _ => None,
268        }
269    }
270}
271
272/// Convert from standard I/O errors
273impl From<std::io::Error> for CargoCryptError {
274    fn from(error: std::io::Error) -> Self {
275        Self::Io {
276            message: error.to_string(),
277            source: error,
278        }
279    }
280}
281
282/// Convert from CryptoError
283impl From<crate::crypto::CryptoError> for CargoCryptError {
284    fn from(error: crate::crypto::CryptoError) -> Self {
285        use crate::crypto::CryptoError;
286        
287        let kind = match &error {
288            CryptoError::KeyDerivation { .. } => CryptoErrorKind::KeyDerivation,
289            CryptoError::Encryption { .. } => CryptoErrorKind::Encryption,
290            CryptoError::Decryption { .. } => CryptoErrorKind::Decryption,
291            CryptoError::AuthenticationFailed => CryptoErrorKind::AuthenticationFailed,
292            CryptoError::InvalidKey { .. } => CryptoErrorKind::InvalidKey,
293            CryptoError::InvalidNonce { .. } => CryptoErrorKind::InvalidNonce,
294            CryptoError::RandomGeneration { .. } => CryptoErrorKind::RandomGenerationFailed,
295            _ => CryptoErrorKind::Encryption, // Default fallback
296        };
297        
298        Self::Crypto {
299            message: error.to_string(),
300            kind,
301        }
302    }
303}
304
305/// Convert from serde JSON errors
306impl From<serde_json::Error> for CargoCryptError {
307    fn from(error: serde_json::Error) -> Self {
308        Self::Serialization {
309            message: format!("JSON serialization failed: {}", error),
310            source: Box::new(error),
311        }
312    }
313}
314
315/// Convert from TOML errors
316impl From<toml::de::Error> for CargoCryptError {
317    fn from(error: toml::de::Error) -> Self {
318        Self::Serialization {
319            message: format!("TOML parsing failed: {}", error),
320            source: Box::new(error),
321        }
322    }
323}
324
325/// Convert from reqwest errors
326impl From<reqwest::Error> for CargoCryptError {
327    fn from(error: reqwest::Error) -> Self {
328        Self::Network {
329            message: format!("HTTP request failed: {}", error),
330            source: Box::new(error),
331        }
332    }
333}
334
335/// Convert from git2 errors
336impl From<git2::Error> for CargoCryptError {
337    fn from(error: git2::Error) -> Self {
338        Self::Git {
339            message: format!("Git operation failed: {}", error.message()),
340            source: Some(error),
341        }
342    }
343}
344
345impl From<crate::git::GitError> for CargoCryptError {
346    fn from(error: crate::git::GitError) -> Self {
347        Self::Git {
348            message: format!("Git integration failed: {}", error),
349            source: None,
350        }
351    }
352}
353
354/// Error kind enumeration for programmatic error handling
355#[derive(Debug, Clone, PartialEq)]
356pub enum ErrorKind {
357    /// Configuration-related errors
358    Config,
359    /// File system operation errors
360    Io,
361    /// Cryptographic operation errors
362    Crypto,
363    /// Network operation errors
364    Network,
365    /// Authentication/authorization errors
366    Auth,
367    /// Git operation errors
368    Git,
369    /// Project structure errors
370    Project,
371    /// Key management errors
372    KeyManagement,
373    /// Serialization errors
374    Serialization,
375}
376
377impl CargoCryptError {
378    /// Get the general error kind
379    pub fn kind(&self) -> ErrorKind {
380        match self {
381            CargoCryptError::Config { .. } => ErrorKind::Config,
382            CargoCryptError::Io { .. } => ErrorKind::Io,
383            CargoCryptError::Crypto { .. } => ErrorKind::Crypto,
384            CargoCryptError::Network { .. } => ErrorKind::Network,
385            CargoCryptError::Auth { .. } => ErrorKind::Auth,
386            CargoCryptError::Git { .. } => ErrorKind::Git,
387            CargoCryptError::Project { .. } => ErrorKind::Project,
388            CargoCryptError::KeyManagement { .. } => ErrorKind::KeyManagement,
389            CargoCryptError::Serialization { .. } => ErrorKind::Serialization,
390            CargoCryptError::Validation { .. } => ErrorKind::Config,
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn test_error_constructors() {
401        let err = CargoCryptError::project_not_found();
402        assert!(matches!(err.kind(), ErrorKind::Project));
403        assert!(err.suggestion().is_some());
404
405        let err = CargoCryptError::invalid_password();
406        assert!(matches!(err.kind(), ErrorKind::Auth));
407        assert!(err.is_recoverable());
408    }
409
410    #[test]
411    fn test_crypto_error_kinds() {
412        let err = CargoCryptError::decryption_failed("test");
413        assert_eq!(err.crypto_kind(), Some(&CryptoErrorKind::Decryption));
414
415        let err = CargoCryptError::encryption_failed("test");
416        assert_eq!(err.crypto_kind(), Some(&CryptoErrorKind::Encryption));
417    }
418
419    #[test]
420    fn test_error_conversions() {
421        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
422        let crypto_err: CargoCryptError = io_err.into();
423        assert!(matches!(crypto_err.kind(), ErrorKind::Io));
424    }
425
426    #[test]
427    fn test_recoverable_errors() {
428        let auth_err = CargoCryptError::invalid_password();
429        assert!(auth_err.is_recoverable());
430
431        let config_err = CargoCryptError::config_not_found();
432        assert!(!config_err.is_recoverable());
433    }
434}