const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
pub(crate) fn encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let mut triple = u32::from(chunk[0]) << 16;
triple |= u32::from(chunk.get(1).copied().unwrap_or(0)) << 8;
triple |= u32::from(chunk.get(2).copied().unwrap_or(0));
let digit = |shift: u32| char::from(ALPHABET[((triple >> shift) & 0b0011_1111) as usize]);
out.push(digit(18));
out.push(digit(12));
if chunk.len() > 1 {
out.push(digit(6));
}
if chunk.len() > 2 {
out.push(digit(0));
}
}
out
}
pub(crate) fn decode(text: &str) -> Option<Vec<u8>> {
let input = text.as_bytes();
if input.len() % 4 == 1 {
return None;
}
let mut out = Vec::with_capacity(input.len() / 4 * 3);
for group in input.chunks(4) {
let mut packed = 0u32;
for (index, byte) in group.iter().enumerate() {
packed |= u32::from(value_of(*byte)?) << (18 - 6 * index);
}
let bytes = group.len() - 1;
let unused_bits = 24 - 8 * bytes;
if packed & ((1u32 << unused_bits) - 1) != 0 {
return None;
}
for index in 0..bytes {
out.push(u8::try_from((packed >> (16 - 8 * index)) & 0xff).ok()?);
}
}
Some(out)
}
fn value_of(byte: u8) -> Option<u8> {
match byte {
b'A'..=b'Z' => Some(byte - b'A'),
b'a'..=b'z' => Some(byte - b'a' + 26),
b'0'..=b'9' => Some(byte - b'0' + 52),
b'-' => Some(62),
b'_' => Some(63),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{decode, encode};
#[test]
fn the_rfc_4648_vectors_round_trip_without_padding() {
for (plain, encoded) in [
("", ""),
("f", "Zg"),
("fo", "Zm8"),
("foo", "Zm9v"),
("foob", "Zm9vYg"),
("fooba", "Zm9vYmE"),
("foobar", "Zm9vYmFy"),
] {
assert_eq!(encode(plain.as_bytes()), encoded, "encoding {plain:?}");
assert_eq!(
decode(encoded).as_deref(),
Some(plain.as_bytes()),
"decoding {encoded:?}"
);
}
}
#[test]
fn the_url_safe_alphabet_is_used_for_sixty_two_and_sixty_three() {
assert_eq!(encode(&[0xfb, 0xef, 0xff]), "--__");
assert_eq!(decode("--__"), Some(vec![0xfb, 0xef, 0xff]));
}
#[test]
fn every_byte_value_round_trips() {
let all: Vec<u8> = (0..=255).collect();
assert_eq!(decode(&encode(&all)), Some(all));
}
#[test]
fn padding_is_rejected() {
assert_eq!(decode("Zg=="), None);
assert_eq!(decode("Zm8="), None);
}
#[test]
fn a_character_outside_the_alphabet_is_rejected() {
assert_eq!(decode("Zm9v!"), None);
assert_eq!(decode("Zm9+"), None);
assert_eq!(decode("Zm9/"), None);
assert_eq!(decode("Zm9 "), None);
}
#[test]
fn an_impossible_length_is_rejected() {
assert_eq!(decode("Z"), None);
assert_eq!(decode("Zm9vY"), None);
}
#[test]
fn non_canonical_trailing_bits_are_rejected() {
assert_eq!(decode("Zg"), Some(vec![0x66]));
assert_eq!(decode("Zh"), None);
assert_eq!(decode("Zm8"), Some(vec![0x66, 0x6f]));
assert_eq!(decode("Zm9"), None);
}
}