1use std::path::PathBuf;
59use thiserror::Error;
60
61use crate::types::AuthorId;
62
63#[derive(Error, Debug)]
74#[non_exhaustive]
75pub enum AionError {
76 #[error("Failed to read file: {path}")]
81 FileReadError {
82 path: PathBuf,
84 #[source]
86 source: std::io::Error,
87 },
88
89 #[error("Failed to write file: {path}")]
91 FileWriteError {
92 path: PathBuf,
94 #[source]
96 source: std::io::Error,
97 },
98
99 #[error("File already exists: {path}")]
101 FileExists {
102 path: PathBuf,
104 },
105
106 #[error("File not found: {path}")]
108 FileNotFound {
109 path: PathBuf,
111 },
112
113 #[error("Permission denied: {path}")]
115 PermissionDenied {
116 path: PathBuf,
118 },
119
120 #[error("Invalid file format: {reason}")]
125 InvalidFormat {
126 reason: String,
128 },
129
130 #[error("Corrupted file: checksum mismatch (expected: {expected}, got: {actual})")]
132 CorruptedFile {
133 expected: String,
135 actual: String,
137 },
138
139 #[error("Unsupported file version: {version} (supported: {supported})")]
141 UnsupportedVersion {
142 version: u16,
144 supported: String,
146 },
147
148 #[error("Invalid header: {reason}")]
150 InvalidHeader {
151 reason: String,
153 },
154
155 #[error("Signature verification failed for version {version} by author {author}")]
160 SignatureVerificationFailed {
161 version: u64,
163 author: AuthorId,
165 },
166
167 #[error("Invalid signature: {reason}")]
169 InvalidSignature {
170 reason: String,
172 },
173
174 #[error(
178 "Unauthorized signer: author {author} has no active registry epoch at version {version}"
179 )]
180 UnauthorizedSigner {
181 author: AuthorId,
183 version: u64,
185 },
186
187 #[error(
192 "Key mismatch: author {author} signing key does not match registry epoch {epoch} operational key"
193 )]
194 KeyMismatch {
195 author: AuthorId,
197 epoch: u32,
199 },
200
201 #[error("Decryption failed: {reason}")]
203 DecryptionFailed {
204 reason: String,
206 },
207
208 #[error("Encryption failed: {reason}")]
210 EncryptionFailed {
211 reason: String,
213 },
214
215 #[error("Hash mismatch: expected {expected:x?}, got {actual:x?}")]
217 HashMismatch {
218 expected: [u8; 32],
220 actual: [u8; 32],
222 },
223
224 #[error("Invalid private key: {reason}")]
226 InvalidPrivateKey {
227 reason: String,
229 },
230
231 #[error("Invalid public key: {reason}")]
233 InvalidPublicKey {
234 reason: String,
236 },
237
238 #[error("Version chain broken at version {version}: parent hash mismatch")]
243 BrokenVersionChain {
244 version: u64,
246 },
247
248 #[error("Invalid version number: {version} (current: {current})")]
250 InvalidVersionNumber {
251 version: u64,
253 current: u64,
255 },
256
257 #[error("Version overflow: cannot increment beyond {max}")]
259 VersionOverflow {
260 max: u64,
262 },
263
264 #[error("Missing version: {version}")]
266 MissingVersion {
267 version: u64,
269 },
270
271 #[error("Key not found for author {author_id}: {reason}")]
276 KeyNotFound {
277 author_id: crate::types::AuthorId,
279 reason: String,
281 },
282
283 #[error("Keyring access denied: {reason}")]
285 KeyringAccessDenied {
286 reason: String,
288 },
289
290 #[error("Failed to store key: {reason}")]
292 KeyStoreFailed {
293 reason: String,
295 },
296
297 #[error("Keyring {operation} failed: {reason}")]
299 KeyringError {
300 operation: String,
302 reason: String,
304 },
305
306 #[error("Invalid file ID: {file_id}")]
311 InvalidFileId {
312 file_id: u64,
314 },
315
316 #[error("Invalid author ID: {author_id}")]
318 InvalidAuthorId {
319 author_id: u64,
321 },
322
323 #[error("Invalid timestamp: {reason}")]
325 InvalidTimestamp {
326 reason: String,
328 },
329
330 #[error("Invalid action code: {code}")]
332 InvalidActionCode {
333 code: u16,
335 },
336
337 #[error("Broken audit chain: expected hash {expected:?}, got {actual:?}")]
339 BrokenAuditChain {
340 expected: [u8; 32],
342 actual: [u8; 32],
344 },
345
346 #[error("Invalid UTF-8: {reason}")]
348 InvalidUtf8 {
349 reason: String,
351 },
352
353 #[error("Rules too large: {size} bytes (max: {max} bytes)")]
355 RulesTooLarge {
356 size: usize,
358 max: usize,
360 },
361
362 #[error("Operation not permitted: {operation} requires {required}")]
367 OperationNotPermitted {
368 operation: String,
370 required: String,
372 },
373
374 #[error("Conflicting operation: {reason}")]
376 Conflict {
377 reason: String,
379 },
380
381 #[error("Resource exhausted: {resource}")]
383 ResourceExhausted {
384 resource: String,
386 },
387}
388
389pub type Result<T> = std::result::Result<T, AionError>;
404
405#[cfg(test)]
409#[allow(clippy::unwrap_used)] mod 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}