Skip to main content

rfc9162_tiles/
base64.rs

1//! RFC 4648 base64 with padding, as notes and checkpoints carry hashes and
2//! signatures. Decoding accepts the canonical encoding only: the unused bits
3//! of the last symbol are zero and the padding is exactly what the length
4//! calls for.
5
6const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
8/// The padded base64 text of `bytes`.
9#[must_use]
10pub fn encode(bytes: &[u8]) -> String {
11    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
12    for chunk in bytes.chunks(3) {
13        let mut block = [0u8; 3];
14        block[..chunk.len()].copy_from_slice(chunk);
15        let n = u32::from_be_bytes([0, block[0], block[1], block[2]]);
16        for i in 0..4 {
17            if i <= chunk.len() {
18                out.push(ALPHABET[((n >> (18 - 6 * i)) & 63) as usize] as char);
19            } else {
20                out.push('=');
21            }
22        }
23    }
24    out
25}
26
27fn value(symbol: u8) -> Option<u32> {
28    let v = match symbol {
29        b'A'..=b'Z' => symbol - b'A',
30        b'a'..=b'z' => symbol - b'a' + 26,
31        b'0'..=b'9' => symbol - b'0' + 52,
32        b'+' => 62,
33        b'/' => 63,
34        _ => return None,
35    };
36    Some(u32::from(v))
37}
38
39/// The bytes `text` encodes, or nothing when `text` is not a canonical padded
40/// encoding.
41#[must_use]
42pub fn decode(text: &str) -> Option<Vec<u8>> {
43    let bytes = text.as_bytes();
44    if bytes.len() % 4 != 0 {
45        return None;
46    }
47    let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
48    for (i, chunk) in bytes.chunks_exact(4).enumerate() {
49        let last = i == bytes.len() / 4 - 1;
50        let padding = chunk.iter().rev().take_while(|b| **b == b'=').count();
51        if padding > 2 || (padding > 0 && !last) {
52            return None;
53        }
54        let mut n = 0u32;
55        for symbol in &chunk[..4 - padding] {
56            n = (n << 6) | value(*symbol)?;
57        }
58        n <<= 6 * padding as u32;
59        let block = n.to_be_bytes();
60        let keep = 3 - padding;
61        // Bits the kept bytes do not cover have to be zero, or two texts
62        // would decode to the same bytes.
63        if n & ((1u32 << (6 * padding as u32)) - 1) != 0 {
64            return None;
65        }
66        if padding > 0 && block[1 + keep..].iter().any(|b| *b != 0) {
67            return None;
68        }
69        out.extend_from_slice(&block[1..1 + keep]);
70    }
71    Some(out)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn the_rfc_4648_vectors() {
80        let vectors = [
81            ("", ""),
82            ("f", "Zg=="),
83            ("fo", "Zm8="),
84            ("foo", "Zm9v"),
85            ("foob", "Zm9vYg=="),
86            ("fooba", "Zm9vYmE="),
87            ("foobar", "Zm9vYmFy"),
88        ];
89        for (plain, encoded) in vectors {
90            assert_eq!(encode(plain.as_bytes()), encoded);
91            assert_eq!(decode(encoded).unwrap(), plain.as_bytes());
92        }
93    }
94
95    #[test]
96    fn only_the_canonical_text_decodes() {
97        // "Zh==" has non-zero bits below the one byte it carries.
98        assert_eq!(decode("Zh=="), None);
99        assert_eq!(decode("Zm9="), None);
100        assert_eq!(decode("Zg="), None);
101        assert_eq!(decode("Zg"), None);
102        assert_eq!(decode("Zg==Zg=="), None);
103        assert_eq!(decode("Zm9v!A=="), None);
104        assert_eq!(decode("===="), None);
105        let root = "CsUYapGGPo4dkMgIAUqom/Xajj7h2fB2MPA3j2jxq2I=";
106        let bytes = decode(root).unwrap();
107        assert_eq!(bytes.len(), 32);
108        assert_eq!(encode(&bytes), root);
109    }
110}