diff --git a/cli/src/main.rs b/cli/src/main.rs index 5cad0f7..2062d63 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -37,14 +37,11 @@ impl FromStr for Alphabet { "flickr" => Alphabet::Flickr, custom if custom.starts_with("custom(") && custom.ends_with(')') => { let alpha = custom.trim_start_matches("custom(").trim_end_matches(')'); - let bytes = alpha.as_bytes(); - if bytes.iter().any(|&c| c > 128) { - return Err(anyhow!("custom alphabet must be ASCII characters only")); - } - let bytes = bytes + let bytes = alpha + .as_bytes() .try_into() .context("custom alphabet is not 58 characters long")?; - Alphabet::Custom(bs58::Alphabet::new(bytes)) + Alphabet::Custom(bs58::Alphabet::new(bytes)?) } other => { return Err(anyhow!("'{}' is not a known alphabet", other)); diff --git a/src/alphabet.rs b/src/alphabet.rs index 14d9bd9..3a68fe4 100644 --- a/src/alphabet.rs +++ b/src/alphabet.rs @@ -1,21 +1,35 @@ +//! Support for configurable alphabets + use core::fmt; -/// Prepared Alphabet for [`EncodeBuilder`](crate::encode::EncodeBuilder) and -/// [`DecodeBuilder`](crate::decode::DecodeBuilder). +/// Prepared Alphabet for +/// [`EncodeBuilder::with_alphabet`](crate::encode::EncodeBuilder::with_alphabet) and +/// [`DecodeBuilder::with_alphabet`](crate::decode::DecodeBuilder::with_alphabet). #[derive(Clone, Copy)] pub struct Alphabet { pub(crate) encode: [u8; 58], pub(crate) decode: [u8; 128], } -impl fmt::Debug for Alphabet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Ok(s) = core::str::from_utf8(&self.encode) { - f.debug_tuple("Alphabet").field(&s).finish() - } else { - f.debug_tuple("Alphabet").field(&self.encode).finish() - } - } +/// Errors that could occur when preparing a Base58 alphabet. +#[non_exhaustive] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + /// The alphabet contained a duplicate character at at least 2 indexes. + DuplicateCharacter { + /// The duplicate character encountered. + character: char, + /// The first index the character was seen at. + first: usize, + /// The second index the character was seen at. + second: usize, + }, + + /// The alphabet contained a multi-byte (or non-utf8) character. + NonAsciiCharacter { + /// The index at which the non-ASCII character was seen. + index: usize, + }, } impl Alphabet { @@ -23,43 +37,159 @@ impl Alphabet { /// /// See pub const BITCOIN: &'static Self = - &Self::new(b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + &Self::new_unwrap(b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); /// Monero's alphabet as defined in this forum post. /// /// See pub const MONERO: &'static Self = - &Self::new(b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + &Self::new_unwrap(b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); /// Ripple's alphabet as defined in their wiki. /// /// See pub const RIPPLE: &'static Self = - &Self::new(b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"); + &Self::new_unwrap(b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"); /// Flickr's alphabet for creating short urls from photo ids. /// /// See pub const FLICKR: &'static Self = - &Self::new(b"123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"); + &Self::new_unwrap(b"123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"); /// The default alphabet used if none is given. Currently is the /// [`BITCOIN`](Self::BITCOIN) alphabet. pub const DEFAULT: &'static Self = Self::BITCOIN; - /// Create prepared alphabet. - pub const fn new(base: &[u8; 58]) -> Alphabet { + /// Create prepared alphabet, checks that the alphabet is pure ASCII and that there are no + /// duplicate characters, which would result in inconsistent encoding/decoding + /// + /// ```rust + /// let alpha = bs58::Alphabet::new( + /// b" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXY" + /// )?; + /// + /// let decoded = bs58::decode("he11owor1d") + /// .with_alphabet(bs58::Alphabet::RIPPLE) + /// .into_vec()?; + /// let encoded = bs58::encode(decoded) + /// .with_alphabet(&alpha) + /// .into_string(); + /// + /// assert_eq!("#ERRN)N RD", encoded); + /// # Ok::<(), Box>(()) + /// ``` + /// ## Errors + /// + /// ### Duplicate Character + /// + /// ```rust + /// let alpha = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + /// assert_eq!( + /// bs58::alphabet::Error::DuplicateCharacter { character: 'a', first: 0, second: 1 }, + /// bs58::Alphabet::new(alpha).unwrap_err()); + /// ``` + /// + /// ### Non-ASCII Character + /// + /// ```rust + /// let mut alpha = *b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + /// alpha[1] = 255; + /// assert_eq!( + /// bs58::alphabet::Error::NonAsciiCharacter { index: 1 }, + /// bs58::Alphabet::new(&alpha).unwrap_err()); + /// ``` + pub const fn new(base: &[u8; 58]) -> Result { let mut encode = [0x00; 58]; let mut decode = [0xFF; 128]; let mut i = 0; while i < encode.len() { + if base[i] >= 128 { + return Err(Error::NonAsciiCharacter { index: i }); + } + if decode[base[i] as usize] != 0xFF { + return Err(Error::DuplicateCharacter { + character: base[i] as char, + first: decode[base[i] as usize] as usize, + second: i, + }); + } encode[i] = base[i]; decode[base[i] as usize] = i as u8; i += 1; } - Alphabet { encode, decode } + Ok(Self { encode, decode }) + } + + /// Same as [`Self::new`], but gives a panic instead of an [`Err`] on bad input. + /// + /// Intended to support usage in `const` context until [`Result::unwrap`] is able to be called. + /// + /// ```rust + /// const ALPHA: &'static bs58::Alphabet = &bs58::Alphabet::new_unwrap( + /// b" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXY" + /// ); + /// + /// let decoded = bs58::decode("he11owor1d") + /// .with_alphabet(bs58::Alphabet::RIPPLE) + /// .into_vec()?; + /// let encoded = bs58::encode(decoded) + /// .with_alphabet(ALPHA) + /// .into_string(); + /// + /// assert_eq!("#ERRN)N RD", encoded); + /// # Ok::<(), Box>(()) + /// ``` + /// + /// If your alphabet is inconsistent then this will fail to compile in a `const` context: + /// + /// ```compile_fail + /// const _: &'static bs58::Alphabet = &bs58::Alphabet::new_unwrap( + /// b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + /// ); + /// ``` + pub const fn new_unwrap(base: &[u8; 58]) -> Self { + let result = Self::new(base); + #[allow(unconditional_panic)] // https://github.com/rust-lang/rust/issues/78803 + [][match result { + Ok(alphabet) => return alphabet, + Err(_) => 0, + }] + } +} + +impl fmt::Debug for Alphabet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Ok(s) = core::str::from_utf8(&self.encode) { + f.debug_tuple("Alphabet").field(&s).finish() + } else { + unreachable!() + } + } +} + +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::DuplicateCharacter { + character, + first, + second, + } => write!( + f, + "alphabet contained a duplicate character `{}` at indexes {} and {}", + character, first, second, + ), + Error::NonAsciiCharacter { index } => { + write!(f, "alphabet contained a non-ascii character at {}", index) + } + } } } @@ -71,3 +201,9 @@ const _: () = { let _ = Alphabet::FLICKR; let _ = Alphabet::DEFAULT; }; + +#[test] +#[should_panic] +fn test_new_unwrap_does_panic() { + Alphabet::new_unwrap(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); +} diff --git a/src/lib.rs b/src/lib.rs index e7564d1..fee4c3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,7 +78,8 @@ extern crate std; #[cfg(feature = "alloc")] extern crate alloc; -mod alphabet; +pub mod alphabet; +#[doc(inline)] pub use alphabet::Alphabet; pub mod decode;