ruint/
string.rs

1#![allow(clippy::missing_inline_in_public_items)] // allow format functions
2
3use crate::{base_convert::BaseConvertError, Uint};
4use core::{fmt, str::FromStr};
5
6/// Error for [`from_str_radix`](Uint::from_str_radix).
7#[derive(Debug, Copy, Clone, PartialEq, Eq)]
8pub enum ParseError {
9    /// Invalid digit in string.
10    InvalidDigit(char),
11
12    /// Invalid radix, up to base 64 is supported.
13    InvalidRadix(u64),
14
15    /// Error from [`Uint::from_base_be`].
16    BaseConvertError(BaseConvertError),
17}
18
19#[cfg(feature = "std")]
20impl std::error::Error for ParseError {
21    #[inline]
22    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
23        match self {
24            Self::BaseConvertError(e) => Some(e),
25            _ => None,
26        }
27    }
28}
29
30impl From<BaseConvertError> for ParseError {
31    #[inline]
32    fn from(value: BaseConvertError) -> Self {
33        Self::BaseConvertError(value)
34    }
35}
36
37impl fmt::Display for ParseError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::BaseConvertError(e) => e.fmt(f),
41            Self::InvalidDigit(c) => write!(f, "invalid digit: {c}"),
42            Self::InvalidRadix(r) => write!(f, "invalid radix {r}, up to 64 is supported"),
43        }
44    }
45}
46
47impl<const BITS: usize, const LIMBS: usize> Uint<BITS, LIMBS> {
48    /// Parse a string into a [`Uint`].
49    ///
50    /// For bases 2 to 36, the case-agnostic alphabet 0—1, a—b is used and `_`
51    /// are ignored. For bases 37 to 64, the case-sensitive alphabet a—z, A—Z,
52    /// 0—9, {+-}, {/,_} is used. That is, for base 64 it is compatible with
53    /// all the common base64 variants.
54    ///
55    /// # Errors
56    ///
57    /// * [`ParseError::InvalidDigit`] if the string contains a non-digit.
58    /// * [`ParseError::InvalidRadix`] if the radix is larger than 64.
59    /// * [`ParseError::BaseConvertError`] if [`Uint::from_base_be`] fails.
60    // FEATURE: Support proper unicode. Ignore zero-width spaces, joiners, etc.
61    // Recognize digits from other alphabets.
62    pub fn from_str_radix(src: &str, radix: u64) -> Result<Self, ParseError> {
63        if radix > 64 {
64            return Err(ParseError::InvalidRadix(radix));
65        }
66        let mut err = None;
67        let digits = src.chars().filter_map(|c| {
68            if err.is_some() {
69                return None;
70            }
71            let digit = if radix <= 36 {
72                // Case insensitive 0—9, a—z.
73                match c {
74                    '0'..='9' => u64::from(c) - u64::from('0'),
75                    'a'..='z' => u64::from(c) - u64::from('a') + 10,
76                    'A'..='Z' => u64::from(c) - u64::from('A') + 10,
77                    '_' => return None, // Ignored character.
78                    _ => {
79                        err = Some(ParseError::InvalidDigit(c));
80                        return None;
81                    }
82                }
83            } else {
84                // The Base-64 alphabets
85                match c {
86                    'A'..='Z' => u64::from(c) - u64::from('A'),
87                    'a'..='f' => u64::from(c) - u64::from('a') + 26,
88                    '0'..='9' => u64::from(c) - u64::from('0') + 52,
89                    '+' | '-' => 62,
90                    '/' | ',' | '_' => 63,
91                    '=' | '\r' | '\n' => return None, // Ignored characters.
92                    _ => {
93                        err = Some(ParseError::InvalidDigit(c));
94                        return None;
95                    }
96                }
97            };
98            Some(digit)
99        });
100        let value = Self::from_base_be(radix, digits)?;
101        err.map_or(Ok(value), Err)
102    }
103}
104
105impl<const BITS: usize, const LIMBS: usize> FromStr for Uint<BITS, LIMBS> {
106    type Err = ParseError;
107
108    fn from_str(src: &str) -> Result<Self, Self::Err> {
109        let (src, radix) = if src.is_char_boundary(2) {
110            let (prefix, rest) = src.split_at(2);
111            match prefix {
112                "0x" | "0X" => (rest, 16),
113                "0o" | "0O" => (rest, 8),
114                "0b" | "0B" => (rest, 2),
115                _ => (src, 10),
116            }
117        } else {
118            (src, 10)
119        };
120        Self::from_str_radix(src, radix)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use proptest::{prop_assert_eq, proptest};
128
129    #[test]
130    fn test_parse() {
131        proptest!(|(value: u128)| {
132            type U = Uint<128, 2>;
133            prop_assert_eq!(U::from_str(&format!("{value:#b}")), Ok(U::from(value)));
134            prop_assert_eq!(U::from_str(&format!("{value:#o}")), Ok(U::from(value)));
135            prop_assert_eq!(U::from_str(&format!("{value:}")), Ok(U::from(value)));
136            prop_assert_eq!(U::from_str(&format!("{value:#x}")), Ok(U::from(value)));
137            prop_assert_eq!(U::from_str(&format!("{value:#X}")), Ok(U::from(value)));
138        });
139    }
140}