pack_io/error.rs
1//! Error type for the codec.
2//!
3//! `pack-io` uses a single, `#[non_exhaustive]` error enum that covers every
4//! failure mode of both encode and decode. The encode side is infallible for
5//! sized in-memory values today; it remains fallible at the type level so the
6//! streaming API can wrap the underlying `Write` failure mode in `0.3`.
7//!
8//! The variants are kept small and concrete: each one names a single failure
9//! mode and carries the smallest amount of context needed to act on it. None
10//! of them include the malformed bytes themselves — error messages from the
11//! codec never echo untrusted input back into a log line.
12
13use core::fmt;
14
15/// Every error returned by the codec.
16///
17/// `#[non_exhaustive]` so additional variants can be added in a MINOR release
18/// without breaking downstream `match` arms. Callers MUST include a wildcard
19/// arm.
20///
21/// # Examples
22///
23/// ```
24/// use pack_io::{decode, SerialError};
25///
26/// // A length prefix that runs off the end of the buffer is rejected, not
27/// // accepted-and-corrected.
28/// let bad: &[u8] = &[0xff, 0xff, 0xff, 0xff, 0x0f]; // varint = u32::MAX
29/// match decode::<String>(bad) {
30/// Ok(_) => unreachable!("hostile length should not decode"),
31/// Err(SerialError::InvalidLength { .. })
32/// | Err(SerialError::UnexpectedEof { .. }) => {} // expected
33/// Err(other) => panic!("unexpected error variant: {other}"),
34/// }
35/// ```
36#[non_exhaustive]
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum SerialError {
39 /// The decoder needed more bytes than the input contained.
40 ///
41 /// `needed` is the number of additional bytes the codec required to make
42 /// progress; `remaining` is what was actually left in the buffer.
43 UnexpectedEof {
44 /// Number of bytes the codec required to make progress.
45 needed: usize,
46 /// Number of bytes still available in the input when the read failed.
47 remaining: usize,
48 },
49
50 /// A length prefix declared a value larger than the buffer can hold.
51 ///
52 /// This is the primary defence against a hostile length-prefix attack:
53 /// the decoder refuses to allocate or read past the available input.
54 InvalidLength {
55 /// The length declared by the prefix, in bytes.
56 declared: u64,
57 /// Bytes remaining in the input when the prefix was read.
58 remaining: usize,
59 },
60
61 /// A LEB128 varint exceeded the maximum legal byte count for its target
62 /// width (10 bytes for `u64`, 5 for `u32`, 19 for `u128`, etc.).
63 VarintOverflow,
64
65 /// A decoded varint did not fit in the requested integer width
66 /// (e.g. `u64` decoded successfully but the target was `u32`).
67 IntegerOutOfRange,
68
69 /// A boolean byte was neither `0x00` nor `0x01`.
70 InvalidBool {
71 /// The offending byte. Kept so the caller can log a sanitised summary
72 /// (`{:02x}`) without echoing the surrounding payload.
73 byte: u8,
74 },
75
76 /// A length-prefixed byte run was not valid UTF-8 when decoding a
77 /// `String`.
78 InvalidUtf8,
79
80 /// A tag byte for `Option` (`0x00` / `0x01`) or `Result` (`0x00` / `0x01`)
81 /// was outside the legal range.
82 InvalidTag {
83 /// Name of the type that owns this tag (`"Option"`, `"Result"`).
84 kind: &'static str,
85 /// The offending tag byte.
86 tag: u8,
87 },
88
89 /// The input buffer contained trailing bytes after a strict decode
90 /// completed. Returned only by [`crate::decode`], which requires the
91 /// payload to be fully consumed.
92 TrailingBytes {
93 /// Number of bytes left over after the value was decoded.
94 remaining: usize,
95 },
96
97 /// An underlying `std::io::Write` / `std::io::Read` operation failed
98 /// while a streaming codec was in flight. Returned only by the
99 /// `std`-gated I/O integration (`IoEncoder`, `IoDecoder`,
100 /// `encode_into`, `decode_from`).
101 ///
102 /// The error kind and a stringified message are captured so the variant
103 /// remains `Clone + Eq`. The original `std::io::Error` is not preserved
104 /// — log the captured `message` field for diagnostics.
105 #[cfg(feature = "std")]
106 Io {
107 /// Classification of the underlying I/O failure.
108 kind: std::io::ErrorKind,
109 /// Human-readable rendering of the original `std::io::Error`.
110 message: alloc::string::String,
111 },
112}
113
114impl fmt::Display for SerialError {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 Self::UnexpectedEof { needed, remaining } => write!(
118 f,
119 "unexpected end of input: needed {needed} more byte(s), {remaining} remaining"
120 ),
121 Self::InvalidLength {
122 declared,
123 remaining,
124 } => write!(
125 f,
126 "length prefix exceeds remaining buffer: declared {declared}, remaining {remaining}"
127 ),
128 Self::VarintOverflow => {
129 f.write_str("varint exceeds the maximum byte count for its target width")
130 }
131 Self::IntegerOutOfRange => {
132 f.write_str("decoded integer does not fit in the requested width")
133 }
134 Self::InvalidBool { byte } => write!(f, "invalid boolean byte: 0x{byte:02x}"),
135 Self::InvalidUtf8 => f.write_str("length-prefixed bytes were not valid UTF-8"),
136 Self::InvalidTag { kind, tag } => write!(f, "invalid {kind} tag: 0x{tag:02x}"),
137 Self::TrailingBytes { remaining } => {
138 write!(
139 f,
140 "trailing input after strict decode: {remaining} byte(s) unread"
141 )
142 }
143 #[cfg(feature = "std")]
144 Self::Io { kind, message } => write!(f, "I/O error ({kind:?}): {message}"),
145 }
146 }
147}
148
149#[cfg(feature = "std")]
150impl std::error::Error for SerialError {}
151
152/// Convenience alias for `Result<T, SerialError>`.
153///
154/// Used throughout the codec so the trait surface stays terse. Crates that
155/// implement `Serialize` / `Deserialize` for their own types are encouraged to
156/// use it as well; nothing in the public API requires it.
157///
158/// # Examples
159///
160/// ```
161/// use pack_io::Result;
162///
163/// fn parse_header(_bytes: &[u8]) -> Result<u32> {
164/// Ok(0)
165/// }
166/// ```
167pub type Result<T> = core::result::Result<T, SerialError>;
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use alloc::format;
173 use alloc::string::ToString;
174
175 #[test]
176 fn display_unexpected_eof_reports_counts() {
177 let err = SerialError::UnexpectedEof {
178 needed: 4,
179 remaining: 1,
180 };
181 let msg = err.to_string();
182 assert!(msg.contains("needed 4"));
183 assert!(msg.contains("1 remaining"));
184 }
185
186 #[test]
187 fn display_invalid_length_reports_declared_and_remaining() {
188 let err = SerialError::InvalidLength {
189 declared: 1 << 20,
190 remaining: 16,
191 };
192 let msg = err.to_string();
193 assert!(msg.contains("1048576"));
194 assert!(msg.contains("16"));
195 }
196
197 #[test]
198 fn display_invalid_bool_is_hex_with_zero_pad() {
199 let err = SerialError::InvalidBool { byte: 0x2a };
200 assert_eq!(err.to_string(), "invalid boolean byte: 0x2a");
201 }
202
203 #[test]
204 fn display_invalid_tag_carries_kind_and_byte() {
205 let err = SerialError::InvalidTag {
206 kind: "Option",
207 tag: 0x7f,
208 };
209 assert!(err.to_string().contains("Option"));
210 assert!(err.to_string().contains("0x7f"));
211 }
212
213 #[test]
214 fn equality_distinguishes_variants() {
215 let a = SerialError::VarintOverflow;
216 let b = SerialError::VarintOverflow;
217 let c = SerialError::IntegerOutOfRange;
218 assert_eq!(a, b);
219 assert_ne!(a, c);
220 }
221
222 #[test]
223 fn clone_preserves_variant() {
224 let err = SerialError::TrailingBytes { remaining: 8 };
225 let cloned = err.clone();
226 assert_eq!(err, cloned);
227 }
228
229 #[test]
230 fn debug_format_does_not_panic() {
231 // We never put untrusted bytes into Debug, so it's safe to print.
232 let _ = format!("{:?}", SerialError::InvalidUtf8);
233 }
234}