apr_format/error.rs
1//! Error type for the sovereign `apr-format` leaf crate.
2//!
3//! # Wrapper error seam (issue #2231, locked decision 3)
4//!
5//! `apr-format` owns its own `AprFormatError`. It deliberately does **not**
6//! share an error crate with `aprender-core`. Instead, `aprender-core` provides
7//! `impl From<AprFormatError> for AprenderError` so the leaf's errors wrap
8//! transparently into the framework's error type at the crate boundary. This
9//! keeps the leaf sovereign (no dependency back on the framework's error type)
10//! while preserving `?`-ergonomics for core consumers.
11
12/// Errors produced while reading or writing the `.apr` container format.
13///
14/// `#[non_exhaustive]` so new variants can be added without a breaking change.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum AprFormatError {
18 /// CRC32 integrity check failed: the stored trailer does not match the
19 /// recomputed checksum of the file body.
20 #[error("Checksum mismatch: expected 0x{expected:08X}, got 0x{actual:08X}")]
21 ChecksumMismatch {
22 /// Checksum stored in the file trailer.
23 expected: u32,
24 /// Checksum recomputed over the file body.
25 actual: u32,
26 },
27
28 /// Invalid or corrupt container structure (bad magic, truncated header,
29 /// unknown model type, out-of-range sizes, …).
30 #[error("Invalid model format: {message}")]
31 FormatError {
32 /// Human-readable description of the format violation.
33 message: String,
34 },
35
36 /// Serialization / deserialization failure (bincode, msgpack, JSON).
37 #[error("Serialization error: {0}")]
38 Serialization(String),
39
40 /// Poka-yoke / Jidoka validation failure (e.g. refusing to save a model
41 /// whose quality gate scored zero).
42 #[error("Validation failed: {message}")]
43 ValidationError {
44 /// Description of the validation failure.
45 message: String,
46 },
47
48 /// Underlying I/O failure (file open / read / write).
49 #[error("I/O error: {0}")]
50 Io(#[from] std::io::Error),
51
52 /// A computed offset fell outside the file/data boundary.
53 #[error("Invalid offset: out of bounds for the supplied buffer")]
54 InvalidOffset,
55
56 /// The declared header / metadata section exceeds the permitted maximum
57 /// (compression-bomb / overflow protection).
58 #[error("Header or metadata section too large")]
59 HeaderTooLarge,
60
61 /// The on-disk format version is newer than this reader supports.
62 #[error("Unsupported format version: found {}.{}, max supported {}.{}", found.0, found.1, supported.0, supported.1)]
63 UnsupportedVersion {
64 /// Version found in the file header.
65 found: (u8, u8),
66 /// Maximum version this reader supports.
67 supported: (u8, u8),
68 },
69}
70
71/// Convenience alias for results in this crate.
72pub type Result<T> = std::result::Result<T, AprFormatError>;
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn test_checksum_mismatch_display() {
80 let e = AprFormatError::ChecksumMismatch {
81 expected: 0xDEAD_BEEF,
82 actual: 0xCAFE_BABE,
83 };
84 let s = e.to_string();
85 assert!(s.contains("Checksum mismatch"));
86 assert!(s.contains("DEADBEEF"));
87 }
88
89 #[test]
90 fn test_format_error_display() {
91 let e = AprFormatError::FormatError {
92 message: "corrupt header".to_string(),
93 };
94 assert!(e.to_string().contains("corrupt header"));
95 }
96
97 #[test]
98 fn test_io_from() {
99 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "nope");
100 let e: AprFormatError = io.into();
101 assert!(matches!(e, AprFormatError::Io(_)));
102 }
103
104 #[test]
105 fn test_unsupported_version_display() {
106 let e = AprFormatError::UnsupportedVersion {
107 found: (3, 0),
108 supported: (1, 0),
109 };
110 assert!(e.to_string().contains("3.0"));
111 }
112}