use core::fmt;
const BASE32_ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const BASE32_DECODE: [u8; 256] = build_decode_table();
#[allow(clippy::cast_possible_truncation)]
const fn build_decode_table() -> [u8; 256] {
let mut table = [255u8; 256];
let mut i = 0u8;
while i < 32 {
let ch = BASE32_ALPHABET[i as usize];
table[ch as usize] = i;
if ch >= b'A' && ch <= b'Z' {
table[(ch + 32) as usize] = i;
}
i += 1;
}
table
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Base32DecodeErrorKind {
InvalidCharacter,
InvalidLength,
NonCanonical,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Base32DecodeError {
kind: Base32DecodeErrorKind,
}
impl Base32DecodeError {
const fn new(kind: Base32DecodeErrorKind) -> Self {
Self { kind }
}
}
impl fmt::Display for Base32DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
Base32DecodeErrorKind::InvalidCharacter => {
write!(f, "base32: invalid character in input")
}
Base32DecodeErrorKind::InvalidLength => {
write!(f, "base32: invalid input length")
}
Base32DecodeErrorKind::NonCanonical => {
write!(f, "base32: non-canonical encoding (non-zero trailing bits)")
}
}
}
}
impl std::error::Error for Base32DecodeError {}
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn base32_encode(input: &[u8]) -> String {
if input.is_empty() {
return String::new();
}
let full_chunks = input.len() / 5;
let remainder = input.len() % 5;
let capacity = (full_chunks + usize::from(remainder > 0)) * 8;
let mut out = Vec::with_capacity(capacity);
let chunks = input.chunks_exact(5);
let tail = chunks.remainder();
for chunk in chunks {
let n: u64 = (u64::from(chunk[0]) << 32)
| (u64::from(chunk[1]) << 24)
| (u64::from(chunk[2]) << 16)
| (u64::from(chunk[3]) << 8)
| u64::from(chunk[4]);
out.push(BASE32_ALPHABET[((n >> 35) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[((n >> 30) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[((n >> 25) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[((n >> 20) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[((n >> 15) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[((n >> 10) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[((n >> 5) & 0x1F) as usize]);
out.push(BASE32_ALPHABET[(n & 0x1F) as usize]);
}
if !tail.is_empty() {
let mut buf = [0u8; 5];
buf[..tail.len()].copy_from_slice(tail);
let n: u64 = (u64::from(buf[0]) << 32)
| (u64::from(buf[1]) << 24)
| (u64::from(buf[2]) << 16)
| (u64::from(buf[3]) << 8)
| u64::from(buf[4]);
let (chars, pad) = match tail.len() {
1 => (2, 6),
2 => (4, 4),
3 => (5, 3),
4 => (7, 1),
_ => unreachable!(),
};
let shifts = [35, 30, 25, 20, 15, 10, 5, 0];
for &shift in &shifts[..chars] {
out.push(BASE32_ALPHABET[((n >> shift) & 0x1F) as usize]);
}
out.extend(std::iter::repeat_n(b'=', pad));
}
#[allow(clippy::expect_used)]
String::from_utf8(out).expect("base32 output is always valid ASCII")
}
#[allow(clippy::cast_possible_truncation)]
pub fn base32_decode(input: &str) -> Result<Vec<u8>, Base32DecodeError> {
if input.is_empty() {
return Ok(Vec::new());
}
let input = input.as_bytes();
let pad_count = input.iter().rev().take_while(|&&b| b == b'=').count();
let data = &input[..input.len() - pad_count];
if data.is_empty() {
return Err(Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength));
}
let rem = data.len() % 8;
if rem == 1 || rem == 3 || rem == 6 {
return Err(Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength));
}
if pad_count > 0 {
let expected_pad = (8 - rem) % 8;
if pad_count != expected_pad {
return Err(Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength));
}
}
let full_groups = data.len() / 8;
let out_len = full_groups * 5
+ match rem {
2 => 1,
4 => 2,
5 => 3,
7 => 4,
_ => 0,
};
let mut out = Vec::with_capacity(out_len);
let chunks = data.chunks_exact(8);
let tail = chunks.remainder();
for chunk in chunks {
let mut n: u64 = 0;
for &byte in chunk {
let val = decode_char(byte)?;
n = (n << 5) | u64::from(val);
}
out.push((n >> 32) as u8);
out.push((n >> 24) as u8);
out.push((n >> 16) as u8);
out.push((n >> 8) as u8);
out.push(n as u8);
}
if !tail.is_empty() {
let mut n: u64 = 0;
for &byte in tail {
let val = decode_char(byte)?;
n = (n << 5) | u64::from(val);
}
let shift = (8 - tail.len()) * 5;
n <<= shift;
let bytes_out = match tail.len() {
2 => 1,
4 => 2,
5 => 3,
7 => 4,
_ => return Err(Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength)),
};
let residual_mask = (1u64 << (40 - bytes_out * 8)) - 1;
if n & residual_mask != 0 {
return Err(Base32DecodeError::new(Base32DecodeErrorKind::NonCanonical));
}
let all_bytes = [
(n >> 32) as u8,
(n >> 24) as u8,
(n >> 16) as u8,
(n >> 8) as u8,
n as u8,
];
out.extend_from_slice(&all_bytes[..bytes_out]);
}
Ok(out)
}
#[inline]
fn decode_char(byte: u8) -> Result<u8, Base32DecodeError> {
let val = BASE32_DECODE[byte as usize];
if val == 255 {
return Err(Base32DecodeError::new(
Base32DecodeErrorKind::InvalidCharacter,
));
}
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_empty() {
assert_eq!(base32_encode(b""), "");
}
#[test]
fn encode_f() {
assert_eq!(base32_encode(b"f"), "MY======");
}
#[test]
fn encode_fo() {
assert_eq!(base32_encode(b"fo"), "MZXQ====");
}
#[test]
fn encode_foo() {
assert_eq!(base32_encode(b"foo"), "MZXW6===");
}
#[test]
fn encode_foob() {
assert_eq!(base32_encode(b"foob"), "MZXW6YQ=");
}
#[test]
fn encode_fooba() {
assert_eq!(base32_encode(b"fooba"), "MZXW6YTB");
}
#[test]
fn encode_foobar() {
assert_eq!(base32_encode(b"foobar"), "MZXW6YTBOI======");
}
#[test]
fn decode_empty() {
assert_eq!(base32_decode("").unwrap(), b"");
}
#[test]
fn decode_f() {
assert_eq!(base32_decode("MY======").unwrap(), b"f");
}
#[test]
fn decode_fo() {
assert_eq!(base32_decode("MZXQ====").unwrap(), b"fo");
}
#[test]
fn decode_foo() {
assert_eq!(base32_decode("MZXW6===").unwrap(), b"foo");
}
#[test]
fn decode_foob() {
assert_eq!(base32_decode("MZXW6YQ=").unwrap(), b"foob");
}
#[test]
fn decode_fooba() {
assert_eq!(base32_decode("MZXW6YTB").unwrap(), b"fooba");
}
#[test]
fn decode_foobar() {
assert_eq!(base32_decode("MZXW6YTBOI======").unwrap(), b"foobar");
}
#[test]
fn round_trip_all_lengths() {
for len in 0..=32_u8 {
let data: Vec<u8> = (0..len).collect();
let encoded = base32_encode(&data);
let decoded = base32_decode(&encoded).unwrap();
assert_eq!(decoded, data, "round-trip failed for length {len}");
}
}
#[test]
fn round_trip_binary() {
let data: Vec<u8> = (0..=255).collect();
let encoded = base32_encode(&data);
let decoded = base32_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn decode_case_insensitive() {
assert_eq!(base32_decode("mzxw6===").unwrap(), b"foo");
assert_eq!(base32_decode("Mzxw6===").unwrap(), b"foo");
assert_eq!(base32_decode("mZXW6===").unwrap(), b"foo");
}
#[test]
fn decode_no_padding() {
assert_eq!(base32_decode("MY").unwrap(), b"f");
assert_eq!(base32_decode("MZXQ").unwrap(), b"fo");
assert_eq!(base32_decode("MZXW6").unwrap(), b"foo");
assert_eq!(base32_decode("MZXW6YQ").unwrap(), b"foob");
assert_eq!(base32_decode("MZXW6YTB").unwrap(), b"fooba");
}
#[test]
fn decode_rejects_invalid_character() {
let err = base32_decode("MZ!W6===").unwrap_err();
assert_eq!(
err,
Base32DecodeError::new(Base32DecodeErrorKind::InvalidCharacter)
);
}
#[test]
fn decode_rejects_digit_0() {
assert!(base32_decode("M0======").is_err());
assert!(base32_decode("M1======").is_err());
}
#[test]
fn decode_rejects_invalid_length() {
let err = base32_decode("A").unwrap_err();
assert_eq!(
err,
Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength)
);
}
#[test]
fn decode_rejects_wrong_padding_count() {
assert_eq!(base32_decode("MY======").unwrap(), b"f");
assert_eq!(base32_decode("MY").unwrap(), b"f");
for bad in ["MY=", "MY==", "MY===", "MY====", "MY=====", "MY======="] {
assert!(base32_decode(bad).is_err(), "expected reject: {bad}");
}
assert!(base32_decode("MZXW6YTB=").is_err());
}
#[test]
fn decode_rejects_all_padding() {
let err = base32_decode("========").unwrap_err();
assert_eq!(
err,
Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength)
);
}
#[test]
fn encoder_output_always_round_trips() {
for len in 0..=130usize {
let bytes: Vec<u8> = (0..len)
.map(|i| u8::try_from((i * 31 + 7) % 256).unwrap())
.collect();
let encoded = base32_encode(&bytes);
assert_eq!(base32_decode(&encoded).unwrap(), bytes, "len {len}");
}
}
#[test]
fn decode_rejects_non_canonical_trailing_bits() {
assert_eq!(base32_decode("MY").unwrap(), b"f");
let err = base32_decode("MZ").unwrap_err();
assert_eq!(
err,
Base32DecodeError::new(Base32DecodeErrorKind::NonCanonical)
);
}
#[test]
fn error_display_messages() {
let invalid_char = Base32DecodeError::new(Base32DecodeErrorKind::InvalidCharacter);
assert_eq!(
invalid_char.to_string(),
"base32: invalid character in input"
);
let invalid_len = Base32DecodeError::new(Base32DecodeErrorKind::InvalidLength);
assert_eq!(invalid_len.to_string(), "base32: invalid input length");
}
#[test]
fn error_implements_std_error() {
let err: Box<dyn std::error::Error> = Box::new(Base32DecodeError::new(
Base32DecodeErrorKind::InvalidCharacter,
));
let _ = err.to_string();
}
}