1use std::io;
8use thiserror::Error;
9
10pub type Result<T> = std::result::Result<T, VaultError>;
12
13#[derive(Error, Debug)]
17pub enum CryptoError {
18 #[error("Key derivation failed: {0}")]
20 KeyDerivation(String),
21
22 #[error("Encryption failed: {0}")]
24 Encryption(String),
25
26 #[error("Decryption failed: {0}")]
28 Decryption(String),
29
30 #[error("Integrity check failed: {0}")]
32 Integrity(String),
33
34 #[error("Cryptographic error: {0}")]
36 Other(String),
37}
38
39#[derive(Error, Debug)]
41pub enum StorageError {
42 #[error(transparent)]
44 Io(#[from] io::Error),
45
46 #[error("Serialization error: {0}")]
48 Serialization(String),
49
50 #[error("Compression error: {0}")]
52 Compression(String),
53
54 #[error("Database error: {0}")]
56 Database(String),
57
58 #[error("Storage error: {0}")]
60 Other(String),
61}
62
63#[derive(Error, Debug)]
65pub enum ConversionError {
66 #[error("Unsupported conversion: {0}")]
68 Unsupported(String),
69
70 #[error("Validation failed: {0}")]
72 Validation(String),
73
74 #[error("Conversion error: {0}")]
76 Other(String),
77}
78
79impl From<CryptoError> for VaultError {
82 fn from(err: CryptoError) -> Self {
83 match err {
84 CryptoError::Integrity(msg) => VaultError::IntegrityError(msg),
85 other => VaultError::CryptoError(other.to_string()),
86 }
87 }
88}
89
90impl From<StorageError> for VaultError {
91 fn from(err: StorageError) -> Self {
92 match err {
93 StorageError::Io(e) => VaultError::IoError(e),
94 StorageError::Serialization(msg) => VaultError::SerializationError(msg),
95 StorageError::Compression(msg) => VaultError::CompressionError(msg),
96 StorageError::Database(msg) => VaultError::StorageError(msg),
97 StorageError::Other(msg) => VaultError::StorageError(msg),
98 }
99 }
100}
101
102impl From<ConversionError> for VaultError {
103 fn from(err: ConversionError) -> Self {
104 match err {
105 ConversionError::Unsupported(msg) => VaultError::UnsupportedFormat(msg),
106 other => VaultError::ConversionError(other.to_string()),
107 }
108 }
109}
110
111#[derive(Error, Debug)]
121#[non_exhaustive]
122pub enum VaultError {
123 #[error("Cryptographic error: {0}")]
125 CryptoError(String),
126
127 #[error("Authentication failed: invalid passphrase or corrupted data")]
129 AuthenticationFailed,
130
131 #[error("Integrity check failed: {0}")]
133 IntegrityError(String),
134
135 #[error("Version control error: {0}")]
137 VersionError(String),
138
139 #[error("Model not found: {0}")]
141 ModelNotFound(String),
142
143 #[error("Version {0} not found for model {1}")]
145 VersionNotFound(u32, String),
146
147 #[error("Not found: {0}")]
153 NotFound(String),
154
155 #[error("Format conversion error: {0}")]
157 ConversionError(String),
158
159 #[error("Unsupported model format: {0}")]
161 UnsupportedFormat(String),
162
163 #[error("I/O error: {0}")]
165 IoError(#[from] io::Error),
166
167 #[error("Configuration error: {0}")]
169 ConfigError(String),
170
171 #[error("Serialization error: {0}")]
173 SerializationError(String),
174
175 #[error("Compression error: {0}")]
177 CompressionError(String),
178
179 #[error("Security policy violation: {0}")]
181 SecurityViolation(String),
182
183 #[error("Compliance violation: {0}")]
185 ComplianceViolation(String),
186
187 #[error("Audit log error: {0}")]
189 AuditError(String),
190
191 #[error("Invalid input: {0}")]
193 InvalidInput(String),
194
195 #[error("Storage error: {0}")]
197 StorageError(String),
198}
199
200pub const EXIT_SUCCESS: u8 = 0;
202pub const EXIT_GENERAL: u8 = 1;
204pub const EXIT_AUTH: u8 = 2;
206pub const EXIT_NOT_FOUND: u8 = 3;
208pub const EXIT_PERMISSION: u8 = 4;
210pub const EXIT_INTEGRITY: u8 = 5;
212pub const EXIT_INVALID_INPUT: u8 = 6;
214pub const EXIT_CONFIG: u8 = 7;
216pub const EXIT_COMPLIANCE: u8 = 8;
218
219impl VaultError {
220 #[must_use]
237 pub fn exit_code(&self) -> u8 {
238 match self {
239 VaultError::AuthenticationFailed => EXIT_AUTH,
240
241 VaultError::ModelNotFound(_)
242 | VaultError::VersionNotFound(_, _)
243 | VaultError::NotFound(_) => EXIT_NOT_FOUND,
244
245 VaultError::SecurityViolation(_) => EXIT_PERMISSION,
246 VaultError::IoError(err) if err.kind() == io::ErrorKind::PermissionDenied => {
247 EXIT_PERMISSION
248 }
249
250 VaultError::IntegrityError(_) => EXIT_INTEGRITY,
251
252 VaultError::InvalidInput(_) | VaultError::UnsupportedFormat(_) => EXIT_INVALID_INPUT,
253
254 VaultError::ConfigError(_) => EXIT_CONFIG,
255
256 VaultError::ComplianceViolation(_) => EXIT_COMPLIANCE,
257
258 VaultError::CryptoError(_)
262 | VaultError::VersionError(_)
263 | VaultError::ConversionError(_)
264 | VaultError::IoError(_)
265 | VaultError::SerializationError(_)
266 | VaultError::CompressionError(_)
267 | VaultError::AuditError(_)
268 | VaultError::StorageError(_) => EXIT_GENERAL,
269 }
270 }
271
272 #[must_use]
282 pub fn kind(&self) -> &'static str {
283 match self {
284 VaultError::CryptoError(_) => "crypto",
285 VaultError::AuthenticationFailed => "authentication_failed",
286 VaultError::IntegrityError(_) => "integrity",
287 VaultError::VersionError(_) => "version",
288 VaultError::ModelNotFound(_) => "model_not_found",
289 VaultError::VersionNotFound(_, _) => "version_not_found",
290 VaultError::NotFound(_) => "not_found",
291 VaultError::ConversionError(_) => "conversion",
292 VaultError::UnsupportedFormat(_) => "unsupported_format",
293 VaultError::IoError(_) => "io",
294 VaultError::ConfigError(_) => "config",
295 VaultError::SerializationError(_) => "serialization",
296 VaultError::CompressionError(_) => "compression",
297 VaultError::SecurityViolation(_) => "security_violation",
298 VaultError::InvalidInput(_) => "invalid_input",
299 VaultError::ComplianceViolation(_) => "compliance_violation",
300 VaultError::AuditError(_) => "audit",
301 VaultError::StorageError(_) => "storage",
302 }
303 }
304}
305
306impl From<serde_json::Error> for VaultError {
307 fn from(err: serde_json::Error) -> Self {
308 VaultError::SerializationError(err.to_string())
309 }
310}
311
312impl From<serde_yaml_ng::Error> for VaultError {
313 fn from(err: serde_yaml_ng::Error) -> Self {
314 VaultError::SerializationError(err.to_string())
315 }
316}
317
318impl From<zip::result::ZipError> for VaultError {
319 fn from(err: zip::result::ZipError) -> Self {
320 VaultError::IoError(io::Error::other(err.to_string()))
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_zip_error_conversion() {
330 let zip_err = zip::result::ZipError::FileNotFound;
332 let vault_err: VaultError = zip_err.into();
333 match vault_err {
334 VaultError::IoError(_) => {} _ => panic!("Expected IoError"),
336 }
337 }
338
339 #[test]
340 fn test_serde_yml_error_conversion() {
341 let yaml_err = serde_yaml_ng::from_str::<serde_yaml_ng::Value>("\t").unwrap_err();
342 let vault_err: VaultError = yaml_err.into();
343 match vault_err {
344 VaultError::SerializationError(_) => {}
345 _ => panic!("Expected SerializationError"),
346 }
347 }
348
349 #[test]
350 fn test_serde_json_error_conversion() {
351 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
353 let vault_err: VaultError = json_err.into();
354 match vault_err {
355 VaultError::SerializationError(msg) => {
356 assert!(!msg.is_empty());
357 }
358 _ => panic!("Expected SerializationError"),
359 }
360 }
361
362 #[test]
363 fn test_error_display_messages() {
364 let errors: Vec<VaultError> = vec![
366 VaultError::CryptoError("crypto fail".into()),
367 VaultError::AuthenticationFailed,
368 VaultError::IntegrityError("integrity fail".into()),
369 VaultError::VersionError("version fail".into()),
370 VaultError::ModelNotFound("model1".into()),
371 VaultError::VersionNotFound(3, "model1".into()),
372 VaultError::ConversionError("conv fail".into()),
373 VaultError::UnsupportedFormat("xyz".into()),
374 VaultError::ConfigError("config fail".into()),
375 VaultError::SerializationError("serde fail".into()),
376 VaultError::CompressionError("comp fail".into()),
377 VaultError::SecurityViolation("sec fail".into()),
378 VaultError::ComplianceViolation("cc fail".into()),
379 VaultError::AuditError("audit fail".into()),
380 VaultError::InvalidInput("bad input".into()),
381 VaultError::StorageError("store fail".into()),
382 ];
383
384 let expected_substrings = [
385 "crypto fail",
386 "invalid passphrase",
387 "integrity fail",
388 "version fail",
389 "model1",
390 "Version 3 not found for model model1",
391 "conv fail",
392 "xyz",
393 "config fail",
394 "serde fail",
395 "comp fail",
396 "sec fail",
397 "cc fail",
398 "audit fail",
399 "bad input",
400 "store fail",
401 ];
402
403 for (err, expected) in errors.iter().zip(expected_substrings.iter()) {
404 let msg = format!("{}", err);
405 assert!(
406 msg.contains(expected),
407 "Error '{}' should contain '{}'",
408 msg,
409 expected
410 );
411 }
412 }
413
414 #[test]
415 fn test_result_type_alias() {
416 let ok_result: super::Result<i32> = Ok(42);
418 assert_eq!(ok_result.ok(), Some(42));
419
420 let err_result: super::Result<i32> = Err(VaultError::CryptoError("test".into()));
421 assert!(err_result.is_err());
422 }
423
424 #[test]
425 fn test_io_error_conversion() {
426 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
428 let vault_err: VaultError = io_err.into();
429 match vault_err {
430 VaultError::IoError(e) => {
431 assert!(e.to_string().contains("file not found"));
432 }
433 _ => panic!("Expected IoError"),
434 }
435 }
436
437 #[test]
438 fn test_error_debug_format() {
439 let err = VaultError::StorageError("db fail".into());
441 let debug = format!("{:?}", err);
442 assert!(debug.contains("StorageError"));
443 }
444
445 #[test]
446 fn test_domain_error_display() {
447 assert!(CryptoError::KeyDerivation("kdf".into())
449 .to_string()
450 .contains("kdf"));
451 assert!(CryptoError::Encryption("enc".into())
452 .to_string()
453 .contains("enc"));
454 assert!(CryptoError::Decryption("dec".into())
455 .to_string()
456 .contains("dec"));
457 assert!(CryptoError::Integrity("int".into())
458 .to_string()
459 .contains("int"));
460 assert!(CryptoError::Other("oth".into()).to_string().contains("oth"));
461
462 let io = StorageError::Io(std::io::Error::other("io"));
464 assert!(io.to_string().contains("io"));
465 assert!(StorageError::Serialization("ser".into())
466 .to_string()
467 .contains("ser"));
468 assert!(StorageError::Compression("comp".into())
469 .to_string()
470 .contains("comp"));
471 assert!(StorageError::Database("db".into())
472 .to_string()
473 .contains("db"));
474 assert!(StorageError::Other("oth".into())
475 .to_string()
476 .contains("oth"));
477
478 assert!(ConversionError::Unsupported("uns".into())
480 .to_string()
481 .contains("uns"));
482 assert!(ConversionError::Validation("val".into())
483 .to_string()
484 .contains("val"));
485 assert!(ConversionError::Other("oth".into())
486 .to_string()
487 .contains("oth"));
488 }
489
490 #[test]
493 fn test_crypto_error_into_vault_error() {
494 let cases: Vec<(CryptoError, &str)> = vec![
495 (CryptoError::KeyDerivation("bad salt".into()), "bad salt"),
496 (CryptoError::Encryption("aes fail".into()), "aes fail"),
497 (CryptoError::Decryption("wrong key".into()), "wrong key"),
498 (CryptoError::Other("misc".into()), "misc"),
499 ];
500 for (crypto_err, expected) in cases {
501 let vault_err: VaultError = crypto_err.into();
502 match &vault_err {
503 VaultError::CryptoError(msg) => assert!(msg.contains(expected)),
504 _ => panic!("Expected CryptoError, got {:?}", vault_err),
505 }
506 }
507
508 let integrity = CryptoError::Integrity("hash mismatch".into());
510 let vault_err: VaultError = integrity.into();
511 assert!(matches!(vault_err, VaultError::IntegrityError(_)));
512 }
513
514 #[test]
515 fn test_storage_error_into_vault_error() {
516 let io_err = StorageError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
517 assert!(matches!(VaultError::from(io_err), VaultError::IoError(_)));
518
519 let ser = StorageError::Serialization("bad json".into());
520 assert!(matches!(
521 VaultError::from(ser),
522 VaultError::SerializationError(_)
523 ));
524
525 let comp = StorageError::Compression("zlib".into());
526 assert!(matches!(
527 VaultError::from(comp),
528 VaultError::CompressionError(_)
529 ));
530
531 let db = StorageError::Database("sqlite locked".into());
532 assert!(matches!(VaultError::from(db), VaultError::StorageError(_)));
533
534 let other = StorageError::Other("unknown".into());
535 assert!(matches!(
536 VaultError::from(other),
537 VaultError::StorageError(_)
538 ));
539 }
540
541 #[test]
542 fn test_conversion_error_into_vault_error() {
543 let unsup = ConversionError::Unsupported("onnx→gguf".into());
544 assert!(matches!(
545 VaultError::from(unsup),
546 VaultError::UnsupportedFormat(_)
547 ));
548
549 let val = ConversionError::Validation("shape mismatch".into());
550 assert!(matches!(
551 VaultError::from(val),
552 VaultError::ConversionError(_)
553 ));
554
555 let other = ConversionError::Other("misc".into());
556 assert!(matches!(
557 VaultError::from(other),
558 VaultError::ConversionError(_)
559 ));
560 }
561
562 #[test]
568 fn test_exit_codes_match_the_published_contract() {
569 let cases: &[(VaultError, u8)] = &[
570 (VaultError::AuthenticationFailed, 2),
571 (VaultError::ModelNotFound("m".into()), 3),
572 (VaultError::VersionNotFound(2, "m".into()), 3),
573 (VaultError::NotFound("profile 'p'".into()), 3),
574 (VaultError::SecurityViolation("policy".into()), 4),
575 (VaultError::IntegrityError("checksum".into()), 5),
576 (VaultError::InvalidInput("bad".into()), 6),
577 (VaultError::UnsupportedFormat("xyz".into()), 6),
578 (VaultError::ConfigError("malformed".into()), 7),
579 (VaultError::ComplianceViolation("weak".into()), 8),
580 (VaultError::CryptoError("aead".into()), 1),
581 (VaultError::VersionError("index".into()), 1),
582 (VaultError::ConversionError("path".into()), 1),
583 (VaultError::SerializationError("json".into()), 1),
584 (VaultError::CompressionError("zstd".into()), 1),
585 (VaultError::AuditError("log".into()), 1),
586 (VaultError::StorageError("db".into()), 1),
587 ];
588
589 for (err, expected) in cases {
590 assert_eq!(
591 err.exit_code(),
592 *expected,
593 "{err:?} must exit {expected} — this mapping is a published contract"
594 );
595 }
596 }
597
598 #[test]
599 fn test_permission_denied_io_error_is_distinguished_from_other_io_failures() {
600 let denied = VaultError::IoError(io::Error::from(io::ErrorKind::PermissionDenied));
603 assert_eq!(denied.exit_code(), EXIT_PERMISSION);
604
605 let not_found = VaultError::IoError(io::Error::from(io::ErrorKind::NotFound));
606 assert_eq!(not_found.exit_code(), EXIT_GENERAL);
607
608 let broken = VaultError::IoError(io::Error::other("disk on fire"));
609 assert_eq!(broken.exit_code(), EXIT_GENERAL);
610 }
611
612 #[test]
613 fn test_success_is_the_only_zero_code() {
614 let all = [
617 VaultError::AuthenticationFailed,
618 VaultError::ModelNotFound("m".into()),
619 VaultError::VersionNotFound(1, "m".into()),
620 VaultError::NotFound("thing".into()),
621 VaultError::SecurityViolation("p".into()),
622 VaultError::IntegrityError("c".into()),
623 VaultError::InvalidInput("i".into()),
624 VaultError::UnsupportedFormat("f".into()),
625 VaultError::ConfigError("c".into()),
626 VaultError::ComplianceViolation("v".into()),
627 VaultError::CryptoError("c".into()),
628 VaultError::VersionError("v".into()),
629 VaultError::ConversionError("c".into()),
630 VaultError::IoError(io::Error::other("io")),
631 VaultError::SerializationError("s".into()),
632 VaultError::CompressionError("c".into()),
633 VaultError::AuditError("a".into()),
634 VaultError::StorageError("s".into()),
635 ];
636 for err in &all {
637 assert_ne!(err.exit_code(), EXIT_SUCCESS, "{err:?} must not exit 0");
638 }
639 assert_eq!(EXIT_SUCCESS, 0);
640 }
641}