use std::path::PathBuf;
use thiserror::Error;
use crate::types::AuthorId;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum AionError {
#[error("Failed to read file: {path}")]
FileReadError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to write file: {path}")]
FileWriteError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("File already exists: {path}")]
FileExists {
path: PathBuf,
},
#[error("File not found: {path}")]
FileNotFound {
path: PathBuf,
},
#[error("Permission denied: {path}")]
PermissionDenied {
path: PathBuf,
},
#[error("Invalid file format: {reason}")]
InvalidFormat {
reason: String,
},
#[error("Corrupted file: checksum mismatch (expected: {expected}, got: {actual})")]
CorruptedFile {
expected: String,
actual: String,
},
#[error("Unsupported file version: {version} (supported: {supported})")]
UnsupportedVersion {
version: u16,
supported: String,
},
#[error("Invalid header: {reason}")]
InvalidHeader {
reason: String,
},
#[error("Signature verification failed for version {version} by author {author}")]
SignatureVerificationFailed {
version: u64,
author: AuthorId,
},
#[error("Invalid signature: {reason}")]
InvalidSignature {
reason: String,
},
#[error(
"Unauthorized signer: author {author} has no active registry epoch at version {version}"
)]
UnauthorizedSigner {
author: AuthorId,
version: u64,
},
#[error(
"Key mismatch: author {author} signing key does not match registry epoch {epoch} operational key"
)]
KeyMismatch {
author: AuthorId,
epoch: u32,
},
#[error("Decryption failed: {reason}")]
DecryptionFailed {
reason: String,
},
#[error("Encryption failed: {reason}")]
EncryptionFailed {
reason: String,
},
#[error("Hash mismatch: expected {expected:x?}, got {actual:x?}")]
HashMismatch {
expected: [u8; 32],
actual: [u8; 32],
},
#[error("Invalid private key: {reason}")]
InvalidPrivateKey {
reason: String,
},
#[error("Invalid public key: {reason}")]
InvalidPublicKey {
reason: String,
},
#[error("Version chain broken at version {version}: parent hash mismatch")]
BrokenVersionChain {
version: u64,
},
#[error("Invalid version number: {version} (current: {current})")]
InvalidVersionNumber {
version: u64,
current: u64,
},
#[error("Version overflow: cannot increment beyond {max}")]
VersionOverflow {
max: u64,
},
#[error("Missing version: {version}")]
MissingVersion {
version: u64,
},
#[error("Key not found for author {author_id}: {reason}")]
KeyNotFound {
author_id: crate::types::AuthorId,
reason: String,
},
#[error("Keyring access denied: {reason}")]
KeyringAccessDenied {
reason: String,
},
#[error("Failed to store key: {reason}")]
KeyStoreFailed {
reason: String,
},
#[error("Keyring {operation} failed: {reason}")]
KeyringError {
operation: String,
reason: String,
},
#[error("Invalid file ID: {file_id}")]
InvalidFileId {
file_id: u64,
},
#[error("Invalid author ID: {author_id}")]
InvalidAuthorId {
author_id: u64,
},
#[error("Invalid timestamp: {reason}")]
InvalidTimestamp {
reason: String,
},
#[error("Invalid action code: {code}")]
InvalidActionCode {
code: u16,
},
#[error("Broken audit chain: expected hash {expected:?}, got {actual:?}")]
BrokenAuditChain {
expected: [u8; 32],
actual: [u8; 32],
},
#[error("Invalid UTF-8: {reason}")]
InvalidUtf8 {
reason: String,
},
#[error("Rules too large: {size} bytes (max: {max} bytes)")]
RulesTooLarge {
size: usize,
max: usize,
},
#[error("Operation not permitted: {operation} requires {required}")]
OperationNotPermitted {
operation: String,
required: String,
},
#[error("Conflicting operation: {reason}")]
Conflict {
reason: String,
},
#[error("Resource exhausted: {resource}")]
ResourceExhausted {
resource: String,
},
}
pub type Result<T> = std::result::Result<T, AionError>;
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn error_should_implement_error_trait() {
let err = AionError::InvalidFormat {
reason: "test".to_string(),
};
let _: &dyn std::error::Error = &err;
}
#[test]
fn error_should_be_send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<AionError>();
assert_sync::<AionError>();
}
mod file_errors {
use super::*;
#[test]
fn file_read_error_should_display_path_and_source() {
let err = AionError::FileReadError {
path: PathBuf::from("/test/file.aion"),
source: std::io::Error::from(std::io::ErrorKind::NotFound),
};
let msg = format!("{err}");
assert!(msg.contains("/test/file.aion"));
assert!(msg.contains("Failed to read file"));
}
#[test]
fn file_not_found_should_display_path() {
let err = AionError::FileNotFound {
path: PathBuf::from("/missing.aion"),
};
assert_eq!(format!("{err}"), "File not found: /missing.aion");
}
#[test]
fn permission_denied_should_display_path() {
let err = AionError::PermissionDenied {
path: PathBuf::from("/protected.aion"),
};
assert_eq!(format!("{err}"), "Permission denied: /protected.aion");
}
}
mod format_errors {
use super::*;
#[test]
fn invalid_format_should_display_reason() {
let err = AionError::InvalidFormat {
reason: "missing magic number".to_string(),
};
assert_eq!(
format!("{err}"),
"Invalid file format: missing magic number"
);
}
#[test]
fn corrupted_file_should_display_checksums() {
let err = AionError::CorruptedFile {
expected: "abc123".to_string(),
actual: "def456".to_string(),
};
let msg = format!("{err}");
assert!(msg.contains("abc123"));
assert!(msg.contains("def456"));
}
#[test]
fn unsupported_version_should_display_versions() {
let err = AionError::UnsupportedVersion {
version: 99,
supported: "1-2".to_string(),
};
assert_eq!(
format!("{err}"),
"Unsupported file version: 99 (supported: 1-2)"
);
}
}
mod crypto_errors {
use super::*;
#[test]
fn signature_verification_failed_should_display_details() {
let err = AionError::SignatureVerificationFailed {
version: 42,
author: AuthorId::new(1),
};
let msg = format!("{err}");
assert!(msg.contains("42"));
assert!(msg.contains('1'));
}
#[test]
fn invalid_signature_should_display_reason() {
let err = AionError::InvalidSignature {
reason: "wrong length".to_string(),
};
assert_eq!(format!("{err}"), "Invalid signature: wrong length");
}
#[test]
fn decryption_failed_should_display_reason() {
let err = AionError::DecryptionFailed {
reason: "wrong key".to_string(),
};
assert_eq!(format!("{err}"), "Decryption failed: wrong key");
}
}
mod version_errors {
use super::*;
#[test]
fn broken_version_chain_should_display_version() {
let err = AionError::BrokenVersionChain { version: 5 };
assert_eq!(
format!("{err}"),
"Version chain broken at version 5: parent hash mismatch"
);
}
#[test]
fn invalid_version_number_should_display_versions() {
let err = AionError::InvalidVersionNumber {
version: 10,
current: 5,
};
assert_eq!(format!("{err}"), "Invalid version number: 10 (current: 5)");
}
#[test]
fn version_overflow_should_display_max() {
let err = AionError::VersionOverflow { max: u64::MAX };
let msg = format!("{err}");
assert!(msg.contains("overflow"));
assert!(msg.contains(&u64::MAX.to_string()));
}
#[test]
fn missing_version_should_display_version() {
let err = AionError::MissingVersion { version: 3 };
assert_eq!(format!("{err}"), "Missing version: 3");
}
}
mod key_management_errors {
use super::*;
use crate::types::AuthorId;
#[test]
fn key_not_found_should_display_author_id() {
let err = AionError::KeyNotFound {
author_id: AuthorId::new(50001),
reason: "not found".to_string(),
};
assert_eq!(
format!("{err}"),
"Key not found for author 50001: not found"
);
}
#[test]
fn keyring_access_denied_should_display_reason() {
let err = AionError::KeyringAccessDenied {
reason: "locked".to_string(),
};
assert_eq!(format!("{err}"), "Keyring access denied: locked");
}
#[test]
fn keyring_error_should_display_operation() {
let err = AionError::KeyringError {
operation: "store".to_string(),
reason: "permission denied".to_string(),
};
assert_eq!(format!("{err}"), "Keyring store failed: permission denied");
}
}
mod validation_errors {
use super::*;
#[test]
fn invalid_file_id_should_display_id() {
let err = AionError::InvalidFileId { file_id: 0 };
assert_eq!(format!("{err}"), "Invalid file ID: 0");
}
#[test]
fn rules_too_large_should_display_sizes() {
let err = AionError::RulesTooLarge {
size: 2_000_000,
max: 1_000_000,
};
let msg = format!("{err}");
assert!(msg.contains("2000000"));
assert!(msg.contains("1000000"));
}
#[test]
fn invalid_action_code_should_display_code() {
let err = AionError::InvalidActionCode { code: 99 };
assert_eq!(format!("{err}"), "Invalid action code: 99");
}
#[test]
fn broken_audit_chain_should_display_hashes() {
let expected = [0xAB; 32];
let actual = [0xCD; 32];
let err = AionError::BrokenAuditChain { expected, actual };
let msg = format!("{err}");
assert!(msg.contains("Broken audit chain"));
}
#[test]
fn invalid_timestamp_should_display_reason() {
let err = AionError::InvalidTimestamp {
reason: "timestamp is in the future".to_string(),
};
assert_eq!(
format!("{err}"),
"Invalid timestamp: timestamp is in the future"
);
}
#[test]
fn invalid_utf8_should_display_reason() {
let err = AionError::InvalidUtf8 {
reason: "invalid byte sequence at offset 10".to_string(),
};
assert_eq!(
format!("{err}"),
"Invalid UTF-8: invalid byte sequence at offset 10"
);
}
}
mod operational_errors {
use super::*;
#[test]
fn operation_not_permitted_should_display_details() {
let err = AionError::OperationNotPermitted {
operation: "commit".to_string(),
required: "write access".to_string(),
};
let msg = format!("{err}");
assert!(msg.contains("commit"));
assert!(msg.contains("write access"));
}
#[test]
fn conflict_should_display_reason() {
let err = AionError::Conflict {
reason: "version already exists".to_string(),
};
assert_eq!(
format!("{err}"),
"Conflicting operation: version already exists"
);
}
#[test]
fn resource_exhausted_should_display_resource() {
let err = AionError::ResourceExhausted {
resource: "memory".to_string(),
};
assert_eq!(format!("{err}"), "Resource exhausted: memory");
}
}
mod result_type {
use super::*;
#[test]
fn result_should_work_with_ok() {
let result: Result<i32> = Ok(42);
assert!(result.is_ok());
if let Ok(value) = result {
assert_eq!(value, 42);
}
}
#[test]
fn result_should_work_with_err() {
let result: Result<i32> = Err(AionError::InvalidFormat {
reason: "test".to_string(),
});
assert!(result.is_err());
}
}
}