1use thiserror::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[repr(u8)]
12pub enum ErrorCode {
13 Success = 0,
15 FileNotFound = 1,
17 InvalidFormat = 2,
19 DecryptionFailed = 3,
21 MachineMismatch = 4,
23 SecretRequired = 5,
25 InvalidCode = 6,
27 CounterLocked = 7,
29 CounterOverflow = 8,
31 IoError = 9,
33 InvalidAlphabet = 10,
35 InvalidArgument = 11,
37}
38
39#[derive(Error, Debug)]
41pub enum PromocryptError {
42 #[error("Binary file not found: {0}")]
44 FileNotFound(String),
45
46 #[error("Invalid binary file format")]
48 InvalidFileFormat,
49
50 #[error("Invalid binary file format: {0}")]
52 InvalidFileFormatDetails(String),
53
54 #[error("File version {0} not supported (expected 2)")]
56 UnsupportedVersion(u8),
57
58 #[error("Header checksum mismatch")]
60 HeaderChecksumMismatch,
61
62 #[error("Decryption failed: invalid key or corrupted file")]
64 DecryptionFailed,
65
66 #[error("Encryption failed: {0}")]
68 EncryptionFailed(String),
69
70 #[error("Machine ID not available: {0}")]
72 MachineIdUnavailable(String),
73
74 #[error("File not bound to this machine")]
76 MachineMismatch,
77
78 #[error("Operation requires secret key")]
80 SecretRequired,
81
82 #[error("Invalid code length: expected {expected}, got {actual}")]
84 InvalidLength {
85 expected: usize,
87 actual: usize,
89 },
90
91 #[error("Invalid character in code: '{0}'")]
93 InvalidCharacter(char),
94
95 #[error("Invalid check digit")]
97 InvalidCheckDigit,
98
99 #[error("Counter file locked by another process")]
101 CounterLocked,
102
103 #[error("Counter overflow")]
105 CounterOverflow,
106
107 #[error("IO error: {0}")]
109 Io(#[from] std::io::Error),
110
111 #[error("Invalid alphabet: {0}")]
113 InvalidAlphabet(String),
114
115 #[error("Invalid argument: {0}")]
117 InvalidArgument(String),
118
119 #[error("JSON error: {0}")]
121 Json(#[from] serde_json::Error),
122}
123
124impl PromocryptError {
125 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#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum ValidationResult {
157 Valid,
159
160 InvalidLength {
162 expected: usize,
164 actual: usize,
166 },
167
168 InvalidCharacter {
170 char: char,
172 position: usize,
174 },
175
176 InvalidCheckDigit,
178
179 InvalidFormat {
181 reason: String,
183 },
184}
185
186impl ValidationResult {
187 #[inline]
189 pub fn is_valid(&self) -> bool {
190 matches!(self, ValidationResult::Valid)
191 }
192
193 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 pub fn error_position(&self) -> i32 {
206 match self {
207 ValidationResult::InvalidCharacter { position, .. } => *position as i32,
208 _ => -1,
209 }
210 }
211
212 pub fn error_char(&self) -> char {
214 match self {
215 ValidationResult::InvalidCharacter { char, .. } => *char,
216 _ => '\0',
217 }
218 }
219}
220
221pub 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}