From 4b0c640727c1a7158206a225f8ac20b486a4b949 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 22 Sep 2022 21:05:20 -0400 Subject: [PATCH] Allow doing more things without alloc Co-Authored-By: Sebastian --- .github/workflows/rust.yml | 11 +- Cargo.toml | 5 +- contrib/test.sh | 45 +++- embedded/no-allocator/Cargo.toml | 16 +- embedded/no-allocator/src/main.rs | 50 +++- embedded/with-allocator/Cargo.toml | 2 +- src/lib.rs | 407 ++++++++++++++++++++++++----- 7 files changed, 435 insertions(+), 101 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a5c95bf61..a9e98ab82 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -88,7 +88,14 @@ jobs: steps: - name: Checkout Crate uses: actions/checkout@v3 + - name: Set up QEMU + run: sudo apt update && sudo apt install -y qemu-system-arm gcc-arm-none-eabi - name: Checkout Toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@nightly + with: + targets: thumbv7m-none-eabi - name: Run - run: cd embedded/no-allocator && cargo rustc -- -C link-arg=-nostartfiles + env: + RUSTFLAGS: "-C link-arg=-Tlink.x" + CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER: "qemu-system-arm -cpu cortex-m3 -machine mps2-an385 -nographic -semihosting-config enable=on,target=native -kernel" + run: cd embedded/no-allocator && cargo run --target thumbv7m-none-eabi diff --git a/Cargo.toml b/Cargo.toml index 14e8830a4..503f17e56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,7 @@ alloc = [] # Only for CI to make all warnings errors, do not activate otherwise (may break forward compatibility) strict = [] -[dependencies] +[dependencies.arrayvec] +version = "0.7.1" +default-features = false +optional = true diff --git a/contrib/test.sh b/contrib/test.sh index 4ad7facc2..17967247f 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -4,28 +4,31 @@ # # The "strict" feature is used to configure cargo to deny all warnings, always use it in test runs. -set -ex - -FEATURES="std alloc" +set -eu +set -x # Some tests require certain toolchain types. NIGHTLY=false +MIN_VERSION=false if cargo --version | grep nightly; then NIGHTLY=true fi +if cargo --version | grep 1.41; then + MIN_VERSION=true +fi # Sanity, check tools exist. cargo --version rustc --version # Run the linter if told to. -if [ "$DO_LINT" = true ] +if [ "${DO_LINT-false}" = true ] then cargo clippy --all-features --all-targets -- -D warnings fi # Run formatter if told to. -if [ "$DO_FMT" = true ]; then +if [ "${DO_FMT-false}" = true ]; then if [ "$NIGHTLY" = false ]; then echo "DO_FMT requires a nightly toolchain (consider using RUSTUP_TOOLCHAIN)" exit 1 @@ -34,16 +37,34 @@ if [ "$DO_FMT" = true ]; then cargo fmt --check fi +check () { + cargo build --no-default-features --features="strict $1" + cargo test --no-default-features --features="strict $1" +} + # Check without features ("strict" is a CI feature only, see above). -cargo build --no-default-features --features="strict" -cargo test --no-default-features --features="strict" +check "" -# Check "std" feature (implies "alloc", so this is equivalent to --all-features). -cargo build --no-default-features --features="strict std" -cargo test --no-default-features --features="strict std" +# Check "arrayvec" feature alone. +if [ "$MIN_VERSION" != true ]; then + check "arrayvec" +fi # Check "alloc" feature alone. -cargo build --no-default-features --features="strict alloc" -cargo test --no-default-features --features="strict alloc" +check "alloc" + +# Check "alloc" & "arrayvec" features together. +if [ "$MIN_VERSION" != true ]; then + check "alloc arrayvec" +fi + +# Check "std" feature (implies "alloc", so this is equivalent to --all-features). +cargo build --no-default-features --features="std" +cargo test --no-default-features --features="std" + +# Check "std" & "arrayvec" features together. +if [ "$MIN_VERSION" != true ]; then + check "std arrayvec" +fi exit 0 diff --git a/embedded/no-allocator/Cargo.toml b/embedded/no-allocator/Cargo.toml index 72f6fda4a..b1836ba81 100644 --- a/embedded/no-allocator/Cargo.toml +++ b/embedded/no-allocator/Cargo.toml @@ -5,11 +5,21 @@ readme = "README.md" name = "no-allocator" version = "0.1.0" +[dependencies] +cortex-m = "0.6.0" +cortex-m-rt = "0.6.10" +cortex-m-semihosting = "0.3.3" +panic-halt = "0.2.0" +arrayvec = { version = "0.7.1", default-features = false } +bech32 = { path = "../../", default-features = false, features = ["arrayvec"] } + +[[bin]] +name = "no-allocator" +test = false +bench = false + [profile.dev] panic = "abort" [profile.release] panic = "abort" - -[dependencies] -bech32 = { path = "../../", default_features = false } diff --git a/embedded/no-allocator/src/main.rs b/embedded/no-allocator/src/main.rs index d723e82b8..1f7de549b 100644 --- a/embedded/no-allocator/src/main.rs +++ b/embedded/no-allocator/src/main.rs @@ -3,24 +3,52 @@ //! Build with: `cargo rustc -- -C link-arg=-nostartfiles`. //! -#![no_std] +#![feature(alloc_error_handler)] #![no_main] +#![no_std] + +use panic_halt as _; -use core::panic::PanicInfo; +use arrayvec::{ArrayString, ArrayVec}; +use bech32::{self, u5, ComboError, FromBase32, ToBase32, Variant}; +use cortex_m_rt::entry; +use cortex_m_semihosting::{debug, hprintln}; // Note: `#[global_allocator]` is NOT set. -#[allow(unused_imports)] -use bech32; +#[entry] +fn main() -> ! { + let mut encoded = ArrayString::<30>::new(); + + let mut base32 = ArrayVec::::new(); + + [0x00u8, 0x01, 0x02].write_base32(&mut base32).unwrap(); + + bech32::encode_to_fmt_anycase(&mut encoded, "bech32", &base32, Variant::Bech32) + .unwrap() + .unwrap(); + test(&*encoded == "bech321qqqsyrhqy2a"); + + hprintln!("{}", encoded).unwrap(); + + let mut decoded = ArrayVec::::new(); + + let mut scratch = ArrayVec::::new(); + + let (hrp, data, variant) = + bech32::decode_lowercase::(&encoded, &mut decoded, &mut scratch).unwrap(); + test(hrp == "bech32"); + let res = ArrayVec::::from_base32(&data).unwrap(); + test(&res == [0x00, 0x01, 0x02].as_ref()); + test(variant == Variant::Bech32); + + debug::exit(debug::EXIT_SUCCESS); -/// This function is called on panic, defining this ensures build will fail if `std` is enabled -/// because `panic` will be defined twice. -#[panic_handler] -fn panic(_info: &PanicInfo) -> ! { loop {} } -#[no_mangle] -pub extern "C" fn _start() -> ! { - loop {} +fn test(result: bool) { + if !result { + debug::exit(debug::EXIT_FAILURE); + } } diff --git a/embedded/with-allocator/Cargo.toml b/embedded/with-allocator/Cargo.toml index b6c0615cf..8c55ed649 100644 --- a/embedded/with-allocator/Cargo.toml +++ b/embedded/with-allocator/Cargo.toml @@ -11,7 +11,7 @@ cortex-m-rt = "0.6.10" cortex-m-semihosting = "0.3.3" panic-halt = "0.2.0" alloc-cortex-m = "0.4.1" -bech32 = { path="../../", default-features = false, features = ["alloc"] } +bech32 = { path = "../../", default-features = false, features = ["alloc"] } [[bin]] name = "with-allocator" diff --git a/src/lib.rs b/src/lib.rs index ce34b2b11..1558a694d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,19 +63,20 @@ extern crate alloc; #[cfg(any(test, feature = "std"))] extern crate core; -#[cfg(all(feature = "alloc", not(feature = "std")))] +#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))] use alloc::borrow::Cow; -#[cfg(all(feature = "alloc", not(feature = "std")))] +#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))] use alloc::{string::String, vec::Vec}; -#[cfg(feature = "alloc")] -use core::convert::Infallible; -use core::convert::TryFrom; +use core::convert::{Infallible, TryFrom}; use core::{fmt, mem}; -#[cfg(feature = "std")] +#[cfg(any(feature = "std", all(feature = "alloc", test)))] use std::borrow::Cow; mod error; +#[cfg(feature = "arrayvec")] +use arrayvec::{ArrayVec, CapacityError}; + /// Integer in the range `0..32` #[derive(PartialEq, Eq, Debug, Copy, Clone, Default, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types)] @@ -151,10 +152,10 @@ impl AsRef for u5 { /// Interface to write `u5`s into a sink pub trait WriteBase32 { /// Write error - type Err: fmt::Debug; + type Error: fmt::Debug; /// Write a `u5` slice - fn write(&mut self, data: &[u5]) -> Result<(), Self::Err> { + fn write(&mut self, data: &[u5]) -> Result<(), Self::Error> { for b in data { self.write_u5(*b)?; } @@ -162,7 +163,26 @@ pub trait WriteBase32 { } /// Write a single `u5` - fn write_u5(&mut self, data: u5) -> Result<(), Self::Err>; + fn write_u5(&mut self, data: u5) -> Result<(), Self::Error> { self.write(&[data]) } +} + +/// Interface to write `u8`s into a sink +/// +/// Like `std::io::Writer`, but because the associated type is no_std compatible. +pub trait WriteBase256 { + /// Write error + type Error: fmt::Debug; + + /// Write a `u8` slice + fn write(&mut self, data: &[u8]) -> Result<(), Self::Error> { + for b in data { + self.write_u8(*b)?; + } + Ok(()) + } + + /// Write a single `u8` + fn write_u8(&mut self, data: u8) -> Result<(), Self::Error> { self.write(&[data]) } } const CHECKSUM_LENGTH: usize = 6; @@ -237,7 +257,7 @@ impl<'a> Bech32Writer<'a> { } impl<'a> WriteBase32 for Bech32Writer<'a> { - type Err = fmt::Error; + type Error = fmt::Error; /// Writes a single 5 bit value of the data part fn write_u5(&mut self, data: u5) -> fmt::Result { @@ -256,40 +276,93 @@ impl<'a> Drop for Bech32Writer<'a> { /// `ToBase32`. pub trait FromBase32: Sized { /// The associated error which can be returned from parsing (e.g. because of bad padding). - type Err; + type Error; /// Convert a base32 slice to `Self`. - fn from_base32(b32: &[u5]) -> Result; + fn from_base32(b32: &[u5]) -> Result; } -#[cfg(feature = "alloc")] -impl WriteBase32 for Vec { - type Err = Infallible; +macro_rules! write_base_n { + { $tr:ident, $ty:ident, $meth:ident } => { + #[cfg(feature = "arrayvec")] + impl $tr for ArrayVec<$ty, L> { + type Error = CapacityError; - fn write(&mut self, data: &[u5]) -> Result<(), Self::Err> { - self.extend_from_slice(data); - Ok(()) + fn write(&mut self, data: &[$ty]) -> Result<(), Self::Error> { + self.try_extend_from_slice(data)?; + Ok(()) + } + + fn $meth(&mut self, data: $ty) -> Result<(), Self::Error> { + self.push(data); + Ok(()) + } + } + + #[cfg(feature = "alloc")] + impl $tr for Vec<$ty> { + type Error = Infallible; + + fn write(&mut self, data: &[$ty]) -> Result<(), Self::Error> { + self.extend_from_slice(data); + Ok(()) + } + + fn $meth(&mut self, data: $ty) -> Result<(), Self::Error> { + self.push(data); + Ok(()) + } + } } +} - fn write_u5(&mut self, data: u5) -> Result<(), Self::Err> { - self.push(data); - Ok(()) +write_base_n! { WriteBase32, u5, write_u5 } +write_base_n! { WriteBase256, u8, write_u8 } + +#[cfg(feature = "arrayvec")] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +/// Combination of Errors for use with array vec +pub enum ComboError { + /// Error from this crate + Bech32Error(Error), + /// Error from `arrayvec`. + WriteError(CapacityError), +} +#[cfg(feature = "arrayvec")] +impl From for ComboError { + fn from(e: Error) -> ComboError { ComboError::Bech32Error(e) } +} +#[cfg(feature = "arrayvec")] +impl From for ComboError { + fn from(e: CapacityError) -> ComboError { ComboError::WriteError(e) } +} + +#[cfg(feature = "arrayvec")] +impl FromBase32 for ArrayVec { + type Error = ComboError; + + /// Convert base32 to base256, removes null-padding if present, returns + /// `Err(Error::InvalidPadding)` if padding bits are unequal `0` + fn from_base32(b32: &[u5]) -> Result { + let mut ret: ArrayVec = ArrayVec::new(); + convert_bits_in::(b32, 5, 8, false, &mut ret)?; + Ok(ret) } } #[cfg(feature = "alloc")] impl FromBase32 for Vec { - type Err = Error; + type Error = Error; /// Convert base32 to base256, removes null-padding if present, returns /// `Err(Error::InvalidPadding)` if padding bits are unequal `0` - fn from_base32(b32: &[u5]) -> Result { convert_bits(b32, 5, 8, false) } + fn from_base32(b32: &[u5]) -> Result { convert_bits(b32, 5, 8, false) } } /// A trait for converting a value to a type `T` that represents a `u5` slice. -#[cfg(feature = "alloc")] pub trait ToBase32 { /// Convert `Self` to base32 vector + #[cfg(feature = "alloc")] fn to_base32(&self) -> Vec { let mut vec = Vec::new(); self.write_base32(&mut vec).unwrap(); @@ -298,19 +371,21 @@ pub trait ToBase32 { /// Encode as base32 and write it to the supplied writer /// Implementations shouldn't allocate. - fn write_base32(&self, writer: &mut W) -> Result<(), ::Err>; + fn write_base32(&self, writer: &mut W) + -> Result<(), ::Error>; } /// Interface to calculate the length of the base32 representation before actually serializing -#[cfg(feature = "alloc")] pub trait Base32Len: ToBase32 { /// Calculate the base32 serialized length fn base32_len(&self) -> usize; } -#[cfg(feature = "alloc")] impl + ?Sized> ToBase32 for T { - fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + fn write_base32( + &self, + writer: &mut W, + ) -> Result<(), ::Error> { // Amount of bits left over from last round, stored in buffer. let mut buffer_bits = 0u32; // Holds all unwritten bits left over from last round. The bits are stored beginning from @@ -353,7 +428,6 @@ impl + ?Sized> ToBase32 for T { } } -#[cfg(feature = "alloc")] impl + ?Sized> Base32Len for T { fn base32_len(&self) -> usize { let bits = self.as_ref().len() * 8; @@ -367,28 +441,42 @@ impl + ?Sized> Base32Len for T { /// A trait to convert between u8 arrays and u5 arrays without changing the content of the elements, /// but checking that they are in range. -pub trait CheckBase32> { +pub trait CheckBase32 { /// Error type if conversion fails - type Err; + type Error; /// Check if all values are in range and return array-like struct of `u5` values - fn check_base32(self) -> Result; + fn check_base32(self) -> Result; } -#[cfg(feature = "alloc")] -impl> CheckBase32> for T { - type Err = Error; +impl> CheckBase32 for U +where + T: AsRef<[u5]>, + T: core::iter::FromIterator, +{ + type Error = Error; - fn check_base32(self) -> Result, Self::Err> { + fn check_base32(self) -> Result { self.as_ref() .iter() .map(|x| u5::try_from(*x).map_err(Error::TryFrom)) - .collect::, Error>>() + .collect::>() + } +} + +impl> CheckBase32<()> for U { + type Error = Error; + + fn check_base32(self) -> Result<(), Error> { + self.as_ref() + .iter() + .map(|x| u5::try_from(*x).map(|_| ()).map_err(Error::TryFrom)) + .find(|r| r.is_err()) + .unwrap_or(Ok(())) } } #[derive(Clone, Copy, PartialEq, Eq)] -#[cfg(feature = "alloc")] enum Case { Upper, Lower, @@ -401,7 +489,6 @@ enum Case { /// * **MixedCase**: If the HRP contains both uppercase and lowercase characters. /// * **InvalidChar**: If the HRP contains any non-ASCII characters (outside 33..=126). /// * **InvalidLength**: If the HRP is outside 1..83 characters long. -#[cfg(feature = "alloc")] fn check_hrp(hrp: &str) -> Result { if hrp.is_empty() || hrp.len() > 83 { return Err(Error::InvalidLength); @@ -453,7 +540,20 @@ pub fn encode_to_fmt>( Case::Lower | Case::None => Cow::Borrowed(hrp), }; - match Bech32Writer::new(&hrp_lower, variant, fmt) { + encode_to_fmt_anycase(fmt, &hrp_lower, data, variant) +} + +/// Encode a bech32 payload to an [fmt::Write], but with any case. +/// This method is intended for implementing traits from [core::fmt] without [std]. +/// +/// See `encode_to_fmt` for meaning of errors. +pub fn encode_to_fmt_anycase>( + fmt: &mut fmt::Write, + hrp: &str, + data: T, + variant: Variant, +) -> Result { + match Bech32Writer::new(hrp, variant, fmt) { Ok(mut writer) => { Ok(writer.write(data.as_ref()).and_then(|_| { // Finalize manually to avoid panic on drop if write fails @@ -471,12 +571,12 @@ pub fn encode_to_fmt>( /// * If [check_hrp] returns an error for the given HRP. /// # Deviations from standard /// * No length limits are enforced for the data part -#[cfg(feature = "alloc")] pub fn encode_without_checksum_to_fmt>( fmt: &mut fmt::Write, hrp: &str, data: T, ) -> Result { + #[cfg(feature = "alloc")] let hrp = match check_hrp(hrp)? { Case::Upper => Cow::Owned(hrp.to_lowercase()), Case::Lower | Case::None => Cow::Borrowed(hrp), @@ -510,7 +610,6 @@ const BECH32M_CONST: u32 = 0x2bc8_30a3; impl Variant { // Produce the variant based on the remainder of the polymod operation - #[cfg(feature = "alloc")] fn from_remainder(c: u32) -> Option { match c { BECH32_CONST => Some(Variant::Bech32), @@ -639,27 +738,108 @@ fn split_and_decode(s: &str) -> Result<(String, Vec), Error> { Ok((hrp_lower, data)) } -#[cfg(feature = "alloc")] -fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option { - let mut exp = hrp_expand(hrp); - exp.extend_from_slice(data); - Variant::from_remainder(polymod(&exp)) +// TODO deduplicate some +/// Decode a lowercase bech32 string into the raw HRP and the data bytes. +/// +/// Less flexible than [decode], but don't allocate. +pub fn decode_lowercase<'a, 'b, E, R, S>( + s: &'a str, + data: &'b mut R, + scratch: &mut S, +) -> Result<(&'a str, &'b [u5], Variant), E> +where + R: WriteBase32 + AsRef<[u5]>, + S: WriteBase32 + AsRef<[u5]>, + E: From, + E: From, + E: From, +{ + // Ensure overall length is within bounds + if s.len() < 8 { + Err(Error::InvalidLength)?; + } + + // Split at separator and check for two pieces + let (hrp_lower, raw_data) = match s.rfind(SEP) { + None => Err(Error::MissingSeparator)?, + Some(sep) => { + let (hrp, data) = s.split_at(sep); + (hrp, &data[1..]) + } + }; + if raw_data.len() < 6 { + Err(Error::InvalidLength)?; + } + + let case = match check_hrp(hrp_lower)? { + Case::Upper => Err(Error::MixedCase)?, + // already lowercase + Case::Lower | Case::None => Case::Lower, + }; + + // Check data payload + for c in raw_data.chars() { + // Only check if c is in the ASCII range, all invalid ASCII + // characters have the value -1 in CHARSET_REV (which covers + // the whole ASCII range) and will be filtered out later. + if !c.is_ascii() { + Err(Error::InvalidChar(c))?; + } + + match case { + Case::Upper => Err(Error::MixedCase)?, + Case::None | Case::Lower => {} + } + + // c should be <128 since it is in the ASCII range, CHARSET_REV.len() == 128 + let num_value = CHARSET_REV[c as usize]; + + if !(0..32).contains(&num_value) { + Err(Error::InvalidChar(c))?; + } + + data.write_u5( + u5::try_from(num_value as u8).expect("range checked above, num_value <= 31"), + )?; + } + + // Ensure checksum + let variant = verify_checksum_in(hrp_lower.as_bytes(), data.as_ref(), scratch)? + .ok_or(Error::MissingSeparator)?; + + let dbl: usize = data.as_ref().len(); + Ok((hrp_lower, &(*data).as_ref()[..dbl.saturating_sub(6)], variant)) } #[cfg(feature = "alloc")] -fn hrp_expand(hrp: &[u8]) -> Vec { +fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option { let mut v: Vec = Vec::new(); + match verify_checksum_in(hrp, data, &mut v) { + Ok(v) => v, + Err(e) => match e {}, + } +} + +fn verify_checksum_in(hrp: &[u8], data: &[u5], v: &mut T) -> Result, T::Error> +where + T: WriteBase32 + AsRef<[u5]>, +{ + hrp_expand_in(hrp, v)?; + v.write(data)?; + Ok(Variant::from_remainder(polymod(v.as_ref()))) +} + +fn hrp_expand_in(hrp: &[u8], v: &mut T) -> Result<(), T::Error> { for b in hrp { - v.push(u5::try_from(*b >> 5).expect("can't be out of range, max. 7")); + v.write_u5(u5::try_from(*b >> 5).expect("can't be out of range, max. 7"))?; } - v.push(u5::try_from(0).unwrap()); + v.write_u5(u5::try_from(0).unwrap())?; for b in hrp { - v.push(u5::try_from(*b & 0x1f).expect("can't be out of range, max. 31")); + v.write_u5(u5::try_from(*b & 0x1f).expect("can't be out of range, max. 31"))?; } - v + Ok(()) } -#[cfg(feature = "alloc")] fn polymod(values: &[u5]) -> u32 { let mut chk: u32 = 1; let mut b: u8; @@ -688,7 +868,6 @@ const CHARSET: [char; 32] = [ ]; /// Reverse character set. Maps ASCII byte -> CHARSET index on [0,31] -#[cfg(feature = "alloc")] const CHARSET_REV: [i8; 128] = [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, @@ -722,6 +901,10 @@ pub enum Error { TryFrom(TryFromIntError), } +impl From for Error { + fn from(v: Infallible) -> Self { match v {} } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Error::*; @@ -814,51 +997,95 @@ impl std::error::Error for TryFromIntError { pub fn convert_bits(data: &[T], from: u32, to: u32, pad: bool) -> Result, Error> where T: Into + Copy, +{ + let mut ret: Vec = Vec::new(); + convert_bits_in::(data, from, to, pad, &mut ret)?; + Ok(ret) +} + +/// Convert between bit sizes without allocating +/// +/// Like [convert_bits]. +pub fn convert_bits_in( + data: &[T], + from: u32, + to: u32, + pad: bool, + ret: &mut R, +) -> Result<(), E> +where + T: Into + Copy, + R: WriteBase256, + E: From, + E: From, { if from > 8 || to > 8 || from == 0 || to == 0 { panic!("convert_bits `from` and `to` parameters 0 or greater than 8"); } let mut acc: u32 = 0; let mut bits: u32 = 0; - let mut ret: Vec = Vec::new(); let maxv: u32 = (1 << to) - 1; for value in data { let v: u32 = u32::from(Into::::into(*value)); if (v >> from) != 0 { // Input value exceeds `from` bit size - return Err(Error::Overflow); + Err(Error::Overflow)?; } acc = (acc << from) | v; bits += from; while bits >= to { bits -= to; - ret.push(((acc >> bits) & maxv) as u8); + ret.write_u8(((acc >> bits) & maxv) as u8)?; } } if pad { if bits > 0 { - ret.push(((acc << (to - bits)) & maxv) as u8); + ret.write_u8(((acc << (to - bits)) & maxv) as u8)?; } } else if bits >= from || ((acc << (to - bits)) & maxv) != 0 { - return Err(Error::InvalidPadding); + Err(Error::InvalidPadding)?; } - Ok(ret) + Ok(()) } #[cfg(test)] -#[cfg(feature = "alloc")] // Note, all the unit tests currently require an allocator. mod tests { + #[cfg(feature = "arrayvec")] + use arrayvec::ArrayString; + use super::*; + trait TextExt { + fn check_base32_vec(self) -> Result, Error>; + } + impl> TextExt for U { + fn check_base32_vec(self) -> Result, Error> { self.check_base32() } + } + + #[test] + #[cfg(feature = "alloc")] + fn getters_in() { + let mut data_scratch = Vec::new(); + let mut scratch = Vec::new(); + let decoded = + decode_lowercase::("bc1sw50qa3jx3s", &mut data_scratch, &mut scratch) + .unwrap(); + let data = [16, 14, 20, 15, 0].check_base32_vec().unwrap(); + assert_eq!(decoded.0, "bc"); + assert_eq!(decoded.1, data.as_slice()); + } + #[test] + #[cfg(feature = "alloc")] fn getters() { let decoded = decode("BC1SW50QA3JX3S").unwrap(); - let data = [16, 14, 20, 15, 0].check_base32().unwrap(); + let data = [16, 14, 20, 15, 0].check_base32_vec().unwrap(); assert_eq!(&decoded.0, "bc"); assert_eq!(decoded.1, data.as_slice()); } #[test] + #[cfg(feature = "alloc")] fn valid_checksum() { let strings: Vec<&str> = vec!( // Bech32 @@ -888,6 +1115,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn invalid_strings() { let pairs: Vec<(&str, Error)> = vec!( (" 1nwldj5", @@ -948,6 +1176,7 @@ mod tests { #[test] #[allow(clippy::type_complexity)] + #[cfg(feature = "alloc")] fn valid_conversion() { // Set of [data, from_bits, to_bits, pad, result] let tests: Vec<(Vec, u32, u32, bool, Vec)> = vec![ @@ -969,6 +1198,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn invalid_conversion() { // Set of [data, from_bits, to_bits, pad, expected error] let tests: Vec<(Vec, u32, u32, bool, Error)> = vec![ @@ -984,6 +1214,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn convert_bits_invalid_bit_size() { use std::panic::{catch_unwind, set_hook, take_hook}; @@ -1001,37 +1232,40 @@ mod tests { #[test] fn check_base32() { - assert!([0u8, 1, 2, 30, 31].check_base32().is_ok()); - assert!([0u8, 1, 2, 30, 31, 32].check_base32().is_err()); - assert!([0u8, 1, 2, 30, 31, 255].check_base32().is_err()); + assert!([0u8, 1, 2, 30, 31].check_base32_vec().is_ok()); + assert!([0u8, 1, 2, 30, 31, 32].check_base32_vec().is_err()); + assert!([0u8, 1, 2, 30, 31, 255].check_base32_vec().is_err()); - assert!([1u8, 2, 3, 4].check_base32().is_ok()); + assert!([1u8, 2, 3, 4].check_base32_vec().is_ok()); assert_eq!( - [30u8, 31, 35, 20].check_base32(), + [30u8, 31, 35, 20].check_base32_vec(), Err(Error::TryFrom(TryFromIntError::PosOverflow)) ) } #[test] + #[cfg(feature = "alloc")] fn test_encode() { assert_eq!( - encode("", vec![1u8, 2, 3, 4].check_base32().unwrap(), Variant::Bech32), + encode("", vec![1u8, 2, 3, 4].check_base32_vec().unwrap(), Variant::Bech32), Err(Error::InvalidLength) ); } #[test] + #[cfg(feature = "alloc")] fn from_base32() { - assert_eq!(Vec::from_base32(&[0x1f, 0x1c].check_base32().unwrap()), Ok(vec![0xff])); + assert_eq!(Vec::from_base32(&[0x1f, 0x1c].check_base32_vec().unwrap()), Ok(vec![0xff])); assert_eq!( - Vec::from_base32(&[0x1f, 0x1f].check_base32().unwrap()), + Vec::from_base32(&[0x1f, 0x1f].check_base32_vec().unwrap()), Err(Error::InvalidPadding) ); } #[test] + #[cfg(feature = "alloc")] fn to_base32() { - assert_eq!([0xffu8].to_base32(), [0x1f, 0x1c].check_base32().unwrap()); + assert_eq!([0xffu8].to_base32(), [0x1f, 0x1c].check_base32_vec().unwrap()); } #[test] @@ -1051,6 +1285,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn write_with_checksum() { let hrp = "lnbc"; let data = "Hello World!".as_bytes().to_base32(); @@ -1068,6 +1303,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn write_without_checksum() { let hrp = "lnbc"; let data = "Hello World!".as_bytes().to_base32(); @@ -1084,6 +1320,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn write_with_checksum_on_drop() { let hrp = "lntb"; let data = "Hello World!".as_bytes().to_base32(); @@ -1100,6 +1337,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn roundtrip_without_checksum() { let hrp = "lnbc"; let data = "Hello World!".as_bytes().to_base32(); @@ -1113,6 +1351,7 @@ mod tests { } #[test] + #[cfg(feature = "alloc")] fn test_hrp_case() { // Tests for issue with HRP case checking being ignored for encoding let encoded_str = encode("HRP", [0x00, 0x00].to_base32(), Variant::Bech32).unwrap(); @@ -1128,4 +1367,30 @@ mod tests { assert!(u5::try_from(32_u64).is_err()); assert!(u5::try_from(32_u128).is_err()); } + + #[test] + #[cfg(feature = "arrayvec")] + fn test_arrayvec() { + let mut encoded = ArrayString::<30>::new(); + + let mut base32 = ArrayVec::::new(); + + [0x00u8, 0x01, 0x02].write_base32(&mut base32).unwrap(); + + encode_to_fmt_anycase(&mut encoded, "bech32", &base32, Variant::Bech32).unwrap().unwrap(); + assert_eq!(&*encoded, "bech321qqqsyrhqy2a"); + + println!("{}", encoded); + + let mut decoded = ArrayVec::::new(); + + let mut scratch = ArrayVec::::new(); + + let (hrp, data, variant) = + decode_lowercase::(&encoded, &mut decoded, &mut scratch).unwrap(); + assert_eq!(hrp, "bech32"); + let res = ArrayVec::::from_base32(data).unwrap(); + assert_eq!(&res, [0x00, 0x01, 0x02].as_ref()); + assert_eq!(variant, Variant::Bech32); + } }