diff --git a/Cargo.lock b/Cargo.lock index da80b6cd631..6c1c509b974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1429,12 +1429,12 @@ name = "gix-actor" version = "0.30.0" dependencies = [ "bstr", - "btoi", "document-features", "gix-date 0.8.3", "gix-features 0.38.0", "gix-hash 0.14.1", "gix-testtools", + "gix-utils 0.1.9", "itoa", "pretty_assertions", "serde", @@ -2002,7 +2002,6 @@ version = "0.30.0" dependencies = [ "bitflags 2.4.1", "bstr", - "btoi", "document-features", "filetime", "fnv", @@ -2013,6 +2012,7 @@ dependencies = [ "gix-lock 13.1.0", "gix-object 0.41.0", "gix-traverse 0.37.0", + "gix-utils 0.1.9", "hashbrown 0.14.3", "itoa", "libc", @@ -2159,7 +2159,6 @@ name = "gix-object" version = "0.41.0" dependencies = [ "bstr", - "btoi", "criterion", "document-features", "gix-actor 0.30.0", @@ -2167,6 +2166,7 @@ dependencies = [ "gix-features 0.38.0", "gix-hash 0.14.1", "gix-testtools", + "gix-utils 0.1.9", "gix-validate 0.8.3", "itoa", "pretty_assertions", @@ -2347,7 +2347,6 @@ dependencies = [ "async-std", "async-trait", "bstr", - "btoi", "document-features", "futures-io", "futures-lite 2.1.0", @@ -2358,6 +2357,7 @@ dependencies = [ "gix-packetline", "gix-testtools", "gix-transport", + "gix-utils 0.1.9", "maybe-async", "serde", "thiserror", @@ -2380,7 +2380,7 @@ name = "gix-quote" version = "0.4.10" dependencies = [ "bstr", - "btoi", + "gix-utils 0.1.9", "thiserror", ] diff --git a/gix-actor/Cargo.toml b/gix-actor/Cargo.toml index 9068bcf458b..56afd8f3c2d 100644 --- a/gix-actor/Cargo.toml +++ b/gix-actor/Cargo.toml @@ -19,19 +19,24 @@ serde = ["dep:serde", "bstr/serde", "gix-date/serde"] [dependencies] gix-features = { version = "^0.38.0", path = "../gix-features", optional = true } gix-date = { version = "^0.8.3", path = "../gix-date" } +gix-utils = { version = "^0.1.9", path = "../gix-utils" } thiserror = "1.0.38" -btoi = "0.4.2" -bstr = { version = "1.3.0", default-features = false, features = ["std", "unicode"]} +bstr = { version = "1.3.0", default-features = false, features = [ + "std", + "unicode", +] } winnow = { version = "0.6.0", features = ["simd"] } itoa = "1.0.1" -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} +serde = { version = "1.0.114", optional = true, default-features = false, features = [ + "derive", +] } document-features = { version = "0.2.0", optional = true } [dev-dependencies] pretty_assertions = "1.0.0" -gix-testtools = { path = "../tests/tools"} +gix-testtools = { path = "../tests/tools" } gix-hash = { path = "../gix-hash" } [package.metadata.docs.rs] diff --git a/gix-actor/src/signature/decode.rs b/gix-actor/src/signature/decode.rs index 32d0a83516e..b19deb510bd 100644 --- a/gix-actor/src/signature/decode.rs +++ b/gix-actor/src/signature/decode.rs @@ -1,7 +1,7 @@ pub(crate) mod function { use bstr::ByteSlice; - use btoi::btoi; use gix_date::{time::Sign, OffsetInSeconds, SecondsSinceUnixEpoch, Time}; + use gix_utils::btoi::to_signed; use winnow::{ combinator::{alt, separated_pair, terminated}, error::{AddContext, ParserError, StrContext}, @@ -23,7 +23,7 @@ pub(crate) mod function { b" ", ( terminated(take_until(0.., SPACE), take(1usize)) - .verify_map(|v| btoi::(v).ok()) + .verify_map(|v| to_signed::(v).ok()) .context(StrContext::Expected("".into())), alt(( take_while(1.., b'-').map(|_| Sign::Minus), @@ -31,10 +31,10 @@ pub(crate) mod function { )) .context(StrContext::Expected("+|-".into())), take_while(2, AsChar::is_dec_digit) - .verify_map(|v| btoi::(v).ok()) + .verify_map(|v| to_signed::(v).ok()) .context(StrContext::Expected("HH".into())), take_while(1..=2, AsChar::is_dec_digit) - .verify_map(|v| btoi::(v).ok()) + .verify_map(|v| to_signed::(v).ok()) .context(StrContext::Expected("MM".into())), ) .map(|(time, sign, hours, minutes)| { diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index 472094d4e2e..fbd712ee24e 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -20,13 +20,17 @@ test = true serde = ["dep:serde", "smallvec/serde", "gix-hash/serde"] [dependencies] -gix-features = { version = "^0.38.0", path = "../gix-features", features = ["rustsha1", "progress"] } +gix-features = { version = "^0.38.0", path = "../gix-features", features = [ + "rustsha1", + "progress", +] } gix-hash = { version = "^0.14.1", path = "../gix-hash" } gix-bitmap = { version = "^0.2.10", path = "../gix-bitmap" } gix-object = { version = "^0.41.0", path = "../gix-object" } gix-traverse = { version = "^0.37.0", path = "../gix-traverse" } gix-lock = { version = "^13.0.0", path = "../gix-lock" } gix-fs = { version = "^0.10.0", path = "../gix-fs" } +gix-utils = { version = "^0.1.9", path = "../gix-utils" } hashbrown = "0.14.3" fnv = "1.0.7" @@ -35,16 +39,20 @@ memmap2 = "0.9.0" filetime = "0.2.15" bstr = { version = "1.3.0", default-features = false } -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } +serde = { version = "1.0.114", optional = true, default-features = false, features = [ + "derive", +] } smallvec = "1.7.0" -btoi = "0.4.3" itoa = "1.0.3" bitflags = "2" document-features = { version = "0.2.0", optional = true } [target.'cfg(not(windows))'.dependencies] -rustix = { version = "0.38.20", default-features = false, features = ["std", "fs"] } +rustix = { version = "0.38.20", default-features = false, features = [ + "std", + "fs", +] } libc = { version = "0.2.149" } [package.metadata.docs.rs] diff --git a/gix-index/src/extension/tree/decode.rs b/gix-index/src/extension/tree/decode.rs index 530c65d8ff2..a55d12269a1 100644 --- a/gix-index/src/extension/tree/decode.rs +++ b/gix-index/src/extension/tree/decode.rs @@ -22,10 +22,10 @@ fn one_recursive(data: &[u8], hash_len: usize) -> Option<(Tree, &[u8])> { let (path, data) = split_at_byte_exclusive(data, 0)?; let (entry_count, data) = split_at_byte_exclusive(data, b' ')?; - let num_entries: i32 = btoi::btoi(entry_count).ok()?; + let num_entries: i32 = gix_utils::btoi::to_signed(entry_count).ok()?; let (subtree_count, data) = split_at_byte_exclusive(data, b'\n')?; - let subtree_count: usize = btoi::btou(subtree_count).ok()?; + let subtree_count: usize = gix_utils::btoi::to_unsigned(subtree_count).ok()?; let (id, mut data) = if num_entries >= 0 { let (hash, data) = split_at_pos(data, hash_len)?; diff --git a/gix-object/Cargo.toml b/gix-object/Cargo.toml index c2c3692b648..29b6d19de67 100644 --- a/gix-object/Cargo.toml +++ b/gix-object/Cargo.toml @@ -20,7 +20,13 @@ path = "./benches/decode_objects.rs" [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde = ["dep:serde", "bstr/serde", "smallvec/serde", "gix-hash/serde", "gix-actor/serde"] +serde = [ + "dep:serde", + "bstr/serde", + "smallvec/serde", + "gix-hash/serde", + "gix-actor/serde", +] ## When parsing objects by default errors will only be available on the granularity of success or failure, and with the above flag enabled ## details information about the error location will be collected. ## Use it in applications which expect broken or invalid objects or for debugging purposes. Incorrectly formatted objects aren't at all @@ -28,28 +34,35 @@ serde = ["dep:serde", "bstr/serde", "smallvec/serde", "gix-hash/serde", "gix-act verbose-object-parsing-errors = ["winnow/std"] [dependencies] -gix-features = { version = "^0.38.0", path = "../gix-features", features = ["rustsha1", "progress"] } +gix-features = { version = "^0.38.0", path = "../gix-features", features = [ + "rustsha1", + "progress", +] } gix-hash = { version = "^0.14.1", path = "../gix-hash" } gix-validate = { version = "^0.8.3", path = "../gix-validate" } gix-actor = { version = "^0.30.0", path = "../gix-actor" } gix-date = { version = "^0.8.3", path = "../gix-date" } +gix-utils = { version = "^0.1.9", path = "../gix-utils" } -btoi = "0.4.2" itoa = "1.0.1" thiserror = "1.0.34" -bstr = { version = "1.3.0", default-features = false, features = ["std", "unicode"] } +bstr = { version = "1.3.0", default-features = false, features = [ + "std", + "unicode", +] } winnow = { version = "0.6.0", features = ["simd"] } smallvec = { version = "1.4.0", features = ["write"] } -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} +serde = { version = "1.0.114", optional = true, default-features = false, features = [ + "derive", +] } document-features = { version = "0.2.0", optional = true } [dev-dependencies] criterion = "0.5.1" pretty_assertions = "1.0.0" -gix-testtools = { path = "../tests/tools"} +gix-testtools = { path = "../tests/tools" } [package.metadata.docs.rs] all-features = true features = ["document-features"] - diff --git a/gix-object/src/lib.rs b/gix-object/src/lib.rs index 85ff8708e11..43cb5f5bfd0 100644 --- a/gix-object/src/lib.rs +++ b/gix-object/src/lib.rs @@ -358,7 +358,7 @@ pub mod decode { pub enum LooseHeaderDecodeError { #[error("{message}: {number:?}")] ParseIntegerError { - source: btoi::ParseIntegerError, + source: gix_utils::btoi::ParseIntegerError, message: &'static str, number: bstr::BString, }, @@ -383,7 +383,7 @@ pub mod decode { message: "Did not find 0 byte in header", })?; let size_bytes = &input[kind_end + 1..size_end]; - let size = btoi::btoi(size_bytes).map_err(|source| ParseIntegerError { + let size = gix_utils::btoi::to_signed(size_bytes).map_err(|source| ParseIntegerError { source, message: "Object size in header could not be parsed", number: size_bytes.into(), diff --git a/gix-protocol/Cargo.toml b/gix-protocol/Cargo.toml index 1713c640126..0131463afeb 100644 --- a/gix-protocol/Cargo.toml +++ b/gix-protocol/Cargo.toml @@ -23,7 +23,12 @@ doctest = false ## If set, blocking command implementations are available and will use the blocking version of the `gix-transport` crate. blocking-client = ["gix-transport/blocking-client", "maybe-async/is_sync"] ## As above, but provides async implementations instead. -async-client = ["gix-transport/async-client", "async-trait", "futures-io", "futures-lite"] +async-client = [ + "gix-transport/async-client", + "async-trait", + "futures-io", + "futures-lite", +] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. @@ -40,17 +45,24 @@ path = "tests/async-protocol.rs" required-features = ["async-client"] [dependencies] -gix-features = { version = "^0.38.0", path = "../gix-features", features = ["progress"] } +gix-features = { version = "^0.38.0", path = "../gix-features", features = [ + "progress", +] } gix-transport = { version = "^0.41.0", path = "../gix-transport" } gix-hash = { version = "^0.14.1", path = "../gix-hash" } gix-date = { version = "^0.8.3", path = "../gix-date" } gix-credentials = { version = "^0.24.0", path = "../gix-credentials" } +gix-utils = { version = "^0.1.9", path = "../gix-utils" } thiserror = "1.0.32" -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} -bstr = { version = "1.3.0", default-features = false, features = ["std", "unicode"] } +serde = { version = "1.0.114", optional = true, default-features = false, features = [ + "derive", +] } +bstr = { version = "1.3.0", default-features = false, features = [ + "std", + "unicode", +] } winnow = { version = "0.6.0", features = ["simd"] } -btoi = "0.4.2" # for async-client async-trait = { version = "0.1.51", optional = true } @@ -62,7 +74,7 @@ document-features = { version = "0.2.0", optional = true } [dev-dependencies] async-std = { version = "1.9.0", features = ["attributes"] } -gix-packetline = { path = "../gix-packetline" ,version = "^0.17.3" } +gix-packetline = { path = "../gix-packetline", version = "^0.17.3" } gix-testtools = { path = "../tests/tools" } [package.metadata.docs.rs] diff --git a/gix-protocol/src/remote_progress.rs b/gix-protocol/src/remote_progress.rs index 2e066b618a6..d7baa3bf9f5 100644 --- a/gix-protocol/src/remote_progress.rs +++ b/gix-protocol/src/remote_progress.rs @@ -75,7 +75,7 @@ impl<'a> RemoteProgress<'a> { fn parse_number(i: &mut &[u8]) -> PResult { take_till(0.., |c: u8| !c.is_ascii_digit()) - .try_map(btoi::btoi) + .try_map(gix_utils::btoi::to_signed) .parse_next(i) } diff --git a/gix-quote/Cargo.toml b/gix-quote/Cargo.toml index f2e808680d0..050f13a6d93 100644 --- a/gix-quote/Cargo.toml +++ b/gix-quote/Cargo.toml @@ -13,6 +13,7 @@ include = ["src/**/*", "LICENSE-*"] doctest = false [dependencies] -bstr = { version = "1.3.0", default-features = false, features = ["std"]} +gix-utils = { version = "^0.1.9", path = "../gix-utils" } + +bstr = { version = "1.3.0", default-features = false, features = ["std"] } thiserror = "1.0.38" -btoi = "0.4.2" diff --git a/gix-quote/src/ansi_c.rs b/gix-quote/src/ansi_c.rs index 40d8db8d1ae..43856f8992e 100644 --- a/gix-quote/src/ansi_c.rs +++ b/gix-quote/src/ansi_c.rs @@ -89,7 +89,8 @@ pub fn undo(input: &BStr) -> Result<(Cow<'_, BStr>, usize), undo::Error> { })? .read_exact(&mut buf[1..]) .expect("impossible to fail as numbers match"); - let byte = btoi::btou_radix(&buf, 8).map_err(|e| undo::Error::new(e, original))?; + let byte = gix_utils::btoi::to_unsigned_with_radix(&buf, 8) + .map_err(|e| undo::Error::new(e, original))?; out.push(byte); input = &input[2..]; consumed += 2; diff --git a/gix-utils/Cargo.toml b/gix-utils/Cargo.toml index a197b4677ad..b7c9e1e547a 100644 --- a/gix-utils/Cargo.toml +++ b/gix-utils/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.65" include = ["src/**/*", "LICENSE-*"] [lib] -doctest = false +doctest = true [features] bstr = ["dep:bstr"] diff --git a/gix-utils/src/btoi.rs b/gix-utils/src/btoi.rs new file mode 100644 index 00000000000..0b379693d53 --- /dev/null +++ b/gix-utils/src/btoi.rs @@ -0,0 +1,316 @@ +/// A module with utilities to turn byte slices with decimal numbers back into their +/// binary representation. +/// +/// ### Credits +/// +/// This module was ported from version 0.4.3 +/// see for how it came to be in order +/// to save 2.2 seconds of per-core compile time by not compiling the `num-traits` crate +/// anymore. +/// +/// Licensed with compatible licenses [MIT] and [Apache] +/// +/// [MIT]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-MIT +/// [Apache]: https://github.com/niklasf/rust-btoi/blob/master/LICENSE-APACHE + +/// An error that can occur when parsing an integer. +/// +/// * No digits +/// * Invalid digit +/// * Overflow +/// * Underflow +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseIntegerError { + kind: ErrorKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ErrorKind { + Empty, + InvalidDigit, + Overflow, + Underflow, +} + +impl ParseIntegerError { + fn desc(&self) -> &str { + match self.kind { + ErrorKind::Empty => "cannot parse integer without digits", + ErrorKind::InvalidDigit => "invalid digit found in slice", + ErrorKind::Overflow => "number too large to fit in target type", + ErrorKind::Underflow => "number too small to fit in target type", + } + } +} + +impl std::fmt::Display for ParseIntegerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.desc().fmt(f) + } +} + +impl std::error::Error for ParseIntegerError { + fn description(&self) -> &str { + self.desc() + } +} + +/// Converts a byte slice to an integer. Signs are not allowed. +/// +/// # Errors +/// +/// Returns [`ParseIntegerError`] for any of the following conditions: +/// +/// * `bytes` is empty +/// * not all characters of `bytes` are `0-9` +/// * the number overflows `I` +/// +/// # Panics +/// +/// Panics in the pathological case that there is no representation of `10` +/// in `I`. +/// +/// # Examples +/// +/// ``` +/// # use gix_utils::btoi::to_unsigned; +/// assert_eq!(Ok(12345), to_unsigned(b"12345")); +/// assert!(to_unsigned::(b"+1").is_err()); // only btoi allows signs +/// assert!(to_unsigned::(b"256").is_err()); // overflow +/// ``` +#[track_caller] +pub fn to_unsigned(bytes: &[u8]) -> Result { + to_unsigned_with_radix(bytes, 10) +} + +/// Converts a byte slice in a given base to an integer. Signs are not allowed. +/// +/// # Errors +/// +/// Returns [`ParseIntegerError`] for any of the following conditions: +/// +/// * `bytes` is empty +/// * not all characters of `bytes` are `0-9`, `a-z` or `A-Z` +/// * not all characters refer to digits in the given `radix` +/// * the number overflows `I` +/// +/// # Panics +/// +/// Panics if `radix` is not in the range `2..=36` (or in the pathological +/// case that there is no representation of `radix` in `I`). +/// +/// # Examples +/// +/// ``` +/// # use gix_utils::btoi::to_unsigned_with_radix; +/// assert_eq!(Ok(255), to_unsigned_with_radix(b"ff", 16)); +/// assert_eq!(Ok(42), to_unsigned_with_radix(b"101010", 2)); +/// ``` +pub fn to_unsigned_with_radix(bytes: &[u8], radix: u32) -> Result { + assert!( + (2..=36).contains(&radix), + "radix must lie in the range 2..=36, found {radix}" + ); + + let base = I::from_u32(radix).expect("radix can be represented as integer"); + + if bytes.is_empty() { + return Err(ParseIntegerError { kind: ErrorKind::Empty }); + } + + let mut result = I::zero(); + + for &digit in bytes { + let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) { + Some(x) => x, + None => { + return Err(ParseIntegerError { + kind: ErrorKind::InvalidDigit, + }) + } + }; + result = match result.checked_mul(base) { + Some(result) => result, + None => { + return Err(ParseIntegerError { + kind: ErrorKind::Overflow, + }) + } + }; + result = match result.checked_add(x) { + Some(result) => result, + None => { + return Err(ParseIntegerError { + kind: ErrorKind::Overflow, + }) + } + }; + } + + Ok(result) +} + +/// Converts a byte slice to an integer. +/// +/// Like [`to_unsigned`], but numbers may optionally start with a sign (`-` or `+`). +/// +/// # Errors +/// +/// Returns [`ParseIntegerError`] for any of the following conditions: +/// +/// * `bytes` has no digits +/// * not all characters of `bytes` are `0-9`, excluding an optional leading +/// sign +/// * the number overflows or underflows `I` +/// +/// # Panics +/// +/// Panics in the pathological case that there is no representation of `10` +/// in `I`. +/// +/// # Examples +/// +/// ``` +/// # use gix_utils::btoi::to_signed; +/// assert_eq!(Ok(123), to_signed(b"123")); +/// assert_eq!(Ok(123), to_signed(b"+123")); +/// assert_eq!(Ok(-123), to_signed(b"-123")); +/// +/// assert!(to_signed::(b"123456789").is_err()); // overflow +/// assert!(to_signed::(b"-1").is_err()); // underflow +/// +/// assert!(to_signed::(b" 42").is_err()); // leading space +/// ``` +pub fn to_signed(bytes: &[u8]) -> Result { + to_signed_with_radix(bytes, 10) +} + +/// Converts a byte slice in a given base to an integer. +/// +/// Like [`to_unsigned_with_radix`], but numbers may optionally start with a sign +/// (`-` or `+`). +/// +/// # Errors +/// +/// Returns [`ParseIntegerError`] for any of the following conditions: +/// +/// * `bytes` has no digits +/// * not all characters of `bytes` are `0-9`, `a-z`, `A-Z`, exluding an +/// optional leading sign +/// * not all characters refer to digits in the given `radix`, exluding an +/// optional leading sign +/// * the number overflows or underflows `I` +/// +/// # Panics +/// +/// Panics if `radix` is not in the range `2..=36` (or in the pathological +/// case that there is no representation of `radix` in `I`). +/// +/// # Examples +/// +/// ``` +/// # use gix_utils::btoi::to_signed_with_radix; +/// assert_eq!(Ok(10), to_signed_with_radix(b"a", 16)); +/// assert_eq!(Ok(10), to_signed_with_radix(b"+a", 16)); +/// assert_eq!(Ok(-42), to_signed_with_radix(b"-101010", 2)); +/// ``` +pub fn to_signed_with_radix(bytes: &[u8], radix: u32) -> Result { + assert!( + (2..=36).contains(&radix), + "radix must lie in the range 2..=36, found {radix}" + ); + + let base = I::from_u32(radix).expect("radix can be represented as integer"); + + if bytes.is_empty() { + return Err(ParseIntegerError { kind: ErrorKind::Empty }); + } + + let digits = match bytes[0] { + b'+' => return to_unsigned_with_radix(&bytes[1..], radix), + b'-' => &bytes[1..], + _ => return to_unsigned_with_radix(bytes, radix), + }; + + if digits.is_empty() { + return Err(ParseIntegerError { kind: ErrorKind::Empty }); + } + + let mut result = I::zero(); + + for &digit in digits { + let x = match char::from(digit).to_digit(radix).and_then(I::from_u32) { + Some(x) => x, + None => { + return Err(ParseIntegerError { + kind: ErrorKind::InvalidDigit, + }) + } + }; + result = match result.checked_mul(base) { + Some(result) => result, + None => { + return Err(ParseIntegerError { + kind: ErrorKind::Underflow, + }) + } + }; + result = match result.checked_sub(x) { + Some(result) => result, + None => { + return Err(ParseIntegerError { + kind: ErrorKind::Underflow, + }) + } + }; + } + + Ok(result) +} + +/// minimal subset of traits used by [`to_signed_with_radix`] and [`to_unsigned_with_radix`] +pub trait MinNumTraits: Sized + Copy { + /// + fn from_u32(n: u32) -> Option; + /// + fn zero() -> Self; + /// + fn checked_mul(self, v: Self) -> Option; + /// + fn checked_add(self, v: Self) -> Option; + /// + fn checked_sub(self, v: Self) -> Option; +} + +macro_rules! min_num_traits { + ($t : ty, from_u32 => $from_u32 : expr) => { + impl MinNumTraits for $t { + fn from_u32(n: u32) -> Option<$t> { + #[allow(clippy::redundant_closure_call)] + $from_u32(n) + } + + fn zero() -> Self { + 0 + } + + fn checked_mul(self, v: $t) -> Option<$t> { + <$t>::checked_mul(self, v) + } + + fn checked_add(self, v: $t) -> Option<$t> { + <$t>::checked_add(self, v) + } + + fn checked_sub(self, v: $t) -> Option<$t> { + <$t>::checked_sub(self, v) + } + } + }; +} + +min_num_traits!(i32, from_u32 => |n: u32| n.try_into().ok()); +min_num_traits!(i64, from_u32 => |n: u32| Some(n.into())); +min_num_traits!(u64, from_u32 => |n: u32| Some(n.into())); +min_num_traits!(u8, from_u32 => |n: u32| n.try_into().ok()); +min_num_traits!(usize, from_u32 => |n: u32| n.try_into().ok()); diff --git a/gix-utils/src/lib.rs b/gix-utils/src/lib.rs index 2e4b3e35f0b..5e2e32d1365 100644 --- a/gix-utils/src/lib.rs +++ b/gix-utils/src/lib.rs @@ -13,6 +13,9 @@ pub mod buffers; /// pub mod str; +/// +pub mod btoi; + /// A utility to do buffer-swapping with. /// /// Use `src` to read from and `dest` to write to, and after actually changing data, call [Buffers::swap()]. diff --git a/gix-utils/tests/btoi/mod.rs b/gix-utils/tests/btoi/mod.rs new file mode 100644 index 00000000000..8351531c0d9 --- /dev/null +++ b/gix-utils/tests/btoi/mod.rs @@ -0,0 +1,33 @@ +use gix_utils::btoi::{to_signed, to_signed_with_radix, to_unsigned, to_unsigned_with_radix}; + +#[test] +fn binary_to_unsigned() { + assert_eq!(Ok(12345), to_unsigned(b"12345")); + assert!(to_unsigned::(b"+1").is_err()); // only btoi allows signs + assert!(to_unsigned::(b"256").is_err()); // overflow +} + +#[test] +fn binary_to_unsigned_radix() { + assert_eq!(Ok(255), to_unsigned_with_radix(b"ff", 16)); + assert_eq!(Ok(42), to_unsigned_with_radix(b"101010", 2)); +} + +#[test] +fn binary_to_integer_radix() { + assert_eq!(Ok(10), to_signed_with_radix(b"a", 16)); + assert_eq!(Ok(10), to_signed_with_radix(b"+a", 16)); + assert_eq!(Ok(-42), to_signed_with_radix(b"-101010", 2)); +} + +#[test] +fn binary_to_integer() { + assert_eq!(Ok(123), to_signed(b"123")); + assert_eq!(Ok(123), to_signed(b"+123")); + assert_eq!(Ok(-123), to_signed(b"-123")); + + assert!(to_signed::(b"123456789").is_err()); // overflow + assert!(to_signed::(b"-1").is_err()); // underflow + + assert!(to_signed::(b" 42").is_err()); // leading space +} diff --git a/gix-utils/tests/utils.rs b/gix-utils/tests/utils.rs index b48d9dd0de5..161a1191519 100644 --- a/gix-utils/tests/utils.rs +++ b/gix-utils/tests/utils.rs @@ -1,3 +1,4 @@ mod backoff; +mod btoi; mod buffers; mod str;