Skip to main content

aion_context/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Error types for AION v2
3//!
4//! This module defines the error types used throughout AION v2. Following Tiger Style,
5//! all errors are explicit, provide actionable messages, and implement `std::error::Error`.
6//!
7//! # Error Categories
8//!
9//! Errors are organized into logical categories for clarity:
10//!
11//! - **I/O Errors** - File system operations
12//! - **Cryptographic Errors** - Signature and encryption failures
13//! - **Format Errors** - File parsing and validation
14//! - **Version Errors** - Version chain and history issues
15//! - **Key Management Errors** - Keyring and key storage
16//! - **Validation Errors** - Input validation failures
17//! - **Operational Errors** - Runtime operation failures
18//!
19//! # Usage Example
20//!
21//! ```
22//! use aion_context::{AionError, Result};
23//! use std::path::PathBuf;
24//!
25//! fn load_file(path: &str) -> Result<Vec<u8>> {
26//!     std::fs::read(path).map_err(|e| AionError::FileReadError {
27//!         path: PathBuf::from(path),
28//!         source: e,
29//!     })
30//! }
31//!
32//! // Errors provide contextual information
33//! match load_file("nonexistent.aion") {
34//!     Ok(data) => println!("Loaded {} bytes", data.len()),
35//!     Err(e) => eprintln!("Error: {e}"),
36//! }
37//! ```
38//!
39//! # Error Handling Best Practices
40//!
41//! 1. **Always use `?` operator** for error propagation
42//! 2. **Add context** to errors when re-wrapping
43//! 3. **Match on error types** for specific handling
44//! 4. **Display errors to users** with helpful messages
45//!
46//! ```
47//! use aion_context::Result;
48//!
49//! fn process_file() -> Result<()> {
50//!     let data = load_file("file.aion")?;  // ✓ Propagate errors
51//!     verify_signatures(&data)?;            // ✓ Chain operations
52//!     Ok(())
53//! }
54//! # fn load_file(path: &str) -> Result<Vec<u8>> { Ok(vec![]) }
55//! # fn verify_signatures(data: &[u8]) -> Result<()> { Ok(()) }
56//! ```
57
58use std::path::PathBuf;
59use thiserror::Error;
60
61use crate::types::AuthorId;
62
63/// Top-level error type for AION v2
64///
65/// All errors provide contextual information to aid debugging and suggest
66/// solutions to users. Errors are organized by category for clarity.
67///
68/// `#[non_exhaustive]` because the crate is under active development —
69/// new error categories will land for crypto rotations, hardware
70/// attestation, transparency-log proofs, and additional compliance
71/// frameworks. Adding a variant should not force every downstream
72/// consumer's exhaustive `match` to update on a minor release.
73#[derive(Error, Debug)]
74#[non_exhaustive]
75pub enum AionError {
76    // ============================================================================
77    // I/O Errors
78    // ============================================================================
79    /// Failed to read a file
80    #[error("Failed to read file: {path}")]
81    FileReadError {
82        /// Path to the file that couldn't be read
83        path: PathBuf,
84        /// Underlying I/O error
85        #[source]
86        source: std::io::Error,
87    },
88
89    /// Failed to write a file
90    #[error("Failed to write file: {path}")]
91    FileWriteError {
92        /// Path to the file that couldn't be written
93        path: PathBuf,
94        /// Underlying I/O error
95        #[source]
96        source: std::io::Error,
97    },
98
99    /// File already exists
100    #[error("File already exists: {path}")]
101    FileExists {
102        /// Path to the existing file
103        path: PathBuf,
104    },
105
106    /// File not found
107    #[error("File not found: {path}")]
108    FileNotFound {
109        /// Path to the missing file
110        path: PathBuf,
111    },
112
113    /// Permission denied
114    #[error("Permission denied: {path}")]
115    PermissionDenied {
116        /// Path to the file with permission issues
117        path: PathBuf,
118    },
119
120    // ============================================================================
121    // Format Errors
122    // ============================================================================
123    /// Invalid file format
124    #[error("Invalid file format: {reason}")]
125    InvalidFormat {
126        /// Description of the format issue
127        reason: String,
128    },
129
130    /// Corrupted file detected via checksum mismatch
131    #[error("Corrupted file: checksum mismatch (expected: {expected}, got: {actual})")]
132    CorruptedFile {
133        /// Expected checksum
134        expected: String,
135        /// Actual checksum
136        actual: String,
137    },
138
139    /// Unsupported file version
140    #[error("Unsupported file version: {version} (supported: {supported})")]
141    UnsupportedVersion {
142        /// Version found in the file
143        version: u16,
144        /// Supported versions
145        supported: String,
146    },
147
148    /// Invalid file header
149    #[error("Invalid header: {reason}")]
150    InvalidHeader {
151        /// Description of header issue
152        reason: String,
153    },
154
155    // ============================================================================
156    // Cryptographic Errors
157    // ============================================================================
158    /// Signature verification failed
159    #[error("Signature verification failed for version {version} by author {author}")]
160    SignatureVerificationFailed {
161        /// Version number that failed verification
162        version: u64,
163        /// Author ID
164        author: AuthorId,
165    },
166
167    /// Invalid signature
168    #[error("Invalid signature: {reason}")]
169    InvalidSignature {
170        /// Description of signature issue
171        reason: String,
172    },
173
174    /// A commit was attempted by a signer the registry has no active
175    /// epoch for at the target version (issue #25). The write was
176    /// refused before any bytes were emitted.
177    #[error(
178        "Unauthorized signer: author {author} has no active registry epoch at version {version}"
179    )]
180    UnauthorizedSigner {
181        /// The author that attempted to sign.
182        author: AuthorId,
183        /// The version number the commit would have produced.
184        version: u64,
185    },
186
187    /// The supplied signing key's public half does not match the
188    /// operational key pinned in the registry for this author's active
189    /// epoch (issue #25). Most often means the caller used a rotated-
190    /// out key.
191    #[error(
192        "Key mismatch: author {author} signing key does not match registry epoch {epoch} operational key"
193    )]
194    KeyMismatch {
195        /// The author that attempted to sign.
196        author: AuthorId,
197        /// The registry epoch number that pinned a different public key.
198        epoch: u32,
199    },
200
201    /// Decryption failed
202    #[error("Decryption failed: {reason}")]
203    DecryptionFailed {
204        /// Description of decryption failure
205        reason: String,
206    },
207
208    /// Encryption failed
209    #[error("Encryption failed: {reason}")]
210    EncryptionFailed {
211        /// Description of encryption failure
212        reason: String,
213    },
214
215    /// Hash mismatch after decryption
216    #[error("Hash mismatch: expected {expected:x?}, got {actual:x?}")]
217    HashMismatch {
218        /// Expected hash value
219        expected: [u8; 32],
220        /// Actual hash value
221        actual: [u8; 32],
222    },
223
224    /// Invalid private key
225    #[error("Invalid private key: {reason}")]
226    InvalidPrivateKey {
227        /// Description of key issue
228        reason: String,
229    },
230
231    /// Invalid public key
232    #[error("Invalid public key: {reason}")]
233    InvalidPublicKey {
234        /// Description of key issue
235        reason: String,
236    },
237
238    // ============================================================================
239    // Version Chain Errors
240    // ============================================================================
241    /// Version chain integrity broken
242    #[error("Version chain broken at version {version}: parent hash mismatch")]
243    BrokenVersionChain {
244        /// Version where the chain breaks
245        version: u64,
246    },
247
248    /// Invalid version number
249    #[error("Invalid version number: {version} (current: {current})")]
250    InvalidVersionNumber {
251        /// Invalid version number
252        version: u64,
253        /// Current version
254        current: u64,
255    },
256
257    /// Version number overflow
258    #[error("Version overflow: cannot increment beyond {max}")]
259    VersionOverflow {
260        /// Maximum version reached
261        max: u64,
262    },
263
264    /// Missing version in chain
265    #[error("Missing version: {version}")]
266    MissingVersion {
267        /// Missing version number
268        version: u64,
269    },
270
271    // ============================================================================
272    // Key Management Errors
273    // ============================================================================
274    /// Key not found in keyring
275    #[error("Key not found for author {author_id}: {reason}")]
276    KeyNotFound {
277        /// Author identifier
278        author_id: crate::types::AuthorId,
279        /// Description of the error
280        reason: String,
281    },
282
283    /// Keyring access denied
284    #[error("Keyring access denied: {reason}")]
285    KeyringAccessDenied {
286        /// Description of access issue
287        reason: String,
288    },
289
290    /// Failed to store key
291    #[error("Failed to store key: {reason}")]
292    KeyStoreFailed {
293        /// Description of storage failure
294        reason: String,
295    },
296
297    /// Keyring operation error
298    #[error("Keyring {operation} failed: {reason}")]
299    KeyringError {
300        /// Operation that failed
301        operation: String,
302        /// Description of the error
303        reason: String,
304    },
305
306    // ============================================================================
307    // Validation Errors
308    // ============================================================================
309    /// Invalid file ID
310    #[error("Invalid file ID: {file_id}")]
311    InvalidFileId {
312        /// Invalid file ID value
313        file_id: u64,
314    },
315
316    /// Invalid author ID
317    #[error("Invalid author ID: {author_id}")]
318    InvalidAuthorId {
319        /// Invalid author ID value
320        author_id: u64,
321    },
322
323    /// Invalid timestamp
324    #[error("Invalid timestamp: {reason}")]
325    InvalidTimestamp {
326        /// Description of timestamp issue
327        reason: String,
328    },
329
330    /// Invalid action code
331    #[error("Invalid action code: {code}")]
332    InvalidActionCode {
333        /// Invalid action code value
334        code: u16,
335    },
336
337    /// Broken audit chain
338    #[error("Broken audit chain: expected hash {expected:?}, got {actual:?}")]
339    BrokenAuditChain {
340        /// Expected previous hash
341        expected: [u8; 32],
342        /// Actual previous hash
343        actual: [u8; 32],
344    },
345
346    /// Invalid UTF-8 encoding
347    #[error("Invalid UTF-8: {reason}")]
348    InvalidUtf8 {
349        /// Description of UTF-8 validation failure
350        reason: String,
351    },
352
353    /// Rules too large
354    #[error("Rules too large: {size} bytes (max: {max} bytes)")]
355    RulesTooLarge {
356        /// Actual size
357        size: usize,
358        /// Maximum allowed size
359        max: usize,
360    },
361
362    // ============================================================================
363    // Operational Errors
364    // ============================================================================
365    /// Operation not permitted
366    #[error("Operation not permitted: {operation} requires {required}")]
367    OperationNotPermitted {
368        /// Operation attempted
369        operation: String,
370        /// Required permission/state
371        required: String,
372    },
373
374    /// Conflicting operation
375    #[error("Conflicting operation: {reason}")]
376    Conflict {
377        /// Description of conflict
378        reason: String,
379    },
380
381    /// Resource exhausted
382    #[error("Resource exhausted: {resource}")]
383    ResourceExhausted {
384        /// Resource that was exhausted
385        resource: String,
386    },
387}
388
389/// Result type alias for AION operations
390///
391/// # Examples
392///
393/// ```
394/// use aion_context::error::{AionError, Result};
395///
396/// fn read_version(version: u64) -> Result<String> {
397///     if version == 0 {
398///         return Err(AionError::InvalidVersionNumber { version, current: 1 });
399///     }
400///     Ok("version data".to_string())
401/// }
402/// ```
403pub type Result<T> = std::result::Result<T, AionError>;
404
405// Implement Send + Sync for async compatibility
406// (thiserror derives these automatically when possible)
407
408#[cfg(test)]
409#[allow(clippy::unwrap_used)] // Tests are allowed to panic
410mod tests {
411    use super::*;
412
413    #[test]
414    fn error_should_implement_error_trait() {
415        let err = AionError::InvalidFormat {
416            reason: "test".to_string(),
417        };
418        let _: &dyn std::error::Error = &err;
419    }
420
421    #[test]
422    fn error_should_be_send_and_sync() {
423        fn assert_send<T: Send>() {}
424        fn assert_sync<T: Sync>() {}
425        assert_send::<AionError>();
426        assert_sync::<AionError>();
427    }
428
429    mod file_errors {
430        use super::*;
431
432        #[test]
433        fn file_read_error_should_display_path_and_source() {
434            let err = AionError::FileReadError {
435                path: PathBuf::from("/test/file.aion"),
436                source: std::io::Error::from(std::io::ErrorKind::NotFound),
437            };
438            let msg = format!("{err}");
439            assert!(msg.contains("/test/file.aion"));
440            assert!(msg.contains("Failed to read file"));
441        }
442
443        #[test]
444        fn file_not_found_should_display_path() {
445            let err = AionError::FileNotFound {
446                path: PathBuf::from("/missing.aion"),
447            };
448            assert_eq!(format!("{err}"), "File not found: /missing.aion");
449        }
450
451        #[test]
452        fn permission_denied_should_display_path() {
453            let err = AionError::PermissionDenied {
454                path: PathBuf::from("/protected.aion"),
455            };
456            assert_eq!(format!("{err}"), "Permission denied: /protected.aion");
457        }
458    }
459
460    mod format_errors {
461        use super::*;
462
463        #[test]
464        fn invalid_format_should_display_reason() {
465            let err = AionError::InvalidFormat {
466                reason: "missing magic number".to_string(),
467            };
468            assert_eq!(
469                format!("{err}"),
470                "Invalid file format: missing magic number"
471            );
472        }
473
474        #[test]
475        fn corrupted_file_should_display_checksums() {
476            let err = AionError::CorruptedFile {
477                expected: "abc123".to_string(),
478                actual: "def456".to_string(),
479            };
480            let msg = format!("{err}");
481            assert!(msg.contains("abc123"));
482            assert!(msg.contains("def456"));
483        }
484
485        #[test]
486        fn unsupported_version_should_display_versions() {
487            let err = AionError::UnsupportedVersion {
488                version: 99,
489                supported: "1-2".to_string(),
490            };
491            assert_eq!(
492                format!("{err}"),
493                "Unsupported file version: 99 (supported: 1-2)"
494            );
495        }
496    }
497
498    mod crypto_errors {
499        use super::*;
500
501        #[test]
502        fn signature_verification_failed_should_display_details() {
503            let err = AionError::SignatureVerificationFailed {
504                version: 42,
505                author: AuthorId::new(1),
506            };
507            let msg = format!("{err}");
508            assert!(msg.contains("42"));
509            assert!(msg.contains('1'));
510        }
511
512        #[test]
513        fn invalid_signature_should_display_reason() {
514            let err = AionError::InvalidSignature {
515                reason: "wrong length".to_string(),
516            };
517            assert_eq!(format!("{err}"), "Invalid signature: wrong length");
518        }
519
520        #[test]
521        fn decryption_failed_should_display_reason() {
522            let err = AionError::DecryptionFailed {
523                reason: "wrong key".to_string(),
524            };
525            assert_eq!(format!("{err}"), "Decryption failed: wrong key");
526        }
527    }
528
529    mod version_errors {
530        use super::*;
531
532        #[test]
533        fn broken_version_chain_should_display_version() {
534            let err = AionError::BrokenVersionChain { version: 5 };
535            assert_eq!(
536                format!("{err}"),
537                "Version chain broken at version 5: parent hash mismatch"
538            );
539        }
540
541        #[test]
542        fn invalid_version_number_should_display_versions() {
543            let err = AionError::InvalidVersionNumber {
544                version: 10,
545                current: 5,
546            };
547            assert_eq!(format!("{err}"), "Invalid version number: 10 (current: 5)");
548        }
549
550        #[test]
551        fn version_overflow_should_display_max() {
552            let err = AionError::VersionOverflow { max: u64::MAX };
553            let msg = format!("{err}");
554            assert!(msg.contains("overflow"));
555            assert!(msg.contains(&u64::MAX.to_string()));
556        }
557
558        #[test]
559        fn missing_version_should_display_version() {
560            let err = AionError::MissingVersion { version: 3 };
561            assert_eq!(format!("{err}"), "Missing version: 3");
562        }
563    }
564
565    mod key_management_errors {
566        use super::*;
567        use crate::types::AuthorId;
568
569        #[test]
570        fn key_not_found_should_display_author_id() {
571            let err = AionError::KeyNotFound {
572                author_id: AuthorId::new(50001),
573                reason: "not found".to_string(),
574            };
575            assert_eq!(
576                format!("{err}"),
577                "Key not found for author 50001: not found"
578            );
579        }
580
581        #[test]
582        fn keyring_access_denied_should_display_reason() {
583            let err = AionError::KeyringAccessDenied {
584                reason: "locked".to_string(),
585            };
586            assert_eq!(format!("{err}"), "Keyring access denied: locked");
587        }
588
589        #[test]
590        fn keyring_error_should_display_operation() {
591            let err = AionError::KeyringError {
592                operation: "store".to_string(),
593                reason: "permission denied".to_string(),
594            };
595            assert_eq!(format!("{err}"), "Keyring store failed: permission denied");
596        }
597    }
598
599    mod validation_errors {
600        use super::*;
601
602        #[test]
603        fn invalid_file_id_should_display_id() {
604            let err = AionError::InvalidFileId { file_id: 0 };
605            assert_eq!(format!("{err}"), "Invalid file ID: 0");
606        }
607
608        #[test]
609        fn rules_too_large_should_display_sizes() {
610            let err = AionError::RulesTooLarge {
611                size: 2_000_000,
612                max: 1_000_000,
613            };
614            let msg = format!("{err}");
615            assert!(msg.contains("2000000"));
616            assert!(msg.contains("1000000"));
617        }
618
619        #[test]
620        fn invalid_action_code_should_display_code() {
621            let err = AionError::InvalidActionCode { code: 99 };
622            assert_eq!(format!("{err}"), "Invalid action code: 99");
623        }
624
625        #[test]
626        fn broken_audit_chain_should_display_hashes() {
627            let expected = [0xAB; 32];
628            let actual = [0xCD; 32];
629            let err = AionError::BrokenAuditChain { expected, actual };
630            let msg = format!("{err}");
631            assert!(msg.contains("Broken audit chain"));
632        }
633
634        #[test]
635        fn invalid_timestamp_should_display_reason() {
636            let err = AionError::InvalidTimestamp {
637                reason: "timestamp is in the future".to_string(),
638            };
639            assert_eq!(
640                format!("{err}"),
641                "Invalid timestamp: timestamp is in the future"
642            );
643        }
644
645        #[test]
646        fn invalid_utf8_should_display_reason() {
647            let err = AionError::InvalidUtf8 {
648                reason: "invalid byte sequence at offset 10".to_string(),
649            };
650            assert_eq!(
651                format!("{err}"),
652                "Invalid UTF-8: invalid byte sequence at offset 10"
653            );
654        }
655    }
656
657    mod operational_errors {
658        use super::*;
659
660        #[test]
661        fn operation_not_permitted_should_display_details() {
662            let err = AionError::OperationNotPermitted {
663                operation: "commit".to_string(),
664                required: "write access".to_string(),
665            };
666            let msg = format!("{err}");
667            assert!(msg.contains("commit"));
668            assert!(msg.contains("write access"));
669        }
670
671        #[test]
672        fn conflict_should_display_reason() {
673            let err = AionError::Conflict {
674                reason: "version already exists".to_string(),
675            };
676            assert_eq!(
677                format!("{err}"),
678                "Conflicting operation: version already exists"
679            );
680        }
681
682        #[test]
683        fn resource_exhausted_should_display_resource() {
684            let err = AionError::ResourceExhausted {
685                resource: "memory".to_string(),
686            };
687            assert_eq!(format!("{err}"), "Resource exhausted: memory");
688        }
689    }
690
691    mod result_type {
692        use super::*;
693
694        #[test]
695        fn result_should_work_with_ok() {
696            let result: Result<i32> = Ok(42);
697            assert!(result.is_ok());
698            if let Ok(value) = result {
699                assert_eq!(value, 42);
700            }
701        }
702
703        #[test]
704        fn result_should_work_with_err() {
705            let result: Result<i32> = Err(AionError::InvalidFormat {
706                reason: "test".to_string(),
707            });
708            assert!(result.is_err());
709        }
710    }
711}