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    /// 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}