promocrypt_core/
error.rs

1//! Error types for promocrypt-core.
2//!
3//! This module defines all error types used throughout the library,
4//! including error codes for FFI compatibility and detailed validation results.
5
6use thiserror::Error;
7
8/// Error codes aligned with all promocrypt components.
9/// These are used for FFI compatibility and cross-component consistency.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[repr(u8)]
12pub enum ErrorCode {
13    /// Operation completed successfully.
14    Success = 0,
15    /// Binary file was not found at the specified path.
16    FileNotFound = 1,
17    /// File format is invalid or corrupted.
18    InvalidFormat = 2,
19    /// Decryption failed (wrong key or corrupted data).
20    DecryptionFailed = 3,
21    /// File is bound to a different machine.
22    MachineMismatch = 4,
23    /// Operation requires a secret key but none was provided.
24    SecretRequired = 5,
25    /// Promotional code validation failed.
26    InvalidCode = 6,
27    /// Counter file is locked by another process.
28    CounterLocked = 7,
29    /// Counter has reached its maximum value.
30    CounterOverflow = 8,
31    /// An I/O error occurred.
32    IoError = 9,
33    /// The provided alphabet is invalid.
34    InvalidAlphabet = 10,
35    /// An invalid argument was provided.
36    InvalidArgument = 11,
37}
38
39/// Main error type for all promocrypt operations.
40#[derive(Error, Debug)]
41pub enum PromocryptError {
42    /// Binary file was not found at the specified path.
43    #[error("Binary file not found: {0}")]
44    FileNotFound(String),
45
46    /// Binary file format is invalid or unrecognized.
47    #[error("Invalid binary file format")]
48    InvalidFileFormat,
49
50    /// Binary file format is invalid with specific details.
51    #[error("Invalid binary file format: {0}")]
52    InvalidFileFormatDetails(String),
53
54    /// Binary file version is not supported.
55    #[error("File version {0} not supported (expected 2)")]
56    UnsupportedVersion(u8),
57
58    /// Header checksum verification failed.
59    #[error("Header checksum mismatch")]
60    HeaderChecksumMismatch,
61
62    /// Decryption failed due to invalid key or corrupted data.
63    #[error("Decryption failed: invalid key or corrupted file")]
64    DecryptionFailed,
65
66    /// Encryption operation failed.
67    #[error("Encryption failed: {0}")]
68    EncryptionFailed(String),
69
70    /// Machine ID could not be determined.
71    #[error("Machine ID not available: {0}")]
72    MachineIdUnavailable(String),
73
74    /// File is bound to a different machine.
75    #[error("File not bound to this machine")]
76    MachineMismatch,
77
78    /// Operation requires a secret key but none was provided.
79    #[error("Operation requires secret key")]
80    SecretRequired,
81
82    /// Code has incorrect length.
83    #[error("Invalid code length: expected {expected}, got {actual}")]
84    InvalidLength {
85        /// Expected code length.
86        expected: usize,
87        /// Actual code length.
88        actual: usize,
89    },
90
91    /// Code contains a character not in the alphabet.
92    #[error("Invalid character in code: '{0}'")]
93    InvalidCharacter(char),
94
95    /// Check digit verification failed.
96    #[error("Invalid check digit")]
97    InvalidCheckDigit,
98
99    /// Counter file is locked by another process.
100    #[error("Counter file locked by another process")]
101    CounterLocked,
102
103    /// Counter has reached its maximum value.
104    #[error("Counter overflow")]
105    CounterOverflow,
106
107    /// An I/O error occurred.
108    #[error("IO error: {0}")]
109    Io(#[from] std::io::Error),
110
111    /// The provided alphabet is invalid.
112    #[error("Invalid alphabet: {0}")]
113    InvalidAlphabet(String),
114
115    /// An invalid argument was provided.
116    #[error("Invalid argument: {0}")]
117    InvalidArgument(String),
118
119    /// JSON serialization or deserialization error.
120    #[error("JSON error: {0}")]
121    Json(#[from] serde_json::Error),
122}
123
124impl PromocryptError {
125    /// Get the numeric error code for FFI and cross-component consistency.
126    pub fn code(&self) -> ErrorCode {
127        match self {
128            Self::FileNotFound(_) => ErrorCode::FileNotFound,
129            Self::InvalidFileFormat
130            | Self::InvalidFileFormatDetails(_)
131            | Self::UnsupportedVersion(_)
132            | Self::HeaderChecksumMismatch => ErrorCode::InvalidFormat,
133            Self::DecryptionFailed | Self::EncryptionFailed(_) | Self::MachineIdUnavailable(_) => {
134                ErrorCode::DecryptionFailed
135            }
136            Self::MachineMismatch => ErrorCode::MachineMismatch,
137            Self::SecretRequired => ErrorCode::SecretRequired,
138            Self::InvalidLength { .. } | Self::InvalidCharacter(_) | Self::InvalidCheckDigit => {
139                ErrorCode::InvalidCode
140            }
141            Self::CounterLocked => ErrorCode::CounterLocked,
142            Self::CounterOverflow => ErrorCode::CounterOverflow,
143            Self::Io(_) => ErrorCode::IoError,
144            Self::InvalidAlphabet(_) => ErrorCode::InvalidAlphabet,
145            Self::InvalidArgument(_) => ErrorCode::InvalidArgument,
146            Self::Json(_) => ErrorCode::InvalidFormat,
147        }
148    }
149}
150
151/// Validation result with detailed information.
152///
153/// This enum provides specific information about why a code validation
154/// succeeded or failed, useful for user feedback.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum ValidationResult {
157    /// Code is valid (format check passed).
158    Valid,
159
160    /// Code has wrong length.
161    InvalidLength {
162        /// Expected code length.
163        expected: usize,
164        /// Actual code length.
165        actual: usize,
166    },
167
168    /// Code contains character not in alphabet.
169    InvalidCharacter {
170        /// The invalid character found.
171        char: char,
172        /// Position of the invalid character (0-indexed).
173        position: usize,
174    },
175
176    /// Check digit verification failed.
177    InvalidCheckDigit,
178
179    /// Format mismatch (prefix/suffix/separator).
180    InvalidFormat {
181        /// Description of what was wrong with the format.
182        reason: String,
183    },
184}
185
186impl ValidationResult {
187    /// Returns `true` if the validation result indicates a valid code.
188    #[inline]
189    pub fn is_valid(&self) -> bool {
190        matches!(self, ValidationResult::Valid)
191    }
192
193    /// Returns the error code for FFI compatibility.
194    pub fn error_code(&self) -> u8 {
195        match self {
196            ValidationResult::Valid => 0,
197            ValidationResult::InvalidLength { .. } => 1,
198            ValidationResult::InvalidCharacter { .. } => 2,
199            ValidationResult::InvalidCheckDigit => 3,
200            ValidationResult::InvalidFormat { .. } => 4,
201        }
202    }
203
204    /// Returns the error position for character errors, or -1 if not applicable.
205    pub fn error_position(&self) -> i32 {
206        match self {
207            ValidationResult::InvalidCharacter { position, .. } => *position as i32,
208            _ => -1,
209        }
210    }
211
212    /// Returns the invalid character, or '\0' if not applicable.
213    pub fn error_char(&self) -> char {
214        match self {
215            ValidationResult::InvalidCharacter { char, .. } => *char,
216            _ => '\0',
217        }
218    }
219}
220
221/// Result type alias for promocrypt operations.
222pub type Result<T> = std::result::Result<T, PromocryptError>;
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_error_codes() {
230        assert_eq!(
231            PromocryptError::FileNotFound("test.bin".to_string()).code(),
232            ErrorCode::FileNotFound
233        );
234        assert_eq!(
235            PromocryptError::InvalidFileFormat.code(),
236            ErrorCode::InvalidFormat
237        );
238        assert_eq!(
239            PromocryptError::DecryptionFailed.code(),
240            ErrorCode::DecryptionFailed
241        );
242        assert_eq!(
243            PromocryptError::MachineMismatch.code(),
244            ErrorCode::MachineMismatch
245        );
246        assert_eq!(
247            PromocryptError::SecretRequired.code(),
248            ErrorCode::SecretRequired
249        );
250        assert_eq!(
251            PromocryptError::CounterLocked.code(),
252            ErrorCode::CounterLocked
253        );
254        assert_eq!(
255            PromocryptError::CounterOverflow.code(),
256            ErrorCode::CounterOverflow
257        );
258    }
259
260    #[test]
261    fn test_validation_result() {
262        assert!(ValidationResult::Valid.is_valid());
263        assert!(
264            !ValidationResult::InvalidLength {
265                expected: 10,
266                actual: 9
267            }
268            .is_valid()
269        );
270        assert!(
271            !ValidationResult::InvalidCharacter {
272                char: '!',
273                position: 5
274            }
275            .is_valid()
276        );
277        assert!(!ValidationResult::InvalidCheckDigit.is_valid());
278    }
279
280    #[test]
281    fn test_validation_error_codes() {
282        assert_eq!(ValidationResult::Valid.error_code(), 0);
283        assert_eq!(
284            ValidationResult::InvalidLength {
285                expected: 10,
286                actual: 9
287            }
288            .error_code(),
289            1
290        );
291        assert_eq!(
292            ValidationResult::InvalidCharacter {
293                char: '!',
294                position: 5
295            }
296            .error_code(),
297            2
298        );
299        assert_eq!(ValidationResult::InvalidCheckDigit.error_code(), 3);
300    }
301
302    #[test]
303    fn test_validation_error_position() {
304        assert_eq!(ValidationResult::Valid.error_position(), -1);
305        assert_eq!(
306            ValidationResult::InvalidCharacter {
307                char: '!',
308                position: 5
309            }
310            .error_position(),
311            5
312        );
313    }
314}