const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn symbol(value: u8) -> char {
let index = usize::from(value & 0x3f);
ALPHABET.get(index).map_or('=', |byte| char::from(*byte))
}
const fn value_of(symbol: u8) -> Option<u8> {
Some(match symbol {
b'A'..=b'Z' => symbol.wrapping_sub(b'A'),
b'a'..=b'z' => symbol.wrapping_sub(b'a').wrapping_add(26),
b'0'..=b'9' => symbol.wrapping_sub(b'0').wrapping_add(52),
b'+' => 62,
b'/' => 63,
_ => return None,
})
}
fn encode_group(b0: u8, b1: Option<u8>, b2: Option<u8>) -> [char; 4] {
let (c1, c2) = (b1.unwrap_or(0), b2.unwrap_or(0));
[
symbol(b0.wrapping_shr(2)),
symbol((b0 & 0x03).wrapping_shl(4) | c1.wrapping_shr(4)),
b1.map_or('=', |_| {
symbol((c1 & 0x0f).wrapping_shl(2) | c2.wrapping_shr(6))
}),
b2.map_or('=', |_| symbol(c2 & 0x3f)),
]
}
fn decode_group(group: [u8; 4], last: bool) -> Option<[Option<u8>; 3]> {
let [s0, s1, s2, s3] = group;
let (v0, v1) = (value_of(s0)?, value_of(s1)?);
match (s2, s3) {
(b'=', b'=') if last => {
(v1 & 0x0f == 0).then_some([Some(v0.wrapping_shl(2) | v1.wrapping_shr(4)), None, None])
}
(_, b'=') if last => {
let v2 = value_of(s2)?;
(v2 & 0x03 == 0).then_some([
Some(v0.wrapping_shl(2) | v1.wrapping_shr(4)),
Some(v1.wrapping_shl(4) | v2.wrapping_shr(2)),
None,
])
}
_ => {
let (v2, v3) = (value_of(s2)?, value_of(s3)?);
Some([
Some(v0.wrapping_shl(2) | v1.wrapping_shr(4)),
Some(v1.wrapping_shl(4) | v2.wrapping_shr(2)),
Some(v2.wrapping_shl(6) | v3),
])
}
}
}
pub(crate) fn encode(input: &[u8]) -> String {
let mut out = String::with_capacity(input.len().div_ceil(3).saturating_mul(4));
for chunk in input.chunks(3) {
if let Some((&b0, rest)) = chunk.split_first() {
out.extend(encode_group(
b0,
rest.first().copied(),
rest.get(1).copied(),
));
}
}
out
}
pub(crate) fn decode(text: &str) -> Option<Vec<u8>> {
let (groups, remainder) = text.as_bytes().as_chunks::<4>();
if !remainder.is_empty() {
return None;
}
let mut out = Vec::with_capacity(groups.len().saturating_mul(3));
let count = groups.len();
for (at, group) in groups.iter().enumerate() {
let last = at.saturating_add(1) == count;
out.extend(decode_group(*group, last)?.into_iter().flatten());
}
Some(out)
}
pub(crate) fn decode_hash(text: &str) -> Option<[u8; 32]> {
decode(text)?.try_into().ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn group_text(group: [char; 4]) -> [u8; 4] {
group.map(|c| c as u8)
}
#[test]
fn every_three_byte_group_round_trips() {
for value in 0u32..1 << 24 {
let [_, b0, b1, b2] = value.to_be_bytes();
let text = group_text(encode_group(b0, Some(b1), Some(b2)));
assert_eq!(
decode_group(text, true),
Some([Some(b0), Some(b1), Some(b2)]),
"{value:06x}"
);
assert_eq!(decode_group(text, false), decode_group(text, true));
}
}
#[test]
fn every_final_group_is_canonical_or_refused() {
for a in ALPHABET {
for b in ALPHABET {
let one = [*a, *b, b'=', b'='];
match decode_group(one, true) {
Some([Some(byte), None, None]) => {
assert_eq!(group_text(encode_group(byte, None, None)), one);
}
None => assert!(value_of(*b).unwrap() & 0x0f != 0),
other => panic!("{other:?}"),
}
assert_eq!(decode_group(one, false), None, "padding before the end");
for c in ALPHABET {
let two = [*a, *b, *c, b'='];
match decode_group(two, true) {
Some([Some(x), Some(y), None]) => {
assert_eq!(group_text(encode_group(x, Some(y), None)), two);
}
None => assert!(value_of(*c).unwrap() & 0x03 != 0),
other => panic!("{other:?}"),
}
}
}
}
}
#[test]
fn matches_the_rfc_4648_vectors() {
for (plain, encoded) in [
("", ""),
("f", "Zg=="),
("fo", "Zm8="),
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy"),
] {
assert_eq!(encode(plain.as_bytes()), encoded);
assert_eq!(decode(encoded).as_deref(), Some(plain.as_bytes()));
}
}
#[test]
fn refuses_malformed_text() {
for bad in [
"Zg=", "Zg", "Z===", "Zm9v!", "Zg==Zg==", "=Zm9", "Zm=v", "Zh==", "Zm9=", "Zm9v\n",
"Zm 9", "====",
] {
assert_eq!(decode(bad), None, "{bad:?}");
}
assert_eq!(decode_hash("Zm9v"), None);
}
}