const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
pub fn encode(input: &[u8]) -> String {
let mut out = String::with_capacity((input.len() + 4) / 5 * 8);
for chunk in input.chunks(5) {
let mut buf: u64 = 0;
for &b in chunk {
buf = (buf << 8) | b as u64;
}
let pad_bytes = 5 - chunk.len();
buf <<= pad_bytes * 8;
let output_chars = match chunk.len() {
1 => 2,
2 => 4,
3 => 5,
4 => 7,
5 => 8,
_ => unreachable!(),
};
for i in 0..8 {
if i < output_chars {
let shift = 35 - i * 5; let idx = ((buf >> shift) & 0x1F) as usize;
out.push(ALPHABET[idx] as char);
} else {
out.push('=');
}
}
}
out
}
pub fn decode(input: &str) -> Result<Vec<u8>, String> {
let cleaned: Vec<u8> = input
.chars()
.filter(|c| !c.is_whitespace() && *c != '=')
.map(|c| c.to_ascii_uppercase() as u8)
.collect();
let mut out = Vec::with_capacity(cleaned.len() * 5 / 8);
for chunk in cleaned.chunks(8) {
let mut buf: u64 = 0;
for &c in chunk {
let val = ALPHABET
.iter()
.position(|&a| a == c)
.ok_or_else(|| format!("invalid base32 char: {}", c as char))?;
buf = (buf << 5) | val as u64;
}
let pad_chars = 8 - chunk.len();
buf <<= pad_chars * 5;
let output_bytes = match chunk.len() {
2 => 1,
4 => 2,
5 => 3,
7 => 4,
8 => 5,
n => return Err(format!("invalid base32 group length: {}", n)),
};
for i in 0..output_bytes {
let shift = 32 - i * 8; out.push(((buf >> shift) & 0xFF) as u8);
}
}
Ok(out)
}
pub fn encode_grouped(input: &[u8]) -> String {
let raw = encode(input);
let raw = raw.trim_end_matches('=');
let mut out = String::with_capacity(raw.len() + raw.len() / 4);
for (i, c) in raw.chars().enumerate() {
if i > 0 && i % 4 == 0 {
out.push(' ');
}
out.push(c);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc4648_examples() {
let cases = [
("", ""),
("f", "MY======"),
("fo", "MZXQ===="),
("foo", "MZXW6==="),
("foob", "MZXW6YQ="),
("fooba", "MZXW6YTB"),
("foobar", "MZXW6YTBOI======"),
];
for (raw, b32) in cases {
assert_eq!(encode(raw.as_bytes()), b32, "encode {:?}", raw);
assert_eq!(decode(b32).unwrap(), raw.as_bytes(), "decode {:?}", b32);
}
}
#[test]
fn decode_is_lenient() {
assert_eq!(decode("mzxw 6ytb").unwrap(), b"fooba");
assert_eq!(decode("MZXW6YQ").unwrap(), b"foob"); }
#[test]
fn round_trip_random_lengths() {
for len in 0..20 {
let bytes: Vec<u8> = (0..len as u8).collect();
let s = encode(&bytes);
let back = decode(&s).unwrap();
assert_eq!(back, bytes, "round-trip failed at len={}", len);
}
}
}