Skip to main content

hex_conservative/
error.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! The error types.
4//!
5//! These types are returned when hex decoding fails. The high-level ones are
6//! [`DecodeFixedLengthBytesError`] and [`DecodeVariableLengthBytesError`] which represent all
7//! possible ways in which hex decoding may fail in the two most common decoding scenarios.
8
9use core::convert::Infallible;
10use core::fmt;
11#[cfg(feature = "std")]
12use std::error::Error as StdError;
13#[cfg(all(not(feature = "std"), feature = "newer-rust-version"))]
14if_rust_version::if_rust_version! {
15    >= 1.81 {
16        use core::error::Error as StdError;
17    }
18}
19
20#[cfg(feature = "std")]
21macro_rules! if_std_error {
22    ({ $($if_yes:tt)* } $(else { $($if_not:tt)* })?) => {
23        #[cfg_attr(docsrs, doc(cfg(any(feature = "std", all(feature = "newer-rust-version", rust_version = ">= 1.81.0")))))]
24        $($if_yes)*
25    }
26}
27
28#[cfg(all(not(feature = "std"), feature = "newer-rust-version"))]
29macro_rules! if_std_error {
30    ({ $($if_yes:tt)* } $(else { $($if_not:tt)* })?) => {
31        if_rust_version::if_rust_version! {
32            >= 1.81 {
33                #[cfg_attr(docsrs, doc(cfg(any(feature = "std", all(feature = "newer-rust-version", rust_version = ">= 1.81.0")))))]
34                $($if_yes)*
35            } $(else { $($if_not)* })?
36        }
37    }
38}
39
40#[cfg(all(not(feature = "std"), not(feature = "newer-rust-version")))]
41macro_rules! if_std_error {
42    ({ $($if_yes:tt)* } $(else { $($if_not:tt)* })?) => {
43        $($($if_not)*)?
44    }
45}
46
47/// Formats error.
48///
49/// If `std` feature is OFF appends error source (delimited by `: `). We do this because
50/// `e.source()` is only available in std builds, without this macro the error source is lost for
51/// no-std builds.
52macro_rules! write_err {
53    ($writer:expr, $string:literal $(, $args:expr)*; $source:expr) => {
54        {
55            if_std_error! {
56                {
57                    {
58                        let _ = &$source;   // Prevents clippy warnings.
59                        write!($writer, $string $(, $args)*)
60                    }
61                } else {
62                    {
63                        write!($writer, concat!($string, ": {}") $(, $args)*, $source)
64                    }
65                }
66            }
67        }
68    }
69}
70
71/// Error returned when hex decoding a hex string with variable length.
72///
73/// This represents the first error encountered during decoding, however we may add other remaining
74/// ones in the future.
75///
76/// This error differs from [`DecodeFixedLengthBytesError`] in that the number of bytes is only known
77/// at run time - e.g. when decoding `Vec<u8>`.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum DecodeVariableLengthBytesError {
80    /// Non-hexadecimal character.
81    InvalidChar(InvalidCharError),
82    /// Purported hex string had odd (not even) length.
83    OddLengthString(OddLengthStringError),
84}
85
86impl DecodeVariableLengthBytesError {
87    /// Adds `by_bytes` to all character positions stored inside.
88    ///
89    /// If you're parsing a larger string that consists of multiple hex sub-strings and want to
90    /// return `InvalidCharError` you may need to use this function so that the callers of your
91    /// parsing function can tell the exact position where decoding failed relative to the start of
92    /// the string passed into your parsing function.
93    ///
94    /// Note that this function has the standard Rust overflow behavior because you should only
95    /// ever pass in the position of the parsed hex string relative to the start of the entire
96    /// input. In that case overflow is impossible.
97    ///
98    /// This method consumes and returns `self` so that calling it inside a closure passed into
99    /// [`Result::map_err`] is convenient.
100    #[must_use]
101    #[inline]
102    pub fn offset(self, by_bytes: usize) -> Self {
103        use DecodeVariableLengthBytesError as E;
104
105        match self {
106            E::InvalidChar(e) => E::InvalidChar(e.offset(by_bytes)),
107            E::OddLengthString(e) => E::OddLengthString(e),
108        }
109    }
110}
111
112impl From<Infallible> for DecodeVariableLengthBytesError {
113    #[inline]
114    fn from(never: Infallible) -> Self { match never {} }
115}
116
117impl fmt::Display for DecodeVariableLengthBytesError {
118    #[inline]
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        use DecodeVariableLengthBytesError as E;
121
122        match *self {
123            E::InvalidChar(ref e) => write_err!(f, "failed to decode hex"; e),
124            E::OddLengthString(ref e) => write_err!(f, "failed to decode hex"; e),
125        }
126    }
127}
128
129if_std_error! {{
130    impl StdError for DecodeVariableLengthBytesError {
131        #[inline]
132        fn source(&self) -> Option<&(dyn StdError + 'static)> {
133            use DecodeVariableLengthBytesError as E;
134
135            match *self {
136                E::InvalidChar(ref e) => Some(e),
137                E::OddLengthString(ref e) => Some(e),
138            }
139        }
140    }
141}}
142
143impl From<InvalidCharError> for DecodeVariableLengthBytesError {
144    #[inline]
145    fn from(e: InvalidCharError) -> Self { Self::InvalidChar(e) }
146}
147
148impl From<OddLengthStringError> for DecodeVariableLengthBytesError {
149    #[inline]
150    fn from(e: OddLengthStringError) -> Self { Self::OddLengthString(e) }
151}
152
153/// Invalid hex character.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct InvalidCharError {
156    pub(crate) invalid: u8,
157    pub(crate) pos: usize,
158}
159
160impl InvalidCharError {
161    /// Returns the invalid character byte.
162    #[inline]
163    #[deprecated(since = "TBD", note = "not suitable for use with UTF-8 strings")]
164    pub fn invalid_char(&self) -> u8 { self.invalid }
165    /// Returns the position of the invalid character byte.
166    #[inline]
167    pub fn pos(&self) -> usize { self.pos }
168
169    /// Adds `by_bytes` to all character positions stored inside.
170    ///
171    /// **Important**: if you have `DecodeVariableLengthBytesError` or `DecodeFixedLengthBytesError` you
172    /// should call the method *on them* - do not match them and manually call this method. Doing
173    /// so may lead to broken behavior in the future.
174    ///
175    /// If you're parsing a larger string that consists of multiple hex sub-strings and want to
176    /// return `InvalidCharError` you may need to use this function so that the callers of your
177    /// parsing function can tell the exact position where decoding failed relative to the start of
178    /// the string passed into your parsing function.
179    ///
180    /// Note that this function has the standard Rust overflow behavior because you should only
181    /// ever pass in the position of the parsed hex string relative to the start of the entire
182    /// input. In that case overflow is impossible.
183    ///
184    /// This method consumes and returns `self` so that calling it inside a closure passed into
185    /// [`Result::map_err`] is convenient.
186    #[must_use]
187    #[inline]
188    pub fn offset(mut self, by_bytes: usize) -> Self {
189        self.pos += by_bytes;
190        self
191    }
192}
193
194impl From<Infallible> for InvalidCharError {
195    #[inline]
196    fn from(never: Infallible) -> Self { match never {} }
197}
198
199/// Note that the implementation displays position as 1-based instead of 0-based to be more
200/// suitable to end users who might be non-programmers.
201impl fmt::Display for InvalidCharError {
202    #[inline]
203    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
204        // We're displaying this for general audience, not programmers, so we want to do 1-based
205        // position but that might confuse programmers who might think it's 0-based. Hopefully
206        // using more wordy approach will avoid the confusion.
207
208        // format_args! would be simpler but we can't use it because of  Rust issue #92698.
209        struct Format<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result>(F);
210        impl<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result> fmt::Display for Format<F> {
211            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0(f) }
212        }
213
214        // The lifetime is not extended in MSRV, so we need this.
215        let which;
216        let which: &dyn fmt::Display = match self.pos() {
217            0 => &"1st",
218            1 => &"2nd",
219            2 => &"3rd",
220            pos => {
221                which = Format(move |f| write!(f, "{}th", pos + 1));
222                &which
223            }
224        };
225
226        // The lifetime is not extended in MSRV, so we need these.
227        let chr_ascii;
228        let chr_non_ascii;
229
230        let invalid_char = self.invalid_char();
231        // We're currently not storing the entire character, so we need to make sure values >=
232        // 128 don't get misinterpreted as ISO-8859-1.
233        let chr: &dyn fmt::Display = if self.invalid_char().is_ascii() {
234            // Yes, the Debug output is correct here. Display would print the characters
235            // directly which would be confusing in case of control characters and it would
236            // also mess up the formatting. The `Debug` implementation of `char` properly
237            // escapes such characters.
238            chr_ascii = Format(move |f| write!(f, "{:?}", invalid_char as char));
239            &chr_ascii
240        } else {
241            chr_non_ascii = Format(move |f| write!(f, "{:#02x}", invalid_char));
242            &chr_non_ascii
243        };
244
245        write!(f, "the {} character, {}, is not a valid hex digit", which, chr)
246    }
247}
248
249if_std_error! {{
250    impl StdError for InvalidCharError {
251        #[inline]
252        fn source(&self) -> Option<&(dyn StdError + 'static)> { None }
253    }
254}}
255
256/// Purported hex string had odd length.
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct OddLengthStringError {
259    pub(crate) len: usize,
260}
261
262impl OddLengthStringError {
263    /// Returns the odd length of the input string.
264    #[inline]
265    pub fn length(&self) -> usize { self.len }
266}
267
268impl From<Infallible> for OddLengthStringError {
269    #[inline]
270    fn from(never: Infallible) -> Self { match never {} }
271}
272
273impl fmt::Display for OddLengthStringError {
274    #[inline]
275    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
276        if self.length() == 1 {
277            write!(f, "the hex string is 1 byte long which is not an even number")
278        } else {
279            write!(f, "the hex string is {} bytes long which is not an even number", self.length())
280        }
281    }
282}
283
284if_std_error! {{
285    impl StdError for OddLengthStringError {
286        #[inline]
287        fn source(&self) -> Option<&(dyn StdError + 'static)> { None }
288    }
289}}
290
291/// Error returned when hex decoding bytes whose length is known at compile time.
292///
293/// This error differs from [`DecodeVariableLengthBytesError`] in that the number of bytes is known at
294/// compile time - e.g. when decoding to an array of bytes.
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum DecodeFixedLengthBytesError {
297    /// Non-hexadecimal character.
298    InvalidChar(InvalidCharError),
299    /// Tried to parse fixed-length hash from a string with the wrong length.
300    InvalidLength(InvalidLengthError),
301}
302
303impl DecodeFixedLengthBytesError {
304    /// Adds `by_bytes` to all character positions stored inside.
305    ///
306    /// If you're parsing a larger string that consists of multiple hex sub-strings and want to
307    /// return `InvalidCharError` you may need to use this function so that the callers of your
308    /// parsing function can tell the exact position where decoding failed relative to the start of
309    /// the string passed into your parsing function.
310    ///
311    /// Note that this function has the standard Rust overflow behavior because you should only
312    /// ever pass in the position of the parsed hex string relative to the start of the entire
313    /// input. In that case overflow is impossible.
314    ///
315    /// This method consumes and returns `self` so that calling it inside a closure passed into
316    /// [`Result::map_err`] is convenient.
317    #[must_use]
318    #[inline]
319    pub fn offset(self, by_bytes: usize) -> Self {
320        use DecodeFixedLengthBytesError as E;
321
322        match self {
323            E::InvalidChar(e) => E::InvalidChar(e.offset(by_bytes)),
324            E::InvalidLength(e) => E::InvalidLength(e),
325        }
326    }
327}
328
329impl From<Infallible> for DecodeFixedLengthBytesError {
330    #[inline]
331    fn from(never: Infallible) -> Self { match never {} }
332}
333
334impl fmt::Display for DecodeFixedLengthBytesError {
335    #[inline]
336    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
337        use DecodeFixedLengthBytesError as E;
338
339        match *self {
340            E::InvalidChar(ref e) => write_err!(f, "failed to parse hex"; e),
341            E::InvalidLength(ref e) => write_err!(f, "failed to parse hex"; e),
342        }
343    }
344}
345
346if_std_error! {{
347    impl StdError for DecodeFixedLengthBytesError {
348        #[inline]
349        fn source(&self) -> Option<&(dyn StdError + 'static)> {
350            use DecodeFixedLengthBytesError as E;
351
352            match *self {
353                E::InvalidChar(ref e) => Some(e),
354                E::InvalidLength(ref e) => Some(e),
355            }
356        }
357    }
358}}
359
360impl From<InvalidCharError> for DecodeFixedLengthBytesError {
361    #[inline]
362    fn from(e: InvalidCharError) -> Self { Self::InvalidChar(e) }
363}
364
365impl From<InvalidLengthError> for DecodeFixedLengthBytesError {
366    #[inline]
367    fn from(e: InvalidLengthError) -> Self { Self::InvalidLength(e) }
368}
369
370/// Tried to parse fixed-length hash from a string with the wrong length.
371#[derive(Debug, Clone, PartialEq, Eq)]
372#[non_exhaustive]
373pub struct InvalidLengthError {
374    /// The expected length.
375    pub expected: usize,
376    /// The invalid length.
377    pub invalid: usize,
378}
379
380impl InvalidLengthError {
381    /// Returns the expected length.
382    ///
383    /// Note that this represents both the number of bytes and the number of characters that needs
384    /// to be passed into the decoder, since the hex digits are ASCII and thus always 1-byte long.
385    #[inline]
386    pub fn expected_length(&self) -> usize { self.expected }
387
388    /// Returns the number of *hex bytes* passed to the hex decoder.
389    ///
390    /// Note that this does not imply the number of characters nor hex digits since they may be
391    /// invalid (wide unicode chars).
392    #[inline]
393    pub fn invalid_length(&self) -> usize { self.invalid }
394}
395
396impl From<Infallible> for InvalidLengthError {
397    #[inline]
398    fn from(never: Infallible) -> Self { match never {} }
399}
400
401impl fmt::Display for InvalidLengthError {
402    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
403        write!(
404            f,
405            // Note on singular vs plural: expected length is never odd, so it cannot be 1
406            "the hex string is {} bytes long but exactly {} bytes were required",
407            self.invalid_length(),
408            self.expected_length()
409        )
410    }
411}
412
413if_std_error! {{
414    impl StdError for InvalidLengthError {
415        #[inline]
416        fn source(&self) -> Option<&(dyn StdError + 'static)> { None }
417    }
418}}
419
420#[cfg(test)]
421#[cfg(feature = "std")]
422mod tests {
423    use super::*;
424    #[cfg(feature = "alloc")]
425    use crate::decode_to_vec;
426    use crate::error::{InvalidCharError, OddLengthStringError};
427    use crate::{decode_to_array, HexToBytesIter, InvalidLengthError};
428
429    fn check_source<T: std::error::Error>(error: &T) {
430        assert!(error.source().is_some());
431    }
432
433    #[cfg(feature = "alloc")]
434    #[test]
435    fn invalid_char_error() {
436        let result = decode_to_vec("12G4");
437        let error = result.unwrap_err();
438        if let DecodeVariableLengthBytesError::InvalidChar(e) = error {
439            assert!(!format!("{}", e).is_empty());
440            assert_eq!(e.invalid_char(), b'G');
441            assert_eq!(e.pos(), 2);
442        } else {
443            panic!("Expected InvalidCharError");
444        }
445    }
446
447    #[cfg(feature = "alloc")]
448    #[test]
449    fn odd_length_string_error() {
450        let result = decode_to_vec("123");
451        let error = result.unwrap_err();
452        assert!(!format!("{}", error).is_empty());
453        check_source(&error);
454        if let DecodeVariableLengthBytesError::OddLengthString(e) = error {
455            assert!(!format!("{}", e).is_empty());
456            assert_eq!(e.length(), 3);
457        } else {
458            panic!("Expected OddLengthStringError");
459        }
460    }
461
462    #[test]
463    fn invalid_length_error() {
464        let result = decode_to_array::<4>("123");
465        let error = result.unwrap_err();
466        assert!(!format!("{}", error).is_empty());
467        check_source(&error);
468        if let DecodeFixedLengthBytesError::InvalidLength(e) = error {
469            assert!(!format!("{}", e).is_empty());
470            assert_eq!(e.expected_length(), 8);
471            assert_eq!(e.invalid_length(), 3);
472        } else {
473            panic!("Expected InvalidLengthError");
474        }
475    }
476
477    #[test]
478    fn to_bytes_error() {
479        let error =
480            DecodeVariableLengthBytesError::OddLengthString(OddLengthStringError { len: 7 });
481        assert!(!format!("{}", error).is_empty());
482        check_source(&error);
483    }
484
485    #[test]
486    fn to_array_error() {
487        let error = DecodeFixedLengthBytesError::InvalidLength(InvalidLengthError {
488            expected: 8,
489            invalid: 7,
490        });
491        assert!(!format!("{}", error).is_empty());
492        check_source(&error);
493    }
494
495    #[test]
496    #[cfg(feature = "alloc")]
497    fn hex_error() {
498        let oddlen = "0123456789abcdef0";
499        let badchar1 = "Z123456789abcdef";
500        let badchar2 = "012Y456789abcdeb";
501        let badchar3 = "«23456789abcdef";
502
503        assert_eq!(decode_to_vec(oddlen).unwrap_err(), OddLengthStringError { len: 17 }.into());
504        assert_eq!(
505            decode_to_array::<4>(oddlen).unwrap_err(),
506            InvalidLengthError { invalid: 17, expected: 8 }.into()
507        );
508        assert_eq!(
509            decode_to_vec(badchar1).unwrap_err(),
510            InvalidCharError { pos: 0, invalid: b'Z' }.into()
511        );
512        assert_eq!(
513            decode_to_vec(badchar2).unwrap_err(),
514            InvalidCharError { pos: 3, invalid: b'Y' }.into()
515        );
516        assert_eq!(
517            decode_to_vec(badchar3).unwrap_err(),
518            InvalidCharError { pos: 0, invalid: 194 }.into()
519        );
520    }
521
522    #[test]
523    fn hex_error_position() {
524        let badpos1 = "Z123456789abcdef";
525        let badpos2 = "012Y456789abcdeb";
526        let badpos3 = "0123456789abcdeZ";
527        let badpos4 = "0123456789abYdef";
528
529        assert_eq!(
530            HexToBytesIter::new(badpos1).unwrap().next().unwrap().unwrap_err(),
531            InvalidCharError { pos: 0, invalid: b'Z' }
532        );
533        assert_eq!(
534            HexToBytesIter::new(badpos2).unwrap().nth(1).unwrap().unwrap_err(),
535            InvalidCharError { pos: 3, invalid: b'Y' }
536        );
537        assert_eq!(
538            HexToBytesIter::new(badpos3).unwrap().next_back().unwrap().unwrap_err(),
539            InvalidCharError { pos: 15, invalid: b'Z' }
540        );
541        assert_eq!(
542            HexToBytesIter::new(badpos4).unwrap().nth_back(1).unwrap().unwrap_err(),
543            InvalidCharError { pos: 12, invalid: b'Y' }
544        );
545    }
546
547    #[test]
548    fn hex_to_array() {
549        let len_sixteen = "0123456789abcdef";
550        assert!(decode_to_array::<8>(len_sixteen).is_ok());
551    }
552
553    #[test]
554    fn hex_to_array_error() {
555        let len_sixteen = "0123456789abcdef";
556        assert_eq!(
557            decode_to_array::<4>(len_sixteen).unwrap_err(),
558            InvalidLengthError { invalid: 16, expected: 8 }.into()
559        );
560    }
561
562    #[test]
563    #[cfg(feature = "alloc")]
564    fn mixed_case() {
565        use crate::display::DisplayHex as _;
566
567        let s = "DEADbeef0123";
568        let want_lower = "deadbeef0123";
569        let want_upper = "DEADBEEF0123";
570
571        let v = decode_to_vec(s).expect("valid hex");
572        assert_eq!(format!("{:x}", v.as_hex()), want_lower);
573        assert_eq!(format!("{:X}", v.as_hex()), want_upper);
574    }
575}