Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decode: Add const-compatible decoder #116

Merged
merged 3 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Empty file added foo
Empty file.
155 changes: 150 additions & 5 deletions src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ impl<const N: usize> 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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<const N: usize>(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<const N: usize>(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<usize> {
let mut index = 0;
let zero = alpha.encode[0];
Expand Down Expand Up @@ -480,6 +540,69 @@ fn decode_cb58_into(
}
}

const fn decode_into_const<const N: usize>(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 {}

Expand Down Expand Up @@ -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"),
}
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ enum Check {
/// bs58::decode::Error::BufferTooSmall,
/// bs58::decode("he11owor1d").onto(&mut output).unwrap_err());
/// ```
pub fn decode<I: AsRef<[u8]>>(input: I) -> decode::DecodeBuilder<'static, I> {
pub const fn decode<I: AsRef<[u8]>>(input: I) -> decode::DecodeBuilder<'static, I> {
decode::DecodeBuilder::from_input(input)
}

Expand Down
20 changes: 20 additions & 0 deletions tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ fn test_decode() {
assert_eq!((PREFIX, val), vec.split_at(3));
}

{
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);
}

#[cfg(feature = "smallvec")]
{
let mut vec = smallvec::SmallVec::<[u8; 36]>::from(PREFIX);
Expand Down Expand Up @@ -67,6 +74,19 @@ fn test_decode_small_buffer_err() {
);
}

#[test]
#[should_panic]
fn test_decode_const_small_buffer_panic() {
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(sample.as_bytes()).into_array_const_unwrap::<32>();
}

#[test]
fn test_decode_invalid_char() {
let sample = "123456789abcd!efghij";
Expand Down
Loading