havok_types/
parse_int.rs

1//! Fast string parsing of radix(`0b`, `0o`, `0x`) using `lexical` crate
2
3use core::num::NonZeroU8;
4use lexical::NumberFormatBuilder;
5
6const BASE_FORMAT: NumberFormatBuilder = NumberFormatBuilder::new()
7    .integer_internal_digit_separator(true)
8    .digit_separator(NonZeroU8::new(b'_'));
9
10const BIN_FORMAT: u128 = BASE_FORMAT
11    .radix(2)
12    .base_prefix(NonZeroU8::new(b'b'))
13    .base_prefix(NonZeroU8::new(b'B'))
14    .build();
15const OCTAL_FORMAT: u128 = BASE_FORMAT
16    .radix(8)
17    .base_prefix(NonZeroU8::new(b'o'))
18    .base_prefix(NonZeroU8::new(b'O'))
19    .build();
20const HEX_FORMAT: u128 = BASE_FORMAT
21    .radix(16)
22    .base_prefix(NonZeroU8::new(b'x'))
23    .base_prefix(NonZeroU8::new(b'X'))
24    .build();
25
26/// # Errors
27#[inline]
28fn parse_base<T>(input: &str, options: &T::Options) -> Result<T, lexical::Error>
29where
30    T: lexical::FromLexicalWithOptions + lexical::FromLexical,
31{
32    let prefix = if input.len() >= 2 { &input[0..2] } else { "" };
33    let input = input.as_bytes();
34    match prefix {
35        "0b" | "0B" => T::from_lexical_with_options::<BIN_FORMAT>(input, options),
36        "0o" | "0O" => T::from_lexical_with_options::<OCTAL_FORMAT>(input, options),
37        "0x" | "0X" => T::from_lexical_with_options::<HEX_FORMAT>(input, options),
38        _ => lexical::parse(input),
39    }
40}
41
42// Trait implements
43use lexical::parse_integer_options::Options;
44
45/// A trait for parsing numeric values from strings.
46///
47/// Fast string parsing of radix(`0b`, `0o`, `0x`) with separator(`_`) using `lexical` crate.
48pub trait ParseNumber: Sized {
49    /// Parses a string into the target numeric type.
50    ///
51    /// # Errors
52    /// Returns `lexical::Error` if parsing fails.
53    ///
54    /// # Examples
55    /// ```
56    /// use havok_types::parse_int::ParseNumber;
57    /// let value: i64 = <i64 as ParseNumber>::parse("123").unwrap();
58    /// assert_eq!(value, 123);
59    /// ```
60    fn parse(input: &str) -> Result<Self, lexical::Error>;
61
62    /// Parses a string as `i64` or `u64` and casts it to the target type using `as`.
63    ///
64    /// # Behavior
65    /// - If the target type is signed, it uses `i64` for parsing.
66    /// - If the target type is unsigned, it uses `u64` for parsing.
67    /// - The result is cast to the target type with `as`, allowing for wrapping behavior.
68    ///
69    /// # Errors
70    /// Returns `lexical::Error` if parsing fails.
71    ///
72    /// # Examples
73    /// ```
74    /// use havok_types::parse_int::ParseNumber;
75    /// let value: i8 = <i8 as ParseNumber>::parse_wrapping("300").unwrap();
76    /// assert_eq!(value, 44);  // Wrapping overflow
77    /// ```
78    ///
79    /// # Why do we need this?
80    /// `hkFlag` is wrapped if a value greater than [`i16`] comes in, and is stringified to hexadecimal as [`u32`] at XML time.
81    /// This method exists to reproduce that behavior.
82    ///
83    /// - Example: hkxcmd is setting the RoleFlags of `cow/behaviors/quadrupedbehavior.hkx` to `0xfffff300 (-3328)` in XML.
84    ///   by wrapping the input hexadecimal number that is greater than `i16::MAX`
85    /// ```
86    /// use havok_types::parse_int::ParseNumber;
87    /// const OVERFLOW_U32_STR: &str = "0xFFFFF300";
88    /// assert_eq!(
89    ///     <i16 as ParseNumber>::parse_wrapping(OVERFLOW_U32_STR).unwrap(),
90    ///     -3328
91    /// );
92    /// assert_eq!(format!("{:#X}", (-3328_i16) as u32), OVERFLOW_U32_STR);
93    /// ```
94    fn parse_wrapping(input: &str) -> Result<Self, lexical::Error>;
95}
96
97/// Implements `ParseNumber` for all supported numeric types.
98macro_rules! impl_parse_number {
99    ($($t:ty),*) => {
100        $(
101            impl ParseNumber for $t {
102                fn parse(input: &str) -> Result<Self, lexical::Error> {
103                    parse_base(input, &Options::new())
104                }
105
106                fn parse_wrapping(input: &str) -> Result<Self, lexical::Error> {
107                    if input.starts_with('-') {
108                        // Parse as `i64` for signed types
109                        let value = <i64 as ParseNumber>::parse(input)?;
110                        Ok(value as Self)
111                    } else {
112                        // Parse as `u64` for unsigned types
113                        let value = <u64 as ParseNumber>::parse(input)?;
114                        Ok(value as Self)
115                    }
116                }
117            }
118        )*
119    };
120}
121
122impl_parse_number!(
123    i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, usize, isize
124);
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn parse_numbers() {
132        // 0xFFFFF300_u32 -3328_i16 (0xF300)
133        let inputs = [
134            ("0xFFFFF300", 4294963968_i64),   // Hexadecimal
135            ("0b11000", 24),                  // Binary
136            ("0B1111_1111_1111_1111", 65535), // Binary (separator + uppercase)
137            ("0o11000", 4608),                // Octal
138            ("0O71432", 29466),               // Octal (uppercase)
139            ("01432", 1432),                  // Decimal (leading zero)
140            ("3432", 3432),                   // Decimal
141            ("0", 0),                         // Zero
142        ];
143
144        for (input, expected) in inputs {
145            match <i64 as ParseNumber>::parse(input) {
146                Ok(result) => assert_eq!(result, expected, "Failed to parse input: {input}"),
147                Err(e) => panic!("Error parsing input {input}: {e}"),
148            }
149        }
150    }
151
152    #[test]
153    fn test_parse_wrapping_i16_u16() {
154        // i16 and u16 cases
155
156        // Havok specification
157        {
158            // - Example: hkxcmd is setting the RoleFlags of `cow/behaviors/quadrupedbehavior.hkx` to 0xfffff300 (-3328) in XML.
159            //            by wrapping the input hexadecimal number that is greater than i16::MAX
160            const OVERFLOW_U32_STR: &str = "0xFFFFF300";
161            assert_eq!(
162                <i16 as ParseNumber>::parse_wrapping(OVERFLOW_U32_STR).unwrap(),
163                -3328
164            );
165            assert_eq!(format!("{:#X}", (-3328_i16) as u32), OVERFLOW_U32_STR);
166        }
167
168        assert_eq!(
169            <u16 as ParseNumber>::parse_wrapping("0xFFFFF300").unwrap(),
170            0xF300
171        );
172
173        assert_eq!(
174            <i16 as ParseNumber>::parse_wrapping("0x7FFF").unwrap(),
175            32767
176        );
177        assert_eq!(
178            <u16 as ParseNumber>::parse_wrapping("0x7FFF").unwrap(),
179            32767
180        );
181
182        assert_eq!(
183            <i16 as ParseNumber>::parse_wrapping("0x8000").unwrap(),
184            -32768
185        );
186        assert_eq!(
187            <u16 as ParseNumber>::parse_wrapping("0x8000").unwrap(),
188            32768
189        );
190
191        assert_eq!(<i16 as ParseNumber>::parse_wrapping("0xFFFF").unwrap(), -1);
192        assert_eq!(
193            <u16 as ParseNumber>::parse_wrapping("0xFFFF").unwrap(),
194            65535
195        );
196
197        assert_eq!(<i16 as ParseNumber>::parse_wrapping("0x10000").unwrap(), 0);
198        assert_eq!(<u16 as ParseNumber>::parse_wrapping("0x10000").unwrap(), 0);
199    }
200
201    #[test]
202    fn test_parse_wrapping_i8_u8() {
203        // i8 and u8 cases
204        assert_eq!(
205            <i8 as ParseNumber>::parse_wrapping("0b11111111").unwrap(),
206            -1
207        );
208        assert_eq!(
209            <u8 as ParseNumber>::parse_wrapping("0b11111111").unwrap(),
210            255
211        );
212
213        assert_eq!(
214            <i8 as ParseNumber>::parse_wrapping("0b10000000").unwrap(),
215            -128
216        );
217        assert_eq!(
218            <u8 as ParseNumber>::parse_wrapping("0b10000000").unwrap(),
219            128
220        );
221
222        assert_eq!(<i8 as ParseNumber>::parse_wrapping("0xFF").unwrap(), -1);
223        assert_eq!(<u8 as ParseNumber>::parse_wrapping("0xFF").unwrap(), 255);
224
225        assert_eq!(<i8 as ParseNumber>::parse_wrapping("0x100").unwrap(), 0);
226        assert_eq!(<u8 as ParseNumber>::parse_wrapping("0x100").unwrap(), 0);
227    }
228
229    #[test]
230    fn test_parse_wrapping_overflow() {
231        // Overflow behavior
232        assert_eq!(<u8 as ParseNumber>::parse_wrapping("256").unwrap(), 0); // 256 % 256 = 0
233        assert_eq!(<i8 as ParseNumber>::parse_wrapping("-129").unwrap(), 127); // -129 wraps to 127
234    }
235}