const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
#[allow(clippy::indexing_slicing)]
const VALUE: [u8; 256] = {
let mut t = [0xffu8; 256];
let mut i = 0usize;
while i < 64 {
t[ALPHABET[i] as usize] = i as u8;
i += 1;
}
t
};
pub(super) const fn encoded_len(n: usize) -> usize {
n.div_ceil(3) * 4
}
pub(super) fn encode(input: &[u8], out: &mut [u8]) -> usize {
let sym = |v: u32| ALPHABET.get((v & 63) as usize).copied().unwrap_or(b'A');
let mut n = 0usize;
for chunk in input.chunks(3) {
let a = u32::from(chunk.first().copied().unwrap_or(0));
let b = u32::from(chunk.get(1).copied().unwrap_or(0));
let c = u32::from(chunk.get(2).copied().unwrap_or(0));
let v = (a << 16) | (b << 8) | c;
let quad = [
sym(v >> 18),
sym(v >> 12),
if chunk.len() > 1 { sym(v >> 6) } else { b'=' },
if chunk.len() > 2 { sym(v) } else { b'=' },
];
if let Some(d) = out.get_mut(n..n + 4) {
d.copy_from_slice(&quad);
}
n += 4;
}
n
}
pub(super) const MAX_DECODED: usize = 48;
pub(super) fn decode(
text: &[u8],
out: &mut [u8; MAX_DECODED],
) -> core::result::Result<usize, usize> {
if text.len() % 4 != 0 {
return Err(text.len() - text.len() % 4);
}
if text.len() / 4 * 3 > MAX_DECODED {
return Err(MAX_DECODED / 3 * 4);
}
let six = |b: u8| VALUE.get(usize::from(b)).copied().unwrap_or(0xff);
let bad = |at: usize, quad: &[u8]| at + quad.iter().position(|&b| six(b) == 0xff).unwrap_or(0);
let (body, last) = text.split_at(text.len().saturating_sub(4));
let mut n = 0usize;
for (q, quad) in body.chunks_exact(4).enumerate() {
let &[a, b, c, d] = quad else { continue };
let (a, b, c, d) = (six(a), six(b), six(c), six(d));
if (a | b | c | d) & 0x80 != 0 {
return Err(bad(q * 4, quad));
}
let v = (u32::from(a) << 18) | (u32::from(b) << 12) | (u32::from(c) << 6) | u32::from(d);
if let Some(dst) = out.get_mut(n..n + 3) {
dst.copy_from_slice(&[(v >> 16) as u8, (v >> 8) as u8, v as u8]);
}
n += 3;
}
let &[a, b, c, d] = last else {
return Ok(n);
};
let at = body.len();
let pad = match (c, d) {
(b'=', b'=') => 2,
(_, b'=') => 1,
_ => 0,
};
let kept = last.get(..4 - pad).unwrap_or(&[]);
if kept.iter().any(|&x| six(x) == 0xff) {
return Err(bad(at, kept));
}
let (a, b) = (six(a), six(b));
let (c, d) = (
if pad == 2 { 0 } else { six(c) },
if pad >= 1 { 0 } else { six(d) },
);
if (pad == 2 && b & 0x0f != 0) || (pad == 1 && c & 0x03 != 0) {
return Err(at + 3 - pad);
}
let v = (u32::from(a) << 18) | (u32::from(b) << 12) | (u32::from(c) << 6) | u32::from(d);
let bytes = [(v >> 16) as u8, (v >> 8) as u8, v as u8];
let keep = 3 - pad;
if let (Some(dst), Some(src)) = (out.get_mut(n..n + keep), bytes.get(..keep)) {
dst.copy_from_slice(src);
}
n += keep;
Ok(n)
}
#[cfg(test)]
mod tests {
use super::*;
fn dec(s: &[u8]) -> core::result::Result<std::vec::Vec<u8>, usize> {
let mut out = [0u8; MAX_DECODED];
decode(s, &mut out).map(|n| out[..n].to_vec())
}
fn enc(s: &[u8]) -> std::vec::Vec<u8> {
let mut out = [0u8; 128];
let n = encode(s, &mut out);
assert_eq!(n, encoded_len(s.len()));
out[..n].to_vec()
}
#[test]
fn test_rfc4648_vectors_both_ways() {
for (plain, coded) in [
(&b""[..], &b""[..]),
(b"f", b"Zg=="),
(b"fo", b"Zm8="),
(b"foo", b"Zm9v"),
(b"foob", b"Zm9vYg=="),
(b"fooba", b"Zm9vYmE="),
(b"foobar", b"Zm9vYmFy"),
] {
assert_eq!(enc(plain), coded);
assert_eq!(dec(coded).unwrap(), plain);
}
assert_eq!(enc(&[0xff, 0xfe]), b"//4=");
assert_eq!(dec(b"+/+/").unwrap(), [0xfb, 0xff, 0xbf]);
}
#[test]
fn test_decode_is_strict() {
assert_eq!(dec(b"Zg="), Err(0));
assert_eq!(dec(b"Zm9vY"), Err(4));
assert_eq!(dec(b"Zm 9"), Err(2));
assert_eq!(dec(b"Zm-9"), Err(2));
assert_eq!(dec(b"Zm_9"), Err(2));
assert_eq!(dec(b"Zm9\n"), Err(3));
assert_eq!(dec(b"Zg==Zm9v"), Err(2));
assert_eq!(dec(b"Z===").unwrap_err(), 1);
assert_eq!(dec(b"=g=="), Err(0));
assert_eq!(dec(b"Zm=v"), Err(2));
assert_eq!(dec(b"Zh=="), Err(1));
assert_eq!(dec(b"Zm9="), Err(2));
assert_eq!(dec(b"Zm8=").unwrap(), b"fo");
assert_eq!(dec(&[b'A'; 64]).unwrap().len(), 48);
assert_eq!(dec(&[b'A'; 68]), Err(64));
}
proptest::proptest! {
#[test]
fn test_round_trip(v in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..=48)) {
proptest::prop_assert_eq!(dec(&enc(&v)).unwrap(), v);
}
#[test]
fn test_decode_accepts_only_canonical_text(s in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..=64)) {
if let Ok(bytes) = dec(&s) {
proptest::prop_assert_eq!(enc(&bytes), s);
}
}
}
}