const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const fn sextet(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,
}
}
pub(crate) fn decode(input: &[u8]) -> Option<Vec<u8>> {
let pad = input.iter().rev().take_while(|&&b| b == b'=').count();
if pad > 2 {
return None;
}
if pad != 0 && !input.len().is_multiple_of(4) {
return None;
}
let data = &input[..input.len() - pad];
let mut out = Vec::with_capacity(data.len() / 4 * 3 + 2);
let mut quanta = data.chunks_exact(4);
for q in &mut quanta {
let n = (u32::from(sextet(q[0])?) << 18)
| (u32::from(sextet(q[1])?) << 12)
| (u32::from(sextet(q[2])?) << 6)
| u32::from(sextet(q[3])?);
out.extend_from_slice(&[(n >> 16) as u8, (n >> 8) as u8, n as u8]);
}
match quanta.remainder() {
[] => {}
[_] => return None,
[a, b] => {
let (a, b) = (sextet(*a)?, sextet(*b)?);
if b & 0b1111 != 0 {
return None; }
out.push((a << 2) | (b >> 4));
}
[a, b, c] => {
let (a, b, c) = (sextet(*a)?, sextet(*b)?, sextet(*c)?);
if c & 0b11 != 0 {
return None;
}
out.push((a << 2) | (b >> 4));
out.push(((b & 0b1111) << 4) | (c >> 2));
}
_ => unreachable!("chunks_exact(4) leaves at most three characters"),
}
Some(out)
}
pub(crate) fn encode(input: &[u8]) -> String {
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
let mut triples = input.chunks_exact(3);
let glyph = |n: u32, shift: u32| ALPHABET[((n >> shift) & 0b11_1111) as usize] as char;
for t in &mut triples {
let n = (u32::from(t[0]) << 16) | (u32::from(t[1]) << 8) | u32::from(t[2]);
out.extend([glyph(n, 18), glyph(n, 12), glyph(n, 6), glyph(n, 0)]);
}
match triples.remainder() {
[] => {}
[a] => {
let n = u32::from(*a) << 16;
out.extend([glyph(n, 18), glyph(n, 12), '=', '=']);
}
[a, b] => {
let n = (u32::from(*a) << 16) | (u32::from(*b) << 8);
out.extend([glyph(n, 18), glyph(n, 12), glyph(n, 6), '=']);
}
_ => unreachable!("chunks_exact(3) leaves at most two bytes"),
}
out
}
#[cfg(test)]
mod tests {
use super::{decode, encode};
const RFC4648: [(&str, &str); 7] = [
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
];
#[test]
fn encodes_the_rfc4648_vectors() {
for (plain, encoded) in RFC4648 {
assert_eq!(encode(plain.as_bytes()), encoded, "encode({plain:?})");
}
}
#[test]
fn decodes_the_rfc4648_vectors() {
for (plain, encoded) in RFC4648 {
assert_eq!(
decode(encoded.as_bytes()).as_deref(),
Some(plain.as_bytes()),
"decode({encoded:?})"
);
}
}
#[test]
fn round_trips_every_byte_value() {
let all: Vec<u8> = (0..=255u8).collect();
assert_eq!(decode(encode(&all).as_bytes()).as_deref(), Some(&all[..]));
}
#[test]
fn refuses_a_byte_outside_the_alphabet() {
for bad in ["Zm9v!!!!", "Zm-v", "Zm_v", "Zm9v Zm9v", "Zm9\u{f6}"] {
assert_eq!(decode(bad.as_bytes()), None, "decode({bad:?})");
}
}
#[test]
fn refuses_a_length_that_cannot_be_an_encoding() {
for bad in ["Z", "Zm9vY", "Zm9vZm9vY"] {
assert_eq!(decode(bad.as_bytes()), None, "decode({bad:?})");
}
}
#[test]
fn refuses_padding_that_is_not_at_the_end() {
for bad in ["Zg==Zg==", "Z=g=", "=Zm9v", "Zm9v="] {
assert_eq!(decode(bad.as_bytes()), None, "decode({bad:?})");
}
}
#[test]
fn refuses_non_zero_bits_in_a_final_partial_group() {
for bad in ["Zh==", "Zm9=", "Zm9vYmF=", "ZC==", "ZmC="] {
assert_eq!(decode(bad.as_bytes()), None, "decode({bad:?})");
}
for good in ["Zg==", "ZA==", "Zm8=", "ZmA="] {
assert!(decode(good.as_bytes()).is_some(), "decode({good:?})");
}
}
#[test]
fn accepts_all_of_its_padding_or_none_of_it_but_never_part() {
for unpadded in [&b"Zg"[..], b"Zm8", b"Zm9vYmE"] {
assert!(decode(unpadded).is_some(), "unpadded {unpadded:?}");
}
for canonical in [&b"Zg=="[..], b"Zm8=", b"Zm9vYmE="] {
assert!(decode(canonical).is_some(), "canonical {canonical:?}");
}
for partial in [&b"Zg="[..], b"ZmE=x", b"Zm9vYmE=="] {
assert_eq!(decode(partial), None, "partial padding {partial:?}");
}
}
#[test]
fn decodes_the_payload_tmux_was_measured_emitting() {
assert_eq!(
decode(b"SEVMTE9KVVNURVJN").as_deref(),
Some(&b"HELLOJUSTERM"[..])
);
assert_eq!(
decode(b"SlVTVEVSTVBST0JF").as_deref(),
Some(&b"JUSTERMPROBE"[..])
);
}
}