Skip to main content

oxicode/
error.rs

1//! Error types for OxiCode
2
3use core::fmt;
4
5/// Result type alias for OxiCode operations
6pub type Result<T> = core::result::Result<T, Error>;
7
8/// Error type for encoding and decoding operations
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum Error {
12    /// Unexpected end of input during decoding
13    UnexpectedEnd {
14        /// Estimate of additional bytes needed
15        additional: usize,
16    },
17
18    /// Invalid data encountered during decoding
19    InvalidData {
20        /// Description of what went wrong
21        message: &'static str,
22    },
23
24    /// Invalid integer type during varint decoding
25    InvalidIntegerType {
26        /// The type that was expected
27        expected: IntegerType,
28        /// The type that was found
29        found: IntegerType,
30    },
31
32    /// Invalid boolean value
33    InvalidBooleanValue(u8),
34
35    /// Invalid character encoding
36    InvalidCharEncoding([u8; 4]),
37
38    /// UTF-8 decoding error
39    #[cfg(feature = "alloc")]
40    Utf8 {
41        /// The inner UTF-8 error
42        inner: core::str::Utf8Error,
43    },
44
45    /// Configuration limit exceeded
46    LimitExceeded {
47        /// The limit that was configured
48        limit: u64,
49        /// The value that exceeded the limit
50        found: u64,
51    },
52
53    /// IO error during encoding or decoding
54    #[cfg(feature = "std")]
55    Io {
56        /// Error kind
57        kind: std::io::ErrorKind,
58        /// Error message
59        message: String,
60    },
61
62    /// Custom error message (static string)
63    Custom {
64        /// Error message
65        message: &'static str,
66    },
67
68    /// Custom error message (owned string, from serde or dynamic sources)
69    #[cfg(feature = "alloc")]
70    OwnedCustom {
71        /// Error message
72        message: alloc::string::String,
73    },
74
75    /// Value outside usize range (for platforms with smaller usize)
76    OutsideUsizeRange(u64),
77
78    /// NonZero type decoded as zero
79    NonZeroTypeIsZero {
80        /// The NonZero type that was zero
81        non_zero_type: IntegerType,
82    },
83
84    /// Invalid enum variant
85    UnexpectedVariant {
86        /// The variant index found
87        found: u32,
88        /// Type name for context
89        type_name: &'static str,
90    },
91
92    /// Invalid duration (nanos >= 1_000_000_000)
93    ///
94    /// Reserved: this variant is not yet constructed by the current decoding
95    /// paths (which report out-of-range `Duration` values via
96    /// [`Error::InvalidData`] instead). It is kept public and documented so
97    /// that future wire-compatibility work can switch those call sites to a
98    /// structured error without a breaking enum change.
99    #[cfg(feature = "std")]
100    InvalidDuration {
101        /// Seconds component
102        secs: u64,
103        /// Nanoseconds component (should be < 1_000_000_000)
104        nanos: u32,
105    },
106
107    /// Invalid SystemTime (before UNIX_EPOCH)
108    ///
109    /// Reserved: this variant is not yet constructed by the current decoding
110    /// paths (which report out-of-range `SystemTime` values via
111    /// [`Error::InvalidData`] instead). It is kept public and documented so
112    /// that future wire-compatibility work can switch those call sites to a
113    /// structured error without a breaking enum change.
114    #[cfg(feature = "std")]
115    InvalidSystemTime {
116        /// Duration before UNIX_EPOCH
117        duration: std::time::Duration,
118    },
119
120    /// Checksum mismatch during data verification
121    #[cfg(feature = "checksum")]
122    ChecksumMismatch {
123        /// Expected CRC32 checksum
124        expected: u32,
125        /// Found CRC32 checksum
126        found: u32,
127    },
128}
129
130/// Integer type enumeration for better error messages
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum IntegerType {
134    /// u8 type
135    U8,
136    /// u16 type
137    U16,
138    /// u32 type
139    U32,
140    /// u64 type
141    U64,
142    /// u128 type
143    U128,
144    /// usize type
145    Usize,
146    /// i8 type
147    I8,
148    /// i16 type
149    I16,
150    /// i32 type
151    I32,
152    /// i64 type
153    I64,
154    /// i128 type
155    I128,
156    /// isize type
157    Isize,
158    /// Reserved/unknown type
159    Reserved,
160}
161
162impl IntegerType {
163    /// Convert unsigned type to corresponding signed type
164    #[allow(dead_code)]
165    pub(crate) const fn into_signed(self) -> Self {
166        match self {
167            Self::U8 => Self::I8,
168            Self::U16 => Self::I16,
169            Self::U32 => Self::I32,
170            Self::U64 => Self::I64,
171            Self::U128 => Self::I128,
172            Self::Usize => Self::Isize,
173            other => other,
174        }
175    }
176}
177
178impl fmt::Display for IntegerType {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            Self::U8 => write!(f, "u8"),
182            Self::U16 => write!(f, "u16"),
183            Self::U32 => write!(f, "u32"),
184            Self::U64 => write!(f, "u64"),
185            Self::U128 => write!(f, "u128"),
186            Self::Usize => write!(f, "usize"),
187            Self::I8 => write!(f, "i8"),
188            Self::I16 => write!(f, "i16"),
189            Self::I32 => write!(f, "i32"),
190            Self::I64 => write!(f, "i64"),
191            Self::I128 => write!(f, "i128"),
192            Self::Isize => write!(f, "isize"),
193            Self::Reserved => write!(f, "reserved"),
194        }
195    }
196}
197
198impl fmt::Display for Error {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            Error::UnexpectedEnd { additional } => {
202                write!(
203                    f,
204                    "Unexpected end of input (need {} more bytes)",
205                    additional
206                )
207            }
208            Error::InvalidData { message } => write!(f, "Invalid data: {}", message),
209            Error::InvalidIntegerType { expected, found } => {
210                write!(
211                    f,
212                    "Invalid integer type: expected {}, found {}",
213                    expected, found
214                )
215            }
216            Error::InvalidBooleanValue(v) => write!(f, "Invalid boolean value: {}", v),
217            Error::InvalidCharEncoding(bytes) => {
218                write!(f, "Invalid char encoding: {:?}", bytes)
219            }
220            #[cfg(feature = "alloc")]
221            Error::Utf8 { inner } => write!(
222                f,
223                "UTF-8 error at byte offset {}: {}",
224                inner.valid_up_to(),
225                inner
226            ),
227            Error::LimitExceeded { limit, found } => {
228                write!(f, "limit exceeded: found {} but limit is {}", found, limit)
229            }
230            #[cfg(feature = "std")]
231            Error::Io { kind, message } => write!(f, "IO error ({:?}): {}", kind, message),
232            Error::Custom { message } => write!(f, "{}", message),
233            #[cfg(feature = "alloc")]
234            Error::OwnedCustom { message } => write!(f, "{}", message),
235            Error::OutsideUsizeRange(v) => {
236                write!(f, "Value {} outside usize range", v)
237            }
238            Error::NonZeroTypeIsZero { non_zero_type } => {
239                write!(f, "NonZero{} type decoded as zero", non_zero_type)
240            }
241            Error::UnexpectedVariant { found, type_name } => {
242                write!(
243                    f,
244                    "unexpected variant for type `{}`: found discriminant {}",
245                    type_name, found
246                )
247            }
248            #[cfg(feature = "std")]
249            Error::InvalidDuration { secs, nanos } => {
250                write!(
251                    f,
252                    "Invalid duration: {} seconds, {} nanoseconds (nanos must be < 1,000,000,000)",
253                    secs, nanos
254                )
255            }
256            #[cfg(feature = "std")]
257            Error::InvalidSystemTime { duration } => {
258                write!(f, "Invalid SystemTime: {:?} before UNIX_EPOCH", duration)
259            }
260            #[cfg(feature = "checksum")]
261            Error::ChecksumMismatch { expected, found } => {
262                write!(
263                    f,
264                    "Checksum mismatch: expected 0x{:08x}, found 0x{:08x}",
265                    expected, found
266                )
267            }
268        }
269    }
270}
271
272#[cfg(feature = "std")]
273impl std::error::Error for Error {
274    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
275        match self {
276            // `std` implies `alloc` (see Cargo.toml), so the `Utf8` variant
277            // is always available whenever this impl is compiled.
278            Error::Utf8 { inner } => Some(inner),
279            _ => None,
280        }
281    }
282}
283
284#[cfg(feature = "std")]
285impl From<std::io::Error> for Error {
286    fn from(err: std::io::Error) -> Self {
287        Error::Io {
288            kind: err.kind(),
289            message: err.to_string(),
290        }
291    }
292}
293
294impl From<core::str::Utf8Error> for Error {
295    fn from(inner: core::str::Utf8Error) -> Self {
296        #[cfg(feature = "alloc")]
297        {
298            Error::Utf8 { inner }
299        }
300        #[cfg(not(feature = "alloc"))]
301        {
302            let _ = inner;
303            Error::InvalidData {
304                message: "UTF-8 decoding error",
305            }
306        }
307    }
308}
309
310#[cfg(all(test, feature = "std"))]
311mod tests {
312    use super::*;
313    use std::error::Error as StdError;
314
315    #[test]
316    fn test_source_chains_utf8_inner_error() {
317        // The Utf8 variant wraps a real core::str::Utf8Error; source() must
318        // surface it so downstream `?`/anyhow users can walk the chain.
319        // Build the invalid bytes through `black_box` so the compiler cannot
320        // const-evaluate the slice (which would trip the `invalid_from_utf8`
321        // lint on a literal); these bytes are not valid UTF-8.
322        let bytes = [core::hint::black_box(0xFFu8), 0xFFu8];
323        let utf8_err = core::str::from_utf8(&bytes).expect_err("invalid utf-8");
324        let err: Error = utf8_err.into();
325        assert!(matches!(err, Error::Utf8 { .. }));
326
327        let source = StdError::source(&err);
328        assert!(source.is_some(), "Utf8 variant must expose its inner error");
329
330        // The surfaced source downcasts back to the original Utf8Error.
331        let downcast = source
332            .expect("source present")
333            .downcast_ref::<core::str::Utf8Error>();
334        assert!(downcast.is_some(), "source must be the wrapped Utf8Error");
335    }
336
337    #[test]
338    fn test_source_none_for_non_wrapping_variants() {
339        // Variants that carry no inner std::error::Error report no source.
340        let io = Error::from(std::io::Error::other("boom"));
341        assert!(StdError::source(&io).is_none());
342
343        let invalid = Error::InvalidData { message: "nope" };
344        assert!(StdError::source(&invalid).is_none());
345
346        let outside = Error::OutsideUsizeRange(u64::MAX);
347        assert!(StdError::source(&outside).is_none());
348    }
349}