From e65bfa72a23c57fbc05cad66c9b667c6eae946fa Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 29 Feb 2024 01:39:19 +0100 Subject: [PATCH 1/3] decode: Add const-compatible decoder --- src/decode_const.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 42 ++++++++++++++++ tests/decode.rs | 20 ++++++++ 3 files changed, 179 insertions(+) create mode 100644 src/decode_const.rs diff --git a/src/decode_const.rs b/src/decode_const.rs new file mode 100644 index 0000000..57363b2 --- /dev/null +++ b/src/decode_const.rs @@ -0,0 +1,117 @@ +//! Functions for decoding Base58 encoded strings in a const context. + +use crate::Alphabet; + +/// A builder for setting up the alphabet and output of a base58 decode. +/// +/// See the documentation for [`bs58::decode_const`](crate::decode_const()) for +/// a more high level view of how to use this. +#[allow(missing_debug_implementations)] +pub struct DecodeBuilder<'a, 'b> { + input: &'a [u8], + alpha: &'b Alphabet, +} + +impl<'a, 'b> DecodeBuilder<'a, 'b> { + /// Setup decoder for the given string using the given alphabet. + /// Preferably use [`bs58::decode_const`](crate::decode_const()) instead of + /// this directly. + pub const fn new(input: &'a [u8], alpha: &'b Alphabet) -> Self { + Self { input, alpha } + } + + /// Setup decoder for the given string using default prepared alphabet. + pub(crate) const fn from_input(input: &'a [u8]) -> Self { + Self { + input, + alpha: Alphabet::DEFAULT, + } + } + + /// Change the alphabet that will be used for decoding. + /// + /// # Examples + /// + /// ```rust + /// assert_eq!( + /// vec![0x60, 0x65, 0xe7, 0x9b, 0xba, 0x2f, 0x78], + /// bs58::decode_const(b"he11owor1d") + /// .with_alphabet(bs58::Alphabet::RIPPLE) + /// .into_array::<7>()); + /// ``` + pub const fn with_alphabet(self, alpha: &'b Alphabet) -> Self { + Self { alpha, ..self } + } + + /// Decode into a new array. + /// + /// Returns the decoded array as bytes. + /// + /// See the documentation for [`bs58::decode_const`](crate::decode_const()) + /// for an explanation of the errors that may occur. + /// + /// # Examples + /// + /// ```rust + /// let output = bs58::decode_const(b"EUYUqQf").into_array::<5>(); + /// assert_eq!(output.len(), 5); + /// assert_eq!("world", std::str::from_utf8(&output)?); + /// # Ok::<(), std::str::Utf8Error>(()) + /// ``` + pub const fn into_array(&self) -> [u8; N] { + decode_into::(self.input, self.alpha) + } +} + +const fn decode_into(input: &[u8], alpha: &Alphabet) -> [u8; N] { + let mut output = [0u8; N]; + let mut index = 0; + let zero = alpha.encode[0]; + + let mut i = 0; + while i < input.len() { + let c = input[i]; + assert!(c < 128, "provided string contained a non-ascii character"); + + let mut val = alpha.decode[c as usize] as usize; + assert!( + val != 0xFF, + "provided string contained an invalid character" + ); + + let mut j = 0; + while j < index { + let byte = output[j]; + val += (byte as usize) * 58; + output[j] = (val & 0xFF) as u8; + val >>= 8; + j += 1; + } + + while val > 0 { + output[index] = (val & 0xFF) as u8; + index += 1; + val >>= 8 + } + i += 1; + } + + let mut i = 0; + while i < input.len() && input[i] == zero { + output[index] = 0; + index += 1; + i += 1; + } + + // reverse + let mut i = 0; + let n = index / 2; + while i < n { + let x = output[i]; + output[i] = output[index - 1 - i]; + output[index - 1 - i] = x; + i += 1; + } + + output +} diff --git a/src/lib.rs b/src/lib.rs index 8c545b9..5e975ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,7 @@ pub mod alphabet; pub use alphabet::Alphabet; pub mod decode; +pub mod decode_const; pub mod encode; #[cfg(any(feature = "check", feature = "cb58"))] @@ -171,6 +172,47 @@ pub fn decode>(input: I) -> decode::DecodeBuilder<'static, I> { decode::DecodeBuilder::from_input(input) } +/// Setup decoder for the given string using the [default alphabet][Alphabet::DEFAULT]. +/// +/// Usable in `const` contexts, so the size of the output array must be specified. +/// +/// # Examples +/// +/// ## Basic example +/// +/// ```rust +/// assert_eq!( +/// vec![0x04, 0x30, 0x5e, 0x2b, 0x24, 0x73, 0xf0, 0x58], +/// bs58::decode_const(b"he11owor1d").into_array::<8>()); +/// ``` +/// +/// ## Changing the alphabet +/// +/// ```rust +/// assert_eq!( +/// vec![0x60, 0x65, 0xe7, 0x9b, 0xba, 0x2f, 0x78], +/// bs58::decode_const(b"he11owor1d") +/// .with_alphabet(bs58::Alphabet::RIPPLE) +/// .into_array::<7>()); +/// ``` +/// +/// ## Errors +/// +/// ### Invalid Character +/// +/// ```should_panic +/// bs58::decode_const(b"hello world").into_array::<10>(); +/// ``` +/// +/// ### Non-ASCII Character +/// +/// ```should_panic +/// bs58::decode_const("he11o🇳🇿".as_bytes()).into_array::<10>(); +/// ``` +pub const fn decode_const(input: &[u8]) -> decode_const::DecodeBuilder<'_, '_> { + decode_const::DecodeBuilder::from_input(input) +} + /// Setup encoder for the given bytes using the [default alphabet][Alphabet::DEFAULT]. /// /// # Examples diff --git a/tests/decode.rs b/tests/decode.rs index 4a253fd..5680f7c 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -16,6 +16,13 @@ fn test_decode() { assert_eq!((PREFIX, val), vec.split_at(3)); } + { + let vec = bs58::decode_const(s.as_bytes()).into_array::<128>(); + let mut check = [0; 128]; + check[..val.len()].copy_from_slice(val); + assert_eq!(vec, check); + } + #[cfg(feature = "smallvec")] { let mut vec = smallvec::SmallVec::<[u8; 36]>::from(PREFIX); @@ -67,6 +74,19 @@ fn test_decode_small_buffer_err() { ); } +#[test] +#[should_panic] +fn test_decode_const_small_buffer_panic() { + bs58::decode_const(b"a3gV").into_array::<2>(); +} + +#[test] +#[should_panic] +fn test_decode_const_invalid_char_panic() { + let sample = "123456789abcd!efghij"; + let _ = bs58::decode_const(sample.as_bytes()).into_array::<32>(); +} + #[test] fn test_decode_invalid_char() { let sample = "123456789abcd!efghij"; From e18e057bf86e67e028ed6da0ee4f1850978d2301 Mon Sep 17 00:00:00 2001 From: Wim Looman Date: Sun, 10 Mar 2024 12:31:48 +0100 Subject: [PATCH 2/3] Move const-compatible API onto `decode::DecodeBuilder` directly --- src/decode.rs | 155 ++++++++++++++++++++++++++++++++++++++++++-- src/decode_const.rs | 117 --------------------------------- src/lib.rs | 44 +------------ tests/decode.rs | 6 +- 4 files changed, 154 insertions(+), 168 deletions(-) delete mode 100644 src/decode_const.rs diff --git a/src/decode.rs b/src/decode.rs index ab43a27..9463bfd 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -196,7 +196,7 @@ impl DecodeTarget for [u8; N] { impl<'a, I: AsRef<[u8]>> DecodeBuilder<'a, I> { /// Setup decoder for the given string using the given alphabet. /// Preferably use [`bs58::decode`](crate::decode()) instead of this directly. - pub fn new(input: I, alpha: &'a Alphabet) -> DecodeBuilder<'a, I> { + pub const fn new(input: I, alpha: &'a Alphabet) -> DecodeBuilder<'a, I> { DecodeBuilder { input, alpha, @@ -205,7 +205,7 @@ impl<'a, I: AsRef<[u8]>> DecodeBuilder<'a, I> { } /// Setup decoder for the given string using default prepared alphabet. - pub(crate) fn from_input(input: I) -> DecodeBuilder<'static, I> { + pub(crate) const fn from_input(input: I) -> DecodeBuilder<'static, I> { DecodeBuilder { input, alpha: Alphabet::DEFAULT, @@ -225,8 +225,9 @@ impl<'a, I: AsRef<[u8]>> DecodeBuilder<'a, I> { /// .into_vec()?); /// # Ok::<(), bs58::decode::Error>(()) /// ``` - pub fn with_alphabet(self, alpha: &'a Alphabet) -> DecodeBuilder<'a, I> { - DecodeBuilder { alpha, ..self } + pub const fn with_alphabet(mut self, alpha: &'a Alphabet) -> DecodeBuilder<'a, I> { + self.alpha = alpha; + self } /// Expect and check checksum using the [Base58Check][] algorithm when @@ -276,7 +277,6 @@ impl<'a, I: AsRef<[u8]>> DecodeBuilder<'a, I> { let check = Check::CB58(expected_ver); DecodeBuilder { check, ..self } } - /// Decode into a new vector of bytes. /// /// See the documentation for [`bs58::decode`](crate::decode()) for an @@ -348,6 +348,66 @@ impl<'a, I: AsRef<[u8]>> DecodeBuilder<'a, I> { } } +/// For `const` compatibility we are restricted to using a concrete input and output type, as +/// `const` trait implementations and `&mut` are unstable. These methods will eventually be +/// deprecated once the primary interfaces can be converted into `const fn` directly. +impl<'a, 'b> DecodeBuilder<'a, &'b [u8]> { + /// Decode into a new array. + /// + /// Returns the decoded array as bytes. + /// + /// See the documentation for [`bs58::decode`](crate::decode()) + /// for an explanation of the errors that may occur. + /// + /// # Examples + /// + /// ```rust + /// const _: () = { + /// let Ok(output) = bs58::decode(b"EUYUqQf".as_slice()).into_array_const::<5>() else { + /// panic!() + /// }; + /// assert!(matches!(&output, b"world")); + /// }; + /// ``` + pub const fn into_array_const(self) -> Result<[u8; N]> { + assert!( + matches!(self.check, Check::Disabled), + "checksums in const aren't supported (why are you using this API at runtime)", + ); + decode_into_const(self.input, self.alpha) + } + + /// [`Self::into_array_const`] but the result will be unwrapped, turning any error into a panic + /// message via [`Error::unwrap_const`], as a simple `into_array_const().unwrap()` isn't + /// possible yet. + /// + /// # Examples + /// + /// ```rust + /// const _: () = { + /// let output: [u8; 5] = bs58::decode(b"EUYUqQf".as_slice()).into_array_const_unwrap(); + /// assert!(matches!(&output, b"world")); + /// }; + /// ``` + /// + /// ```rust + /// const _: () = { + /// assert!(matches!( + /// bs58::decode(b"he11owor1d".as_slice()) + /// .with_alphabet(bs58::Alphabet::RIPPLE) + /// .into_array_const_unwrap(), + /// [0x60, 0x65, 0xe7, 0x9b, 0xba, 0x2f, 0x78], + /// )); + /// }; + /// ``` + pub const fn into_array_const_unwrap(self) -> [u8; N] { + match self.into_array_const() { + Ok(result) => result, + Err(err) => err.unwrap_const(), + } + } +} + fn decode_into(input: &[u8], output: &mut [u8], alpha: &Alphabet) -> Result { let mut index = 0; let zero = alpha.encode[0]; @@ -480,6 +540,69 @@ fn decode_cb58_into( } } +const fn decode_into_const(input: &[u8], alpha: &Alphabet) -> Result<[u8; N]> { + let mut output = [0u8; N]; + let mut index = 0; + let zero = alpha.encode[0]; + + let mut i = 0; + while i < input.len() { + let c = input[i]; + if c > 127 { + return Err(Error::NonAsciiCharacter { index: i }); + } + + let mut val = alpha.decode[c as usize] as usize; + if val == 0xFF { + return Err(Error::InvalidCharacter { + character: c as char, + index: i, + }); + } + + let mut j = 0; + while j < index { + let byte = output[j]; + val += (byte as usize) * 58; + output[j] = (val & 0xFF) as u8; + val >>= 8; + j += 1; + } + + while val > 0 { + if index >= output.len() { + return Err(Error::BufferTooSmall); + } + output[index] = (val & 0xFF) as u8; + index += 1; + val >>= 8 + } + i += 1; + } + + let mut i = 0; + while i < input.len() && input[i] == zero { + if index >= output.len() { + return Err(Error::BufferTooSmall); + } + output[index] = 0; + index += 1; + i += 1; + } + + // reverse + let mut i = 0; + let n = index / 2; + while i < n { + let x = output[i]; + output[i] = output[index - 1 - i]; + output[index - 1 - i] = x; + i += 1; + } + + Ok(output) +} + #[cfg(feature = "std")] impl std::error::Error for Error {} @@ -520,3 +643,25 @@ impl fmt::Display for Error { } } } + +impl Error { + /// Panic with an error message based on this error. This cannot include any of the dynamic + /// content because formatting in `const` is not yet possible. + pub const fn unwrap_const(self) -> ! { + match self { + Error::BufferTooSmall => { + panic!("buffer provided to decode base58 encoded string into was too small") + } + Error::InvalidCharacter { .. } => panic!("provided string contained invalid character"), + Error::NonAsciiCharacter { .. } => { + panic!("provided string contained non-ascii character") + } + #[cfg(any(feature = "check", feature = "cb58"))] + Error::InvalidChecksum { .. } => panic!("invalid checksum"), + #[cfg(any(feature = "check", feature = "cb58"))] + Error::InvalidVersion { .. } => panic!("invalid version"), + #[cfg(any(feature = "check", feature = "cb58"))] + Error::NoChecksum => panic!("provided string is too small to contain a checksum"), + } + } +} diff --git a/src/decode_const.rs b/src/decode_const.rs deleted file mode 100644 index 57363b2..0000000 --- a/src/decode_const.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Functions for decoding Base58 encoded strings in a const context. - -use crate::Alphabet; - -/// A builder for setting up the alphabet and output of a base58 decode. -/// -/// See the documentation for [`bs58::decode_const`](crate::decode_const()) for -/// a more high level view of how to use this. -#[allow(missing_debug_implementations)] -pub struct DecodeBuilder<'a, 'b> { - input: &'a [u8], - alpha: &'b Alphabet, -} - -impl<'a, 'b> DecodeBuilder<'a, 'b> { - /// Setup decoder for the given string using the given alphabet. - /// Preferably use [`bs58::decode_const`](crate::decode_const()) instead of - /// this directly. - pub const fn new(input: &'a [u8], alpha: &'b Alphabet) -> Self { - Self { input, alpha } - } - - /// Setup decoder for the given string using default prepared alphabet. - pub(crate) const fn from_input(input: &'a [u8]) -> Self { - Self { - input, - alpha: Alphabet::DEFAULT, - } - } - - /// Change the alphabet that will be used for decoding. - /// - /// # Examples - /// - /// ```rust - /// assert_eq!( - /// vec![0x60, 0x65, 0xe7, 0x9b, 0xba, 0x2f, 0x78], - /// bs58::decode_const(b"he11owor1d") - /// .with_alphabet(bs58::Alphabet::RIPPLE) - /// .into_array::<7>()); - /// ``` - pub const fn with_alphabet(self, alpha: &'b Alphabet) -> Self { - Self { alpha, ..self } - } - - /// Decode into a new array. - /// - /// Returns the decoded array as bytes. - /// - /// See the documentation for [`bs58::decode_const`](crate::decode_const()) - /// for an explanation of the errors that may occur. - /// - /// # Examples - /// - /// ```rust - /// let output = bs58::decode_const(b"EUYUqQf").into_array::<5>(); - /// assert_eq!(output.len(), 5); - /// assert_eq!("world", std::str::from_utf8(&output)?); - /// # Ok::<(), std::str::Utf8Error>(()) - /// ``` - pub const fn into_array(&self) -> [u8; N] { - decode_into::(self.input, self.alpha) - } -} - -const fn decode_into(input: &[u8], alpha: &Alphabet) -> [u8; N] { - let mut output = [0u8; N]; - let mut index = 0; - let zero = alpha.encode[0]; - - let mut i = 0; - while i < input.len() { - let c = input[i]; - assert!(c < 128, "provided string contained a non-ascii character"); - - let mut val = alpha.decode[c as usize] as usize; - assert!( - val != 0xFF, - "provided string contained an invalid character" - ); - - let mut j = 0; - while j < index { - let byte = output[j]; - val += (byte as usize) * 58; - output[j] = (val & 0xFF) as u8; - val >>= 8; - j += 1; - } - - while val > 0 { - output[index] = (val & 0xFF) as u8; - index += 1; - val >>= 8 - } - i += 1; - } - - let mut i = 0; - while i < input.len() && input[i] == zero { - output[index] = 0; - index += 1; - i += 1; - } - - // reverse - let mut i = 0; - let n = index / 2; - while i < n { - let x = output[i]; - output[i] = output[index - 1 - i]; - output[index - 1 - i] = x; - i += 1; - } - - output -} diff --git a/src/lib.rs b/src/lib.rs index 5e975ef..cbeee1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,6 @@ pub mod alphabet; pub use alphabet::Alphabet; pub mod decode; -pub mod decode_const; pub mod encode; #[cfg(any(feature = "check", feature = "cb58"))] @@ -168,51 +167,10 @@ enum Check { /// bs58::decode::Error::BufferTooSmall, /// bs58::decode("he11owor1d").onto(&mut output).unwrap_err()); /// ``` -pub fn decode>(input: I) -> decode::DecodeBuilder<'static, I> { +pub const fn decode>(input: I) -> decode::DecodeBuilder<'static, I> { decode::DecodeBuilder::from_input(input) } -/// Setup decoder for the given string using the [default alphabet][Alphabet::DEFAULT]. -/// -/// Usable in `const` contexts, so the size of the output array must be specified. -/// -/// # Examples -/// -/// ## Basic example -/// -/// ```rust -/// assert_eq!( -/// vec![0x04, 0x30, 0x5e, 0x2b, 0x24, 0x73, 0xf0, 0x58], -/// bs58::decode_const(b"he11owor1d").into_array::<8>()); -/// ``` -/// -/// ## Changing the alphabet -/// -/// ```rust -/// assert_eq!( -/// vec![0x60, 0x65, 0xe7, 0x9b, 0xba, 0x2f, 0x78], -/// bs58::decode_const(b"he11owor1d") -/// .with_alphabet(bs58::Alphabet::RIPPLE) -/// .into_array::<7>()); -/// ``` -/// -/// ## Errors -/// -/// ### Invalid Character -/// -/// ```should_panic -/// bs58::decode_const(b"hello world").into_array::<10>(); -/// ``` -/// -/// ### Non-ASCII Character -/// -/// ```should_panic -/// bs58::decode_const("he11o🇳🇿".as_bytes()).into_array::<10>(); -/// ``` -pub const fn decode_const(input: &[u8]) -> decode_const::DecodeBuilder<'_, '_> { - decode_const::DecodeBuilder::from_input(input) -} - /// Setup encoder for the given bytes using the [default alphabet][Alphabet::DEFAULT]. /// /// # Examples diff --git a/tests/decode.rs b/tests/decode.rs index 5680f7c..e466867 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -17,7 +17,7 @@ fn test_decode() { } { - let vec = bs58::decode_const(s.as_bytes()).into_array::<128>(); + let vec = bs58::decode(s.as_bytes()).into_array_const_unwrap::<128>(); let mut check = [0; 128]; check[..val.len()].copy_from_slice(val); assert_eq!(vec, check); @@ -77,14 +77,14 @@ fn test_decode_small_buffer_err() { #[test] #[should_panic] fn test_decode_const_small_buffer_panic() { - bs58::decode_const(b"a3gV").into_array::<2>(); + bs58::decode(&b"a3gV"[..]).into_array_const_unwrap::<2>(); } #[test] #[should_panic] fn test_decode_const_invalid_char_panic() { let sample = "123456789abcd!efghij"; - let _ = bs58::decode_const(sample.as_bytes()).into_array::<32>(); + let _ = bs58::decode(sample.as_bytes()).into_array_const_unwrap::<32>(); } #[test] From b6ad26a72010dec7caf18cf4cb4e1e7131ef57e6 Mon Sep 17 00:00:00 2001 From: Wim Looman Date: Sun, 10 Mar 2024 13:53:22 +0100 Subject: [PATCH 3/3] Prepare to release 0.5.1 --- CHANGELOG.md | 4 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- foo | 0 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 foo diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ede6b..af96814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.5.1 - 2024-03-19 + + * Make it possible to decode in `const`-context (by @joncinque) + ## 0.5.0 - 2023-05-23 * Breaking change: make encoding onto resizable buffers not clear them, instead appending onto any existing data diff --git a/Cargo.lock b/Cargo.lock index f521b49..92917b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ [[package]] name = "bs58" -version = "0.5.0" +version = "0.5.1" dependencies = [ "assert_matches", "base58", diff --git a/Cargo.toml b/Cargo.toml index 4bc8ed4..d673605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bs58" -version = "0.5.0" +version = "0.5.1" description = "Another Base58 codec implementation." repository = "https://github.com/Nullus157/bs58-rs" readme = "README.md" diff --git a/foo b/foo new file mode 100644 index 0000000..e69de29