Skip to main content

fleischwolf_core/
base64.rs

1//! Minimal standard-alphabet Base64 codec (RFC 4648): `encode` for embedding
2//! image bytes as `data:` URIs, `decode` for reading them back out — avoids a
3//! dependency for the two things we need.
4
5const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6
7/// Base64-encode `data` with `=` padding.
8pub fn encode(data: &[u8]) -> String {
9    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
10    for chunk in data.chunks(3) {
11        let b0 = chunk[0] as u32;
12        let b1 = *chunk.get(1).unwrap_or(&0) as u32;
13        let b2 = *chunk.get(2).unwrap_or(&0) as u32;
14        let n = (b0 << 16) | (b1 << 8) | b2;
15        out.push(ALPHABET[(n >> 18 & 0x3f) as usize] as char);
16        out.push(ALPHABET[(n >> 12 & 0x3f) as usize] as char);
17        out.push(if chunk.len() > 1 {
18            ALPHABET[(n >> 6 & 0x3f) as usize] as char
19        } else {
20            '='
21        });
22        out.push(if chunk.len() > 2 {
23            ALPHABET[(n & 0x3f) as usize] as char
24        } else {
25            '='
26        });
27    }
28    out
29}
30
31/// Decode standard-alphabet Base64. Whitespace is ignored and `=` padding ends
32/// the stream; returns `None` on any other invalid character. Lenient about
33/// missing padding (handles the unpadded variant some `data:` URIs use).
34pub fn decode(s: &str) -> Option<Vec<u8>> {
35    let mut out = Vec::with_capacity(s.len() / 4 * 3);
36    let mut acc: u32 = 0;
37    let mut bits: u8 = 0;
38    for &c in s.as_bytes() {
39        let v = match c {
40            b'A'..=b'Z' => c - b'A',
41            b'a'..=b'z' => c - b'a' + 26,
42            b'0'..=b'9' => c - b'0' + 52,
43            b'+' => 62,
44            b'/' => 63,
45            b'=' => break,
46            b' ' | b'\t' | b'\r' | b'\n' => continue,
47            _ => return None,
48        };
49        acc = (acc << 6) | v as u32;
50        bits += 6;
51        if bits >= 8 {
52            bits -= 8;
53            out.push((acc >> bits) as u8);
54        }
55    }
56    Some(out)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::{decode, encode};
62
63    #[test]
64    fn rfc4648_vectors() {
65        assert_eq!(encode(b""), "");
66        assert_eq!(encode(b"f"), "Zg==");
67        assert_eq!(encode(b"fo"), "Zm8=");
68        assert_eq!(encode(b"foo"), "Zm9v");
69        assert_eq!(encode(b"foob"), "Zm9vYg==");
70        assert_eq!(encode(b"fooba"), "Zm9vYmE=");
71        assert_eq!(encode(b"foobar"), "Zm9vYmFy");
72    }
73
74    #[test]
75    fn decode_roundtrips() {
76        for s in ["", "f", "fo", "foo", "foob", "fooba", "foobar"] {
77            assert_eq!(decode(&encode(s.as_bytes())).as_deref(), Some(s.as_bytes()));
78        }
79        // padded, unpadded, and whitespace-laden inputs all decode
80        assert_eq!(decode("Zm9vYmFy").as_deref(), Some(&b"foobar"[..]));
81        assert_eq!(decode("Zm9v\nYmE=").as_deref(), Some(&b"fooba"[..]));
82        assert_eq!(decode("Zg").as_deref(), Some(&b"f"[..]));
83        assert!(decode("not base64!").is_none());
84    }
85}