Skip to main content

const_num_traits/ops/
from_ascii.rs

1//! Integer parsing from ASCII byte strings, mirroring the still-unstable
2//! `from_ascii` / `from_ascii_radix` inherent methods (`int_from_ascii`).
3//!
4//! Parsing from `&[u8]` instead of `&str` avoids dragging core's UTF-8
5//! validation machinery into the binary — valuable on embedded targets —
6//! and is exactly what protocol parsers have in hand anyway.
7//!
8//! The bodies are hand-rolled (std's are unstable, and its `ParseIntError`
9//! can't be constructed by other crates), which buys something std doesn't
10//! offer yet: the impls are **const-callable on nightly**, so byte-string
11//! constants can be parsed at compile time.
12//!
13//! **CT tier C (CT-hostile)**: data-dependent loop and early-exit parsing,
14//! like all string conversion.
15
16use core::fmt;
17
18/// The reason an ASCII parse failed.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AsciiErrorKind {
21    /// The input was empty.
22    Empty,
23    /// A byte was not a valid digit in the requested radix (this includes a
24    /// bare or misplaced sign, and a `-` on an unsigned type).
25    InvalidDigit,
26    /// The value was too large for the type.
27    PosOverflow,
28    /// The value was too small for the type.
29    NegOverflow,
30}
31
32/// Error parsing an integer from an ASCII byte string.
33///
34/// This is this crate's own error type (std's `ParseIntError` is opaque and
35/// can't be constructed here), mirroring `IntErrorKind`.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct AsciiParseError {
38    /// What went wrong.
39    pub kind: AsciiErrorKind,
40}
41
42impl fmt::Display for AsciiParseError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        let description = match self.kind {
45            AsciiErrorKind::Empty => "cannot parse integer from empty byte string",
46            AsciiErrorKind::InvalidDigit => "invalid digit found in byte string",
47            AsciiErrorKind::PosOverflow => "number too large to fit in target type",
48            AsciiErrorKind::NegOverflow => "number too small to fit in target type",
49        };
50        description.fmt(f)
51    }
52}
53
54c0nst::c0nst! {
55/// Parses an integer from an ASCII byte string, without any UTF-8
56/// machinery.
57pub c0nst trait FromAscii: Sized {
58    /// Parses an integer from base-10 ASCII digits with an optional leading
59    /// sign (`+`, and `-` for signed types).
60    ///
61    /// ```
62    /// use const_num_traits::FromAscii;
63    ///
64    /// assert_eq!(<u32 as FromAscii>::from_ascii(b"1234"), Ok(1234));
65    /// assert_eq!(<i32 as FromAscii>::from_ascii(b"-99"), Ok(-99));
66    /// assert!(<u8 as FromAscii>::from_ascii(b"300").is_err());
67    /// ```
68    fn from_ascii(src: &[u8]) -> Result<Self, AsciiParseError>;
69
70    /// Parses an integer from ASCII digits in the given radix, with an
71    /// optional leading sign. Digits beyond 9 are `a`-`z` / `A`-`Z`.
72    ///
73    /// # Panics
74    ///
75    /// Panics if `radix` is not in the range `2..=36`, matching the inherent
76    /// `from_str_radix` methods.
77    ///
78    /// ```
79    /// use const_num_traits::FromAscii;
80    ///
81    /// assert_eq!(<u32 as FromAscii>::from_ascii_radix(b"DeadBeef", 16), Ok(0xdead_beef));
82    /// assert_eq!(<i8 as FromAscii>::from_ascii_radix(b"-80", 16), Ok(i8::MIN));
83    /// ```
84    fn from_ascii_radix(src: &[u8], radix: u32) -> Result<Self, AsciiParseError>;
85}
86}
87
88// digit value of an ASCII byte, or u32::MAX as a sentinel for "not a digit"
89const fn ascii_digit(b: u8) -> u32 {
90    match b {
91        b'0'..=b'9' => (b - b'0') as u32,
92        b'a'..=b'z' => (b - b'a') as u32 + 10,
93        b'A'..=b'Z' => (b - b'A') as u32 + 10,
94        _ => u32::MAX,
95    }
96}
97
98macro_rules! from_ascii_impl {
99    // $signed: whether a leading `-` is accepted
100    ($signed:literal, $($t:ty)*) => {$(
101        c0nst::c0nst! {
102        c0nst impl FromAscii for $t {
103            #[inline]
104            fn from_ascii(src: &[u8]) -> Result<Self, AsciiParseError> {
105                <Self as FromAscii>::from_ascii_radix(src, 10)
106            }
107
108            fn from_ascii_radix(src: &[u8], radix: u32) -> Result<Self, AsciiParseError> {
109                assert!(
110                    radix >= 2 && radix <= 36,
111                    "from_ascii_radix: radix must lie in the range `[2, 36]`"
112                );
113                if src.is_empty() {
114                    return Err(AsciiParseError { kind: AsciiErrorKind::Empty });
115                }
116                let (positive, start) = match src[0] {
117                    b'+' => (true, 1),
118                    b'-' if $signed => (false, 1),
119                    _ => (true, 0),
120                };
121                if src.len() == start {
122                    // a bare sign, like std's `from_str_radix("+")`
123                    return Err(AsciiParseError { kind: AsciiErrorKind::InvalidDigit });
124                }
125                let overflow_kind = if positive {
126                    AsciiErrorKind::PosOverflow
127                } else {
128                    AsciiErrorKind::NegOverflow
129                };
130                let mut result: $t = 0;
131                let mut i = start;
132                while i < src.len() {
133                    let d = ascii_digit(src[i]);
134                    if d >= radix {
135                        return Err(AsciiParseError { kind: AsciiErrorKind::InvalidDigit });
136                    }
137                    result = match <$t>::checked_mul(result, radix as $t) {
138                        Some(v) => v,
139                        None => return Err(AsciiParseError { kind: overflow_kind }),
140                    };
141                    let step = if positive {
142                        <$t>::checked_add(result, d as $t)
143                    } else {
144                        <$t>::checked_sub(result, d as $t)
145                    };
146                    result = match step {
147                        Some(v) => v,
148                        None => return Err(AsciiParseError { kind: overflow_kind }),
149                    };
150                    i += 1;
151                }
152                Ok(result)
153            }
154        }
155        }
156    )*};
157}
158
159from_ascii_impl!(false, usize u8 u16 u32 u64 u128);
160from_ascii_impl!(true, isize i8 i16 i32 i64 i128);
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn basic() {
168        assert_eq!(<u32 as FromAscii>::from_ascii(b"0"), Ok(0));
169        assert_eq!(<u32 as FromAscii>::from_ascii(b"+42"), Ok(42));
170        assert_eq!(<i32 as FromAscii>::from_ascii(b"-42"), Ok(-42));
171        assert_eq!(<u32 as FromAscii>::from_ascii_radix(b"ff", 16), Ok(255));
172        assert_eq!(<u32 as FromAscii>::from_ascii_radix(b"FF", 16), Ok(255));
173        assert_eq!(<u32 as FromAscii>::from_ascii_radix(b"101", 2), Ok(5));
174        assert_eq!(<u32 as FromAscii>::from_ascii_radix(b"z", 36), Ok(35));
175        assert_eq!(<i8 as FromAscii>::from_ascii(b"-128"), Ok(i8::MIN));
176        assert_eq!(<i8 as FromAscii>::from_ascii(b"127"), Ok(i8::MAX));
177    }
178
179    #[test]
180    fn errors() {
181        use AsciiErrorKind::*;
182        let kind = |r: Result<u8, AsciiParseError>| r.unwrap_err().kind;
183        assert_eq!(kind(<u8 as FromAscii>::from_ascii(b"")), Empty);
184        assert_eq!(kind(<u8 as FromAscii>::from_ascii(b"+")), InvalidDigit);
185        assert_eq!(kind(<u8 as FromAscii>::from_ascii(b"-1")), InvalidDigit); // unsigned
186        assert_eq!(kind(<u8 as FromAscii>::from_ascii(b"12x")), InvalidDigit);
187        assert_eq!(kind(<u8 as FromAscii>::from_ascii(b"1 2")), InvalidDigit);
188        assert_eq!(kind(<u8 as FromAscii>::from_ascii(b"256")), PosOverflow);
189        assert_eq!(
190            kind(<u8 as FromAscii>::from_ascii_radix(b"9", 8)),
191            InvalidDigit
192        );
193        assert_eq!(
194            <i8 as FromAscii>::from_ascii(b"-129").unwrap_err().kind,
195            NegOverflow
196        );
197        assert_eq!(
198            <i8 as FromAscii>::from_ascii(b"--1").unwrap_err().kind,
199            InvalidDigit
200        );
201    }
202
203    #[test]
204    #[should_panic(expected = "radix must lie in the range")]
205    fn bad_radix() {
206        let _ = <u8 as FromAscii>::from_ascii_radix(b"1", 37);
207    }
208
209    #[test]
210    fn agrees_with_from_str_radix() {
211        // for valid UTF-8 inputs the two parsers must agree on Ok values
212        // and on whether an error occurred
213        let inputs: &[&str] = &[
214            "0", "1", "127", "128", "255", "256", "-1", "-128", "-129", "+5", "ff", "FF", "7f",
215            "deadbeef", "", "+", "-", "12_3", "0x10",
216        ];
217        for radix in [2u32, 8, 10, 16, 36] {
218            for s in inputs {
219                let ours = <i32 as FromAscii>::from_ascii_radix(s.as_bytes(), radix);
220                let std = i32::from_str_radix(s, radix);
221                assert_eq!(ours.is_ok(), std.is_ok(), "{s:?} radix {radix}");
222                if let (Ok(a), Ok(b)) = (ours, std) {
223                    assert_eq!(a, b, "{s:?} radix {radix}");
224                }
225            }
226        }
227    }
228}