Skip to main content

jugar_apr/
error.rs

1//! Error types for APR model handling.
2//!
3//! Per spec Section 4.1: Errors during model loading/saving.
4
5use thiserror::Error;
6
7/// Errors that can occur when working with APR models
8#[derive(Debug, Error)]
9pub enum AprError {
10    /// Invalid magic number (not "APNR")
11    #[error("Invalid APR file: expected magic 'APNR', got {found:?}")]
12    InvalidMagic {
13        /// The bytes found instead of "APNR"
14        found: [u8; 4],
15    },
16
17    /// Unsupported version
18    #[error("Unsupported APR version: {version} (supported: {min_supported}+)")]
19    UnsupportedVersion {
20        /// Version found in file
21        version: u16,
22        /// Minimum supported version
23        min_supported: u16,
24    },
25
26    /// Checksum mismatch
27    #[error("APR file corrupted: checksum mismatch (expected {expected:08x}, got {computed:08x})")]
28    ChecksumMismatch {
29        /// Expected checksum from header
30        expected: u32,
31        /// Computed checksum from data
32        computed: u32,
33    },
34
35    /// Model too large
36    #[error("Model too large: {size} bytes (max: {max} bytes)")]
37    ModelTooLarge {
38        /// Actual size
39        size: usize,
40        /// Maximum allowed size
41        max: usize,
42    },
43
44    /// File too small
45    #[error("APR file too small: {size} bytes (minimum header: 10 bytes)")]
46    FileTooSmall {
47        /// Actual size
48        size: usize,
49    },
50
51    /// Invalid metadata name
52    #[error("Invalid model name: '{name}' (must be 3-50 alphanumeric/hyphen chars)")]
53    InvalidName {
54        /// The invalid name
55        name: String,
56    },
57
58    /// Invalid semver version
59    #[error("Invalid version string: {version}")]
60    InvalidVersion {
61        /// The invalid version
62        version: String,
63    },
64
65    /// CBOR encoding error
66    #[error("CBOR encoding error: {0}")]
67    CborEncode(String),
68
69    /// CBOR decoding error
70    #[error("CBOR decoding error: {0}")]
71    CborDecode(String),
72
73    /// Compression error
74    #[error("Compression error: {0}")]
75    Compression(String),
76
77    /// Decompression error
78    #[error("Decompression error: {0}")]
79    Decompression(String),
80
81    /// Unknown builtin model
82    #[error("Unknown builtin model: '{name}' (available: chase, patrol, wander)")]
83    UnknownBuiltin {
84        /// The requested builtin name
85        name: String,
86    },
87
88    /// Missing required field in metadata
89    #[error("Missing required field: {field}")]
90    MissingField {
91        /// The missing field name
92        field: &'static str,
93    },
94
95    /// I/O error
96    #[error("I/O error: {0}")]
97    Io(#[from] std::io::Error),
98}
99
100impl AprError {
101    /// Create an invalid magic error from bytes
102    #[must_use]
103    pub fn invalid_magic(bytes: &[u8]) -> Self {
104        let mut found = [0u8; 4];
105        let len = bytes.len().min(4);
106        found[..len].copy_from_slice(&bytes[..len]);
107        Self::InvalidMagic { found }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_error_display() {
117        let err = AprError::InvalidMagic { found: *b"BAAD" };
118        assert!(err.to_string().contains("APNR"));
119    }
120
121    #[test]
122    fn test_checksum_error_hex_format() {
123        let err = AprError::ChecksumMismatch {
124            expected: 0xDEAD_BEEF,
125            computed: 0xCAFE_BABE,
126        };
127        let msg = err.to_string();
128        assert!(msg.contains("deadbeef"));
129        assert!(msg.contains("cafebabe"));
130    }
131
132    #[test]
133    fn test_model_too_large_error() {
134        let err = AprError::ModelTooLarge {
135            size: 2_000_000,
136            max: 1_048_576,
137        };
138        assert!(err.to_string().contains("2000000"));
139        assert!(err.to_string().contains("1048576"));
140    }
141}