const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const BITS_PER_CHAR: u32 = 5;
const BITS_PER_BYTE: u32 = 8;
pub(crate) fn encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(5) * 8);
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for &byte in data {
acc = (acc << BITS_PER_BYTE) | u32::from(byte);
bits += BITS_PER_BYTE;
while bits >= BITS_PER_CHAR {
bits -= BITS_PER_CHAR;
let index = (acc >> bits) & 0x1F;
out.push(ALPHABET[index as usize] as char);
}
}
if bits > 0 {
let index = (acc << (BITS_PER_CHAR - bits)) & 0x1F;
out.push(ALPHABET[index as usize] as char);
}
out
}
pub(crate) fn decode(s: &str) -> Option<Vec<u8>> {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len() * 5 / 8);
let mut acc: u32 = 0;
let mut bits: u32 = 0;
for &c in bytes {
let value = decode_char(c)?;
acc = (acc << BITS_PER_CHAR) | u32::from(value);
bits += BITS_PER_CHAR;
if bits >= BITS_PER_BYTE {
bits -= BITS_PER_BYTE;
out.push(((acc >> bits) & 0xFF) as u8);
}
}
if bits >= BITS_PER_CHAR {
return None;
}
if bits > 0 && (acc & ((1 << bits) - 1)) != 0 {
return None;
}
Some(out)
}
fn decode_char(c: u8) -> Option<u8> {
ALPHABET
.iter()
.position(|&a| a == c)
.and_then(|i| u8::try_from(i).ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_rfc_4648_vectors_round_trip() {
for (plain, encoded) in [
("", ""),
("f", "MY"),
("fo", "MZXQ"),
("foo", "MZXW6"),
("foob", "MZXW6YQ"),
("fooba", "MZXW6YTB"),
("foobar", "MZXW6YTBOI"),
] {
assert_eq!(encode(plain.as_bytes()), encoded, "encoding {plain:?}");
assert_eq!(
decode(encoded).as_deref(),
Some(plain.as_bytes()),
"decoding {encoded:?}"
);
}
}
#[test]
fn every_byte_value_survives_a_round_trip() {
let all: Vec<u8> = (0..=255).collect();
assert_eq!(decode(&encode(&all)), Some(all));
}
#[test]
fn an_impossible_character_count_is_rejected() {
for s in ["A", "AAA", "AAAAAA", "MZXW6YTBOIA"] {
assert_eq!(decode(s), None, "{s} encodes no whole number of bytes");
}
}
#[test]
fn non_zero_bits_in_the_final_group_are_rejected() {
assert_eq!(decode("MY").as_deref(), Some(&b"f"[..]));
for s in ["MZ", "M6", "MZXW6YTBOJ"] {
assert_eq!(decode(s), None, "{s} has rubbish in its final group");
}
}
#[test]
fn characters_outside_the_alphabet_are_rejected() {
for s in [
"MZXW6YT0", "MZXW6YT1", "MZXW6YT8", "MZXW6YT9", "mzxw6ytb", "MZXW-YTB",
] {
assert_eq!(decode(s), None, "{s} is not in the alphabet");
}
}
#[test]
fn padding_is_rejected_rather_than_tolerated() {
assert!(!encode(b"f").contains('='));
assert_eq!(decode("MY======"), None);
}
}