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 /// A decoded enum variant index did not correspond to any known variant
90 /// of the target type. Produced by the `#[derive(Deserialize)]` /
91 /// `#[derive(DeserializeView)]` enum deserialisers in `pack-io-derive`.
92 ///
93 /// `kind` is the enum's type name; `index` is the offending varint
94 /// value (read as `u64` so any overflow case is representable).
95 UnknownVariant {
96 /// Name of the enum that was being decoded.
97 kind: &'static str,
98 /// The offending varint variant index.
99 index: u64,
100 },
101
102 /// The input buffer contained trailing bytes after a strict decode
103 /// completed. Returned only by [`crate::decode`], which requires the
104 /// payload to be fully consumed.
105 TrailingBytes {
106 /// Number of bytes left over after the value was decoded.
107 remaining: usize,
108 },
109
110 /// An underlying `std::io::Write` / `std::io::Read` operation failed
111 /// while a streaming codec was in flight. Returned only by the
112 /// `std`-gated I/O integration (`IoEncoder`, `IoDecoder`,
113 /// `encode_into`, `decode_from`).
114 ///
115 /// The error kind and a stringified message are captured so the variant
116 /// remains `Clone + Eq`. The original `std::io::Error` is not preserved
117 /// — log the captured `message` field for diagnostics.
118 #[cfg(feature = "std")]
119 Io {
120 /// Classification of the underlying I/O failure.
121 kind: std::io::ErrorKind,
122 /// Human-readable rendering of the original `std::io::Error`.
123 message: alloc::string::String,
124 },
125}
126
127impl fmt::Display for SerialError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Self::UnexpectedEof { needed, remaining } => write!(
131 f,
132 "unexpected end of input: needed {needed} more byte(s), {remaining} remaining"
133 ),
134 Self::InvalidLength {
135 declared,
136 remaining,
137 } => write!(
138 f,
139 "length prefix exceeds remaining buffer: declared {declared}, remaining {remaining}"
140 ),
141 Self::VarintOverflow => {
142 f.write_str("varint exceeds the maximum byte count for its target width")
143 }
144 Self::IntegerOutOfRange => {
145 f.write_str("decoded integer does not fit in the requested width")
146 }
147 Self::InvalidBool { byte } => write!(f, "invalid boolean byte: 0x{byte:02x}"),
148 Self::InvalidUtf8 => f.write_str("length-prefixed bytes were not valid UTF-8"),
149 Self::InvalidTag { kind, tag } => write!(f, "invalid {kind} tag: 0x{tag:02x}"),
150 Self::UnknownVariant { kind, index } => {
151 write!(f, "unknown {kind} variant index: {index}")
152 }
153 Self::TrailingBytes { remaining } => {
154 write!(
155 f,
156 "trailing input after strict decode: {remaining} byte(s) unread"
157 )
158 }
159 #[cfg(feature = "std")]
160 Self::Io { kind, message } => write!(f, "I/O error ({kind:?}): {message}"),
161 }
162 }
163}
164
165#[cfg(feature = "std")]
166impl std::error::Error for SerialError {}
167
168/// Convenience alias for `Result<T, SerialError>`.
169///
170/// Used throughout the codec so the trait surface stays terse. Crates that
171/// implement `Serialize` / `Deserialize` for their own types are encouraged to
172/// use it as well; nothing in the public API requires it.
173///
174/// # Examples
175///
176/// ```
177/// use pack_io::Result;
178///
179/// fn parse_header(_bytes: &[u8]) -> Result<u32> {
180/// Ok(0)
181/// }
182/// ```
183pub type Result<T> = core::result::Result<T, SerialError>;
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use alloc::format;
189 use alloc::string::ToString;
190
191 #[test]
192 fn display_unexpected_eof_reports_counts() {
193 let err = SerialError::UnexpectedEof {
194 needed: 4,
195 remaining: 1,
196 };
197 let msg = err.to_string();
198 assert!(msg.contains("needed 4"));
199 assert!(msg.contains("1 remaining"));
200 }
201
202 #[test]
203 fn display_invalid_length_reports_declared_and_remaining() {
204 let err = SerialError::InvalidLength {
205 declared: 1 << 20,
206 remaining: 16,
207 };
208 let msg = err.to_string();
209 assert!(msg.contains("1048576"));
210 assert!(msg.contains("16"));
211 }
212
213 #[test]
214 fn display_invalid_bool_is_hex_with_zero_pad() {
215 let err = SerialError::InvalidBool { byte: 0x2a };
216 assert_eq!(err.to_string(), "invalid boolean byte: 0x2a");
217 }
218
219 #[test]
220 fn display_invalid_tag_carries_kind_and_byte() {
221 let err = SerialError::InvalidTag {
222 kind: "Option",
223 tag: 0x7f,
224 };
225 assert!(err.to_string().contains("Option"));
226 assert!(err.to_string().contains("0x7f"));
227 }
228
229 #[test]
230 fn equality_distinguishes_variants() {
231 let a = SerialError::VarintOverflow;
232 let b = SerialError::VarintOverflow;
233 let c = SerialError::IntegerOutOfRange;
234 assert_eq!(a, b);
235 assert_ne!(a, c);
236 }
237
238 #[test]
239 fn clone_preserves_variant() {
240 let err = SerialError::TrailingBytes { remaining: 8 };
241 let cloned = err.clone();
242 assert_eq!(err, cloned);
243 }
244
245 #[test]
246 fn debug_format_does_not_panic() {
247 // We never put untrusted bytes into Debug, so it's safe to print.
248 let _ = format!("{:?}", SerialError::InvalidUtf8);
249 }
250}