bitcoin_units/
parse.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Parsing utilities.
4
5use alloc::string::String;
6use core::fmt;
7use core::str::FromStr;
8
9use internals::write_err;
10
11/// Error with rich context returned when a string can't be parsed as an integer.
12///
13/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
14/// parse as well as type information. As a result it provides very informative error messages that
15/// make it easier to understand the problem and correct mistakes.
16///
17/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
18/// in a performance-critical application you may want to box it or throw away the context by
19/// converting to `core` type.
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct ParseIntError {
23    pub(crate) input: String,
24    // for displaying - see Display impl with nice error message below
25    bits: u8,
26    // We could represent this as a single bit but it wouldn't actually derease the cost of moving
27    // the struct because String contains pointers so there will be padding of bits at least
28    // pointer_size - 1 bytes: min 1B in practice.
29    is_signed: bool,
30    pub(crate) source: core::num::ParseIntError,
31}
32
33impl ParseIntError {
34    /// Returns the input that was attempted to be parsed.
35    pub fn input(&self) -> &str { &self.input }
36}
37
38impl fmt::Display for ParseIntError {
39    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40        let signed = if self.is_signed { "signed" } else { "unsigned" };
41        let n = if self.bits == 8 { "n" } else { "" };
42        write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
43    }
44}
45
46#[cfg(feature = "std")]
47impl std::error::Error for ParseIntError {
48    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.source) }
49}
50
51impl From<ParseIntError> for core::num::ParseIntError {
52    fn from(value: ParseIntError) -> Self { value.source }
53}
54
55impl AsRef<core::num::ParseIntError> for ParseIntError {
56    fn as_ref(&self) -> &core::num::ParseIntError { &self.source }
57}
58
59/// Not strictly necessary but serves as a lint - avoids weird behavior if someone accidentally
60/// passes non-integer to the `parse()` function.
61pub trait Integer: FromStr<Err = core::num::ParseIntError> + TryFrom<i8> + Sized {}
62
63macro_rules! impl_integer {
64    ($($type:ty),* $(,)?) => {
65        $(
66        impl Integer for $type {}
67        )*
68    }
69}
70
71impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);
72
73/// Parses the input string as an integer returning an error carrying rich context.
74///
75/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
76/// owned since it avoids allocation in error case.
77pub fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
78    s.as_ref().parse().map_err(|error| {
79        ParseIntError {
80            input: s.into(),
81            bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
82            // We detect if the type is signed by checking if -1 can be represented by it
83            // this way we don't have to implement special traits and optimizer will get rid of the
84            // computation.
85            is_signed: T::try_from(-1i8).is_ok(),
86            source: error,
87        }
88    })
89}
90
91/// Parses a `u32` from a hex string.
92///
93/// Input string may or may not contain a `0x` prefix.
94pub fn hex_u32<S: AsRef<str> + Into<String>>(s: S) -> Result<u32, ParseIntError> {
95    let stripped = strip_hex_prefix(s.as_ref());
96    u32::from_str_radix(stripped, 16).map_err(|error| ParseIntError {
97        input: s.into(),
98        bits: 32,
99        is_signed: false,
100        source: error,
101    })
102}
103
104/// Parses a `u128` from a hex string.
105///
106/// Input string may or may not contain a `0x` prefix.
107pub fn hex_u128<S: AsRef<str> + Into<String>>(s: S) -> Result<u128, ParseIntError> {
108    let stripped = strip_hex_prefix(s.as_ref());
109    u128::from_str_radix(stripped, 16).map_err(|error| ParseIntError {
110        input: s.into(),
111        bits: 128,
112        is_signed: false,
113        source: error,
114    })
115}
116
117/// Strips the hex prefix off `s` if one is present.
118pub(crate) fn strip_hex_prefix(s: &str) -> &str {
119    if let Some(stripped) = s.strip_prefix("0x") {
120        stripped
121    } else if let Some(stripped) = s.strip_prefix("0X") {
122        stripped
123    } else {
124        s
125    }
126}
127
128/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using infallible
129/// conversion function `fn`.
130#[macro_export]
131macro_rules! impl_tryfrom_str_from_int_infallible {
132    ($($from:ty, $to:ident, $inner:ident, $fn:ident);*) => {
133        $(
134        impl core::convert::TryFrom<$from> for $to {
135            type Error = $crate::parse::ParseIntError;
136
137            fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
138                $crate::parse::int::<$inner, $from>(s).map($to::$fn)
139            }
140        }
141        )*
142    }
143}
144
145/// Implements `FromStr` and `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping
146/// the output using infallible conversion function `fn`.
147///
148/// The `Error` type is `ParseIntError`
149#[macro_export]
150macro_rules! impl_parse_str_from_int_infallible {
151    ($to:ident, $inner:ident, $fn:ident) => {
152        #[cfg(all(feature = "alloc", not(feature = "std")))]
153        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; alloc::string::String, $to, $inner, $fn; alloc::boxed::Box<str>, $to, $inner, $fn);
154        #[cfg(feature = "std")]
155        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; std::string::String, $to, $inner, $fn; std::boxed::Box<str>, $to, $inner, $fn);
156
157        impl core::str::FromStr for $to {
158            type Err = $crate::parse::ParseIntError;
159
160            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
161                $crate::parse::int::<$inner, &str>(s).map($to::$fn)
162            }
163        }
164
165    }
166}
167
168/// Implements `TryFrom<$from> for $to`.
169#[macro_export]
170macro_rules! impl_tryfrom_str {
171    ($($from:ty, $to:ty, $err:ty, $inner_fn:expr);*) => {
172        $(
173            impl core::convert::TryFrom<$from> for $to {
174                type Error = $err;
175
176                fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
177                    $inner_fn(s)
178                }
179            }
180        )*
181    }
182}
183
184/// Implements standard parsing traits for `$type` by calling into `$inner_fn`.
185#[macro_export]
186macro_rules! impl_parse_str {
187    ($to:ty, $err:ty, $inner_fn:expr) => {
188        $crate::impl_tryfrom_str!(&str, $to, $err, $inner_fn; String, $to, $err, $inner_fn; Box<str>, $to, $err, $inner_fn);
189
190        impl core::str::FromStr for $to {
191            type Err = $err;
192
193            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
194                $inner_fn(s)
195            }
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn parse_u32_from_hex_prefixed() {
206        let want = 171;
207        let got = hex_u32("0xab").expect("failed to parse prefixed hex");
208        assert_eq!(got, want);
209    }
210
211    #[test]
212    fn parse_u32_from_hex_no_prefix() {
213        let want = 171;
214        let got = hex_u32("ab").expect("failed to parse non-prefixed hex");
215        assert_eq!(got, want);
216    }
217
218    #[test]
219    fn parse_u128_from_hex_prefixed() {
220        let want = 3735928559;
221        let got = hex_u128("0xdeadbeef").expect("failed to parse prefixed hex");
222        assert_eq!(got, want);
223    }
224
225    #[test]
226    fn parse_u128_from_hex_no_prefix() {
227        let want = 3735928559;
228        let got = hex_u128("deadbeef").expect("failed to parse non-prefixed hex");
229        assert_eq!(got, want);
230    }
231}