use std::fmt;
const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Base58Error {
Empty,
InvalidByte(u8),
TooLong,
Overflow,
}
impl fmt::Display for Base58Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("empty base58 string"),
Self::InvalidByte(byte) => write!(f, "invalid base58 byte 0x{byte:02x}"),
Self::TooLong => f.write_str("base58 string too long for 32 bytes"),
Self::Overflow => f.write_str("base58 value does not fit in 32 bytes"),
}
}
}
impl std::error::Error for Base58Error {}
#[inline]
pub fn decode_32(value: &str) -> Result<[u8; 32], Base58Error> {
if value.is_empty() {
return Err(Base58Error::Empty);
}
if value.len() > 44 {
return Err(Base58Error::TooLong);
}
let mut output = [0u8; 32];
for byte in value.bytes() {
let digit = decode_byte(byte).ok_or(Base58Error::InvalidByte(byte))? as u32;
let mut carry = digit;
for out in output.iter_mut().rev() {
let value = (*out as u32) * 58 + carry;
*out = value as u8;
carry = value >> 8;
}
if carry != 0 {
return Err(Base58Error::Overflow);
}
}
Ok(output)
}
#[inline]
pub fn encode_fixed(bytes: &[u8; 32]) -> String {
if bytes.iter().all(|byte| *byte == 0) {
return "11111111111111111111111111111111".to_string();
}
let mut value = *bytes;
let mut encoded = [0u8; 44];
let mut out_index = encoded.len();
let mut first_non_zero = 0usize;
while first_non_zero < value.len() {
let mut remainder = 0u32;
for byte in value.iter_mut().skip(first_non_zero) {
let accumulator = (remainder << 8) | *byte as u32;
*byte = (accumulator / 58) as u8;
remainder = accumulator % 58;
}
out_index -= 1;
encoded[out_index] = ALPHABET[remainder as usize];
while first_non_zero < value.len() && value[first_non_zero] == 0 {
first_non_zero += 1;
}
}
for byte in bytes.iter().take_while(|byte| **byte == 0) {
let _ = byte;
out_index -= 1;
encoded[out_index] = b'1';
}
String::from_utf8(encoded[out_index..].to_vec()).expect("base58 alphabet is valid UTF-8")
}
#[inline]
fn decode_byte(byte: u8) -> Option<u8> {
match byte {
b'1'..=b'9' => Some(byte - b'1'),
b'A'..=b'H' => Some(byte - b'A' + 9),
b'J'..=b'N' => Some(byte - b'J' + 17),
b'P'..=b'Z' => Some(byte - b'P' + 22),
b'a'..=b'k' => Some(byte - b'a' + 33),
b'm'..=b'z' => Some(byte - b'm' + 44),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_zero_pubkey() {
let decoded = decode_32("11111111111111111111111111111111").unwrap();
assert_eq!(decoded, [0u8; 32]);
}
#[test]
fn round_trip_fixed_bytes() {
let bytes = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
];
let encoded = encode_fixed(&bytes);
let decoded = decode_32(&encoded).unwrap();
assert_eq!(decoded, bytes);
}
}