gix_utils/btoi.rs
1//! A module with utilities to turn byte slices with decimal numbers back into their
2//! binary representation.
3//!
4//! ### Credits
5//!
6//! This module was ported from <https://github.com/niklasf/rust-btoi> version 0.4.3
7//! see <https://github.com/GitoxideLabs/gitoxide/issues/729> for how it came to be in order
8//! to save 2.2 seconds of per-core compile time by not compiling the `num-traits` crate
9//! anymore.
10//!
11//! Licensed with compatible licenses [MIT] and [Apache]
12//!
13//! [MIT]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-MIT
14//! [Apache]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-APACHE
15
16/// An error that can occur when parsing an integer.
17///
18/// * No digits
19/// * Invalid digit
20/// * Overflow
21/// * Underflow
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ParseIntegerError {
24 kind: ErrorKind,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28enum ErrorKind {
29 Empty,
30 InvalidDigit,
31 Overflow,
32 Underflow,
33}
34
35impl ParseIntegerError {
36 fn desc(&self) -> &str {
37 match self.kind {
38 ErrorKind::Empty => "cannot parse integer without digits",
39 ErrorKind::InvalidDigit => "invalid digit found in slice",
40 ErrorKind::Overflow => "number too large to fit in target type",
41 ErrorKind::Underflow => "number too small to fit in target type",
42 }
43 }
44}
45
46impl std::fmt::Display for ParseIntegerError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 self.desc().fmt(f)
49 }
50}
51
52impl std::error::Error for ParseIntegerError {
53 fn description(&self) -> &str {
54 self.desc()
55 }
56}
57
58/// Converts a byte slice to an integer. Signs are not allowed.
59///
60/// # Errors
61///
62/// Returns [`ParseIntegerError`] for any of the following conditions:
63///
64/// * `bytes` is empty
65/// * not all characters of `bytes` are `0-9`
66/// * the number overflows `I`
67///
68/// # Panics
69///
70/// Panics in the pathological case that there is no representation of `10`
71/// in `I`.
72///
73/// # Examples
74///
75/// ```
76/// # use gix_utils::btoi::to_unsigned;
77/// assert_eq!(Ok(12345), to_unsigned(b"12345"));
78/// assert!(to_unsigned::<u8>(b"+1").is_err()); // only btoi allows signs
79/// assert!(to_unsigned::<u8>(b"256").is_err()); // overflow
80/// ```
81#[track_caller]
82pub fn to_unsigned<I: MinNumTraits>(bytes: &[u8]) -> Result<I, ParseIntegerError> {
83 to_unsigned_with_radix(bytes, 10)
84}
85
86/// Converts a byte slice in a given base to an integer. Signs are not allowed.
87///
88/// # Errors
89///
90/// Returns [`ParseIntegerError`] for any of the following conditions:
91///
92/// * `bytes` is empty
93/// * not all characters of `bytes` are `0-9`, `a-z` or `A-Z`
94/// * not all characters refer to digits in the given `radix`
95/// * the number overflows `I`
96///
97/// # Panics
98///
99/// Panics if `radix` is not in the range `2..=36` (or in the pathological
100/// case that there is no representation of `radix` in `I`).
101///
102/// # Examples
103///
104/// ```
105/// # use gix_utils::btoi::to_unsigned_with_radix;
106/// assert_eq!(Ok(255), to_unsigned_with_radix(b"ff", 16));
107/// assert_eq!(Ok(42), to_unsigned_with_radix(b"101010", 2));
108/// ```
109pub fn to_unsigned_with_radix<I: MinNumTraits>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError> {
110 assert!(
111 (2..=36).contains(&radix),
112 "radix must lie in the range 2..=36, found {radix}"
113 );
114
115 let base = I::from_u32(radix).expect("radix can be represented as integer");
116
117 if bytes.is_empty() {
118 return Err(ParseIntegerError { kind: ErrorKind::Empty });
119 }
120
121 let mut result = I::ZERO;
122
123 for &digit in bytes {
124 let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
125 Some(x) => x,
126 None => {
127 return Err(ParseIntegerError {
128 kind: ErrorKind::InvalidDigit,
129 })
130 }
131 };
132 result = match result.checked_mul(base) {
133 Some(result) => result,
134 None => {
135 return Err(ParseIntegerError {
136 kind: ErrorKind::Overflow,
137 })
138 }
139 };
140 result = match result.checked_add(x) {
141 Some(result) => result,
142 None => {
143 return Err(ParseIntegerError {
144 kind: ErrorKind::Overflow,
145 })
146 }
147 };
148 }
149
150 Ok(result)
151}
152
153/// Converts a byte slice to an integer.
154///
155/// Like [`to_unsigned`], but numbers may optionally start with a sign (`-` or `+`).
156///
157/// # Errors
158///
159/// Returns [`ParseIntegerError`] for any of the following conditions:
160///
161/// * `bytes` has no digits
162/// * not all characters of `bytes` are `0-9`, excluding an optional leading
163/// sign
164/// * the number overflows or underflows `I`
165///
166/// # Panics
167///
168/// Panics in the pathological case that there is no representation of `10`
169/// in `I`.
170///
171/// # Examples
172///
173/// ```
174/// # use gix_utils::btoi::to_signed;
175/// assert_eq!(Ok(123), to_signed(b"123"));
176/// assert_eq!(Ok(123), to_signed(b"+123"));
177/// assert_eq!(Ok(-123), to_signed(b"-123"));
178///
179/// assert!(to_signed::<u8>(b"123456789").is_err()); // overflow
180/// assert!(to_signed::<u8>(b"-1").is_err()); // underflow
181///
182/// assert!(to_signed::<i32>(b" 42").is_err()); // leading space
183/// ```
184pub fn to_signed<I: MinNumTraits>(bytes: &[u8]) -> Result<I, ParseIntegerError> {
185 to_signed_with_radix(bytes, 10)
186}
187
188/// Converts a byte slice in a given base to an integer.
189///
190/// Like [`to_unsigned_with_radix`], but numbers may optionally start with a sign
191/// (`-` or `+`).
192///
193/// # Errors
194///
195/// Returns [`ParseIntegerError`] for any of the following conditions:
196///
197/// * `bytes` has no digits
198/// * not all characters of `bytes` are `0-9`, `a-z`, `A-Z`, excluding an
199/// optional leading sign
200/// * not all characters refer to digits in the given `radix`, excluding an
201/// optional leading sign
202/// * the number overflows or underflows `I`
203///
204/// # Panics
205///
206/// Panics if `radix` is not in the range `2..=36` (or in the pathological
207/// case that there is no representation of `radix` in `I`).
208///
209/// # Examples
210///
211/// ```
212/// # use gix_utils::btoi::to_signed_with_radix;
213/// assert_eq!(Ok(10), to_signed_with_radix(b"a", 16));
214/// assert_eq!(Ok(10), to_signed_with_radix(b"+a", 16));
215/// assert_eq!(Ok(-42), to_signed_with_radix(b"-101010", 2));
216/// ```
217pub fn to_signed_with_radix<I: MinNumTraits>(bytes: &[u8], radix: u32) -> Result<I, ParseIntegerError> {
218 assert!(
219 (2..=36).contains(&radix),
220 "radix must lie in the range 2..=36, found {radix}"
221 );
222
223 let base = I::from_u32(radix).expect("radix can be represented as integer");
224
225 if bytes.is_empty() {
226 return Err(ParseIntegerError { kind: ErrorKind::Empty });
227 }
228
229 let digits = match bytes[0] {
230 b'+' => return to_unsigned_with_radix(&bytes[1..], radix),
231 b'-' => &bytes[1..],
232 _ => return to_unsigned_with_radix(bytes, radix),
233 };
234
235 if digits.is_empty() {
236 return Err(ParseIntegerError { kind: ErrorKind::Empty });
237 }
238
239 let mut result = I::ZERO;
240
241 for &digit in digits {
242 let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) {
243 Some(x) => x,
244 None => {
245 return Err(ParseIntegerError {
246 kind: ErrorKind::InvalidDigit,
247 })
248 }
249 };
250 result = match result.checked_mul(base) {
251 Some(result) => result,
252 None => {
253 return Err(ParseIntegerError {
254 kind: ErrorKind::Underflow,
255 })
256 }
257 };
258 result = match result.checked_sub(x) {
259 Some(result) => result,
260 None => {
261 return Err(ParseIntegerError {
262 kind: ErrorKind::Underflow,
263 })
264 }
265 };
266 }
267
268 Ok(result)
269}
270
271/// minimal subset of traits used by [`to_signed_with_radix`] and [`to_unsigned_with_radix`]
272pub trait MinNumTraits: Sized + Copy + TryFrom<u32> {
273 /// the 0 value for this type
274 const ZERO: Self;
275 /// convert from a unsigned 32-bit word
276 fn from_u32(n: u32) -> Option<Self> {
277 Self::try_from(n).ok()
278 }
279 /// the checked multiplication operation for this type
280 fn checked_mul(self, rhs: Self) -> Option<Self>;
281 /// the chekced addition operation for this type
282 fn checked_add(self, rhs: Self) -> Option<Self>;
283 /// the checked subtraction operation for this type
284 fn checked_sub(self, v: Self) -> Option<Self>;
285}
286
287macro_rules! impl_checked {
288 ($f:ident) => {
289 fn $f(self, rhs: Self) -> Option<Self> {
290 Self::$f(self, rhs)
291 }
292 };
293}
294
295macro_rules! min_num_traits {
296 ($t:ty) => {
297 impl MinNumTraits for $t {
298 const ZERO: Self = 0;
299 impl_checked!(checked_add);
300 impl_checked!(checked_mul);
301 impl_checked!(checked_sub);
302 }
303 };
304}
305
306min_num_traits!(i32);
307min_num_traits!(i64);
308min_num_traits!(u64);
309min_num_traits!(u8);
310min_num_traits!(usize);