Skip to main content

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}