use std::io;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, VaultError>;
#[derive(Error, Debug)]
pub enum CryptoError {
#[error("Key derivation failed: {0}")]
KeyDerivation(String),
#[error("Encryption failed: {0}")]
Encryption(String),
#[error("Decryption failed: {0}")]
Decryption(String),
#[error("Integrity check failed: {0}")]
Integrity(String),
#[error("Cryptographic error: {0}")]
Other(String),
}
#[derive(Error, Debug)]
pub enum StorageError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Compression error: {0}")]
Compression(String),
#[error("Database error: {0}")]
Database(String),
#[error("Storage error: {0}")]
Other(String),
}
#[derive(Error, Debug)]
pub enum ConversionError {
#[error("Unsupported conversion: {0}")]
Unsupported(String),
#[error("Validation failed: {0}")]
Validation(String),
#[error("Conversion error: {0}")]
Other(String),
}
impl From<CryptoError> for VaultError {
fn from(err: CryptoError) -> Self {
match err {
CryptoError::Integrity(msg) => VaultError::IntegrityError(msg),
other => VaultError::CryptoError(other.to_string()),
}
}
}
impl From<StorageError> for VaultError {
fn from(err: StorageError) -> Self {
match err {
StorageError::Io(e) => VaultError::IoError(e),
StorageError::Serialization(msg) => VaultError::SerializationError(msg),
StorageError::Compression(msg) => VaultError::CompressionError(msg),
StorageError::Database(msg) => VaultError::StorageError(msg),
StorageError::Other(msg) => VaultError::StorageError(msg),
}
}
}
impl From<ConversionError> for VaultError {
fn from(err: ConversionError) -> Self {
match err {
ConversionError::Unsupported(msg) => VaultError::UnsupportedFormat(msg),
other => VaultError::ConversionError(other.to_string()),
}
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum VaultError {
#[error("Cryptographic error: {0}")]
CryptoError(String),
#[error("Authentication failed: invalid passphrase or corrupted data")]
AuthenticationFailed,
#[error("Integrity check failed: {0}")]
IntegrityError(String),
#[error("Version control error: {0}")]
VersionError(String),
#[error("Model not found: {0}")]
ModelNotFound(String),
#[error("Version {0} not found for model {1}")]
VersionNotFound(u32, String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Format conversion error: {0}")]
ConversionError(String),
#[error("Unsupported model format: {0}")]
UnsupportedFormat(String),
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Compression error: {0}")]
CompressionError(String),
#[error("Security policy violation: {0}")]
SecurityViolation(String),
#[error("Compliance violation: {0}")]
ComplianceViolation(String),
#[error("Audit log error: {0}")]
AuditError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Storage error: {0}")]
StorageError(String),
}
pub const EXIT_SUCCESS: u8 = 0;
pub const EXIT_GENERAL: u8 = 1;
pub const EXIT_AUTH: u8 = 2;
pub const EXIT_NOT_FOUND: u8 = 3;
pub const EXIT_PERMISSION: u8 = 4;
pub const EXIT_INTEGRITY: u8 = 5;
pub const EXIT_INVALID_INPUT: u8 = 6;
pub const EXIT_CONFIG: u8 = 7;
pub const EXIT_COMPLIANCE: u8 = 8;
impl VaultError {
#[must_use]
pub fn exit_code(&self) -> u8 {
match self {
VaultError::AuthenticationFailed => EXIT_AUTH,
VaultError::ModelNotFound(_)
| VaultError::VersionNotFound(_, _)
| VaultError::NotFound(_) => EXIT_NOT_FOUND,
VaultError::SecurityViolation(_) => EXIT_PERMISSION,
VaultError::IoError(err) if err.kind() == io::ErrorKind::PermissionDenied => {
EXIT_PERMISSION
}
VaultError::IntegrityError(_) => EXIT_INTEGRITY,
VaultError::InvalidInput(_) | VaultError::UnsupportedFormat(_) => EXIT_INVALID_INPUT,
VaultError::ConfigError(_) => EXIT_CONFIG,
VaultError::ComplianceViolation(_) => EXIT_COMPLIANCE,
VaultError::CryptoError(_)
| VaultError::VersionError(_)
| VaultError::ConversionError(_)
| VaultError::IoError(_)
| VaultError::SerializationError(_)
| VaultError::CompressionError(_)
| VaultError::AuditError(_)
| VaultError::StorageError(_) => EXIT_GENERAL,
}
}
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
VaultError::CryptoError(_) => "crypto",
VaultError::AuthenticationFailed => "authentication_failed",
VaultError::IntegrityError(_) => "integrity",
VaultError::VersionError(_) => "version",
VaultError::ModelNotFound(_) => "model_not_found",
VaultError::VersionNotFound(_, _) => "version_not_found",
VaultError::NotFound(_) => "not_found",
VaultError::ConversionError(_) => "conversion",
VaultError::UnsupportedFormat(_) => "unsupported_format",
VaultError::IoError(_) => "io",
VaultError::ConfigError(_) => "config",
VaultError::SerializationError(_) => "serialization",
VaultError::CompressionError(_) => "compression",
VaultError::SecurityViolation(_) => "security_violation",
VaultError::InvalidInput(_) => "invalid_input",
VaultError::ComplianceViolation(_) => "compliance_violation",
VaultError::AuditError(_) => "audit",
VaultError::StorageError(_) => "storage",
}
}
}
impl From<serde_json::Error> for VaultError {
fn from(err: serde_json::Error) -> Self {
VaultError::SerializationError(err.to_string())
}
}
impl From<serde_yaml_ng::Error> for VaultError {
fn from(err: serde_yaml_ng::Error) -> Self {
VaultError::SerializationError(err.to_string())
}
}
impl From<zip::result::ZipError> for VaultError {
fn from(err: zip::result::ZipError) -> Self {
VaultError::IoError(io::Error::other(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zip_error_conversion() {
let zip_err = zip::result::ZipError::FileNotFound;
let vault_err: VaultError = zip_err.into();
match vault_err {
VaultError::IoError(_) => {} _ => panic!("Expected IoError"),
}
}
#[test]
fn test_serde_yml_error_conversion() {
let yaml_err = serde_yaml_ng::from_str::<serde_yaml_ng::Value>("\t").unwrap_err();
let vault_err: VaultError = yaml_err.into();
match vault_err {
VaultError::SerializationError(_) => {}
_ => panic!("Expected SerializationError"),
}
}
#[test]
fn test_serde_json_error_conversion() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let vault_err: VaultError = json_err.into();
match vault_err {
VaultError::SerializationError(msg) => {
assert!(!msg.is_empty());
}
_ => panic!("Expected SerializationError"),
}
}
#[test]
fn test_error_display_messages() {
let errors: Vec<VaultError> = vec![
VaultError::CryptoError("crypto fail".into()),
VaultError::AuthenticationFailed,
VaultError::IntegrityError("integrity fail".into()),
VaultError::VersionError("version fail".into()),
VaultError::ModelNotFound("model1".into()),
VaultError::VersionNotFound(3, "model1".into()),
VaultError::ConversionError("conv fail".into()),
VaultError::UnsupportedFormat("xyz".into()),
VaultError::ConfigError("config fail".into()),
VaultError::SerializationError("serde fail".into()),
VaultError::CompressionError("comp fail".into()),
VaultError::SecurityViolation("sec fail".into()),
VaultError::ComplianceViolation("cc fail".into()),
VaultError::AuditError("audit fail".into()),
VaultError::InvalidInput("bad input".into()),
VaultError::StorageError("store fail".into()),
];
let expected_substrings = [
"crypto fail",
"invalid passphrase",
"integrity fail",
"version fail",
"model1",
"Version 3 not found for model model1",
"conv fail",
"xyz",
"config fail",
"serde fail",
"comp fail",
"sec fail",
"cc fail",
"audit fail",
"bad input",
"store fail",
];
for (err, expected) in errors.iter().zip(expected_substrings.iter()) {
let msg = format!("{}", err);
assert!(
msg.contains(expected),
"Error '{}' should contain '{}'",
msg,
expected
);
}
}
#[test]
fn test_result_type_alias() {
let ok_result: super::Result<i32> = Ok(42);
assert_eq!(ok_result.ok(), Some(42));
let err_result: super::Result<i32> = Err(VaultError::CryptoError("test".into()));
assert!(err_result.is_err());
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let vault_err: VaultError = io_err.into();
match vault_err {
VaultError::IoError(e) => {
assert!(e.to_string().contains("file not found"));
}
_ => panic!("Expected IoError"),
}
}
#[test]
fn test_error_debug_format() {
let err = VaultError::StorageError("db fail".into());
let debug = format!("{:?}", err);
assert!(debug.contains("StorageError"));
}
#[test]
fn test_domain_error_display() {
assert!(CryptoError::KeyDerivation("kdf".into())
.to_string()
.contains("kdf"));
assert!(CryptoError::Encryption("enc".into())
.to_string()
.contains("enc"));
assert!(CryptoError::Decryption("dec".into())
.to_string()
.contains("dec"));
assert!(CryptoError::Integrity("int".into())
.to_string()
.contains("int"));
assert!(CryptoError::Other("oth".into()).to_string().contains("oth"));
let io = StorageError::Io(std::io::Error::other("io"));
assert!(io.to_string().contains("io"));
assert!(StorageError::Serialization("ser".into())
.to_string()
.contains("ser"));
assert!(StorageError::Compression("comp".into())
.to_string()
.contains("comp"));
assert!(StorageError::Database("db".into())
.to_string()
.contains("db"));
assert!(StorageError::Other("oth".into())
.to_string()
.contains("oth"));
assert!(ConversionError::Unsupported("uns".into())
.to_string()
.contains("uns"));
assert!(ConversionError::Validation("val".into())
.to_string()
.contains("val"));
assert!(ConversionError::Other("oth".into())
.to_string()
.contains("oth"));
}
#[test]
fn test_crypto_error_into_vault_error() {
let cases: Vec<(CryptoError, &str)> = vec![
(CryptoError::KeyDerivation("bad salt".into()), "bad salt"),
(CryptoError::Encryption("aes fail".into()), "aes fail"),
(CryptoError::Decryption("wrong key".into()), "wrong key"),
(CryptoError::Other("misc".into()), "misc"),
];
for (crypto_err, expected) in cases {
let vault_err: VaultError = crypto_err.into();
match &vault_err {
VaultError::CryptoError(msg) => assert!(msg.contains(expected)),
_ => panic!("Expected CryptoError, got {:?}", vault_err),
}
}
let integrity = CryptoError::Integrity("hash mismatch".into());
let vault_err: VaultError = integrity.into();
assert!(matches!(vault_err, VaultError::IntegrityError(_)));
}
#[test]
fn test_storage_error_into_vault_error() {
let io_err = StorageError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
assert!(matches!(VaultError::from(io_err), VaultError::IoError(_)));
let ser = StorageError::Serialization("bad json".into());
assert!(matches!(
VaultError::from(ser),
VaultError::SerializationError(_)
));
let comp = StorageError::Compression("zlib".into());
assert!(matches!(
VaultError::from(comp),
VaultError::CompressionError(_)
));
let db = StorageError::Database("sqlite locked".into());
assert!(matches!(VaultError::from(db), VaultError::StorageError(_)));
let other = StorageError::Other("unknown".into());
assert!(matches!(
VaultError::from(other),
VaultError::StorageError(_)
));
}
#[test]
fn test_conversion_error_into_vault_error() {
let unsup = ConversionError::Unsupported("onnx→gguf".into());
assert!(matches!(
VaultError::from(unsup),
VaultError::UnsupportedFormat(_)
));
let val = ConversionError::Validation("shape mismatch".into());
assert!(matches!(
VaultError::from(val),
VaultError::ConversionError(_)
));
let other = ConversionError::Other("misc".into());
assert!(matches!(
VaultError::from(other),
VaultError::ConversionError(_)
));
}
#[test]
fn test_exit_codes_match_the_published_contract() {
let cases: &[(VaultError, u8)] = &[
(VaultError::AuthenticationFailed, 2),
(VaultError::ModelNotFound("m".into()), 3),
(VaultError::VersionNotFound(2, "m".into()), 3),
(VaultError::NotFound("profile 'p'".into()), 3),
(VaultError::SecurityViolation("policy".into()), 4),
(VaultError::IntegrityError("checksum".into()), 5),
(VaultError::InvalidInput("bad".into()), 6),
(VaultError::UnsupportedFormat("xyz".into()), 6),
(VaultError::ConfigError("malformed".into()), 7),
(VaultError::ComplianceViolation("weak".into()), 8),
(VaultError::CryptoError("aead".into()), 1),
(VaultError::VersionError("index".into()), 1),
(VaultError::ConversionError("path".into()), 1),
(VaultError::SerializationError("json".into()), 1),
(VaultError::CompressionError("zstd".into()), 1),
(VaultError::AuditError("log".into()), 1),
(VaultError::StorageError("db".into()), 1),
];
for (err, expected) in cases {
assert_eq!(
err.exit_code(),
*expected,
"{err:?} must exit {expected} — this mapping is a published contract"
);
}
}
#[test]
fn test_permission_denied_io_error_is_distinguished_from_other_io_failures() {
let denied = VaultError::IoError(io::Error::from(io::ErrorKind::PermissionDenied));
assert_eq!(denied.exit_code(), EXIT_PERMISSION);
let not_found = VaultError::IoError(io::Error::from(io::ErrorKind::NotFound));
assert_eq!(not_found.exit_code(), EXIT_GENERAL);
let broken = VaultError::IoError(io::Error::other("disk on fire"));
assert_eq!(broken.exit_code(), EXIT_GENERAL);
}
#[test]
fn test_success_is_the_only_zero_code() {
let all = [
VaultError::AuthenticationFailed,
VaultError::ModelNotFound("m".into()),
VaultError::VersionNotFound(1, "m".into()),
VaultError::NotFound("thing".into()),
VaultError::SecurityViolation("p".into()),
VaultError::IntegrityError("c".into()),
VaultError::InvalidInput("i".into()),
VaultError::UnsupportedFormat("f".into()),
VaultError::ConfigError("c".into()),
VaultError::ComplianceViolation("v".into()),
VaultError::CryptoError("c".into()),
VaultError::VersionError("v".into()),
VaultError::ConversionError("c".into()),
VaultError::IoError(io::Error::other("io")),
VaultError::SerializationError("s".into()),
VaultError::CompressionError("c".into()),
VaultError::AuditError("a".into()),
VaultError::StorageError("s".into()),
];
for err in &all {
assert_ne!(err.exit_code(), EXIT_SUCCESS, "{err:?} must not exit 0");
}
assert_eq!(EXIT_SUCCESS, 0);
}
}