Skip to main content

sofie_core/encoding/
base64.rs

1pub struct Alphabet {
2    symbols: &'static [u8; 64],
3    values: [u8; 256],
4}
5
6const NOT_A_SYMBOL: u8 = u8::MAX;
7
8impl Alphabet {
9    const fn new(symbols: &'static [u8; 64]) -> Self {
10        let mut values = [NOT_A_SYMBOL; 256];
11        let mut value = 0;
12        while value < symbols.len() {
13            #[expect(
14                clippy::cast_possible_truncation,
15                reason = "an alphabet has 64 symbols"
16            )]
17            let symbol_value = value as u8;
18            values[symbols[value] as usize] = symbol_value;
19            value += 1;
20        }
21        Self { symbols, values }
22    }
23
24    fn value_of(&self, symbol: u8) -> Option<u32> {
25        let value = self.values[usize::from(symbol)];
26        (value != NOT_A_SYMBOL).then_some(u32::from(value))
27    }
28}
29
30/// RFC 4648 section 4: what key generators print.
31pub static STANDARD: Alphabet =
32    Alphabet::new(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
33
34/// RFC 4648 section 5: safe in a cookie and a URL.
35pub static URL_SAFE: Alphabet =
36    Alphabet::new(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
37
38#[must_use]
39pub fn encode(alphabet: &Alphabet, bytes: &[u8]) -> String {
40    let mut text = String::with_capacity(bytes.len().div_ceil(3) * 4);
41    push(&mut text, alphabet, bytes);
42    text
43}
44
45/// Padded to a multiple of four, as MIME and SMTP expect.
46#[must_use]
47pub fn encode_padded(alphabet: &Alphabet, bytes: &[u8]) -> String {
48    let mut text = String::with_capacity(bytes.len().div_ceil(3) * 4);
49    push_padded(&mut text, alphabet, bytes);
50    text
51}
52
53pub fn push_padded(text: &mut String, alphabet: &Alphabet, bytes: &[u8]) {
54    push(text, alphabet, bytes);
55    let padding = (3 - bytes.len() % 3) % 3;
56    text.extend(std::iter::repeat_n('=', padding));
57}
58
59fn push(text: &mut String, alphabet: &Alphabet, bytes: &[u8]) {
60    for chunk in bytes.chunks(3) {
61        let group = chunk.iter().enumerate().fold(0u32, |group, (index, byte)| {
62            group | u32::from(*byte) << (16 - 8 * index)
63        });
64        for index in 0..=chunk.len() {
65            let symbol = (group >> (18 - 6 * index)) & 0x3f;
66            text.push(char::from(alphabet.symbols[symbol as usize]));
67        }
68    }
69}
70
71/// Canonical text only: padding absent or complete, and no bits set past the last byte,
72/// so each byte string has exactly one accepted spelling.
73#[must_use]
74pub fn decode(alphabet: &Alphabet, text: &str) -> Option<Vec<u8>> {
75    let symbols = text.trim_end_matches('=').as_bytes();
76    let padding = text.len() - symbols.len();
77    if symbols.len() % 4 == 1 || (padding != 0 && padding != (4 - symbols.len() % 4) % 4) {
78        return None;
79    }
80    let mut bytes = Vec::with_capacity(symbols.len() * 3 / 4);
81    for chunk in symbols.chunks(4) {
82        let mut group = 0u32;
83        for (index, symbol) in chunk.iter().enumerate() {
84            group |= alphabet.value_of(*symbol)? << (18 - 6 * index);
85        }
86        let decoded = group.to_be_bytes();
87        if decoded[chunk.len()..].iter().any(|byte| *byte != 0) {
88            return None;
89        }
90        bytes.extend_from_slice(&decoded[1..chunk.len()]);
91    }
92    Some(bytes)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn matches_the_rfc_4648_test_vectors() {
101        let vectors = [
102            ("", ""),
103            ("f", "Zg"),
104            ("fo", "Zm8"),
105            ("foo", "Zm9v"),
106            ("foob", "Zm9vYg"),
107            ("fooba", "Zm9vYmE"),
108            ("foobar", "Zm9vYmFy"),
109        ];
110        for (plain, encoded) in vectors {
111            assert_eq!(encode(&STANDARD, plain.as_bytes()), encoded);
112            assert_eq!(decode(&STANDARD, encoded).unwrap(), plain.as_bytes());
113        }
114        assert_eq!(decode(&STANDARD, "Zm8=").unwrap(), b"fo");
115    }
116
117    #[test]
118    fn the_alphabets_differ_only_in_their_last_two_symbols() {
119        let bytes = [0xfb, 0xff];
120        assert_eq!(encode(&STANDARD, &bytes), "+/8");
121        assert_eq!(encode(&URL_SAFE, &bytes), "-_8");
122        assert_eq!(decode(&URL_SAFE, "-_8").unwrap(), bytes);
123        assert_eq!(decode(&URL_SAFE, "+/8"), None);
124    }
125
126    #[test]
127    fn refuses_what_no_encoding_produces() {
128        assert_eq!(decode(&STANDARD, "Z"), None);
129        assert_eq!(decode(&STANDARD, "Zm9v!"), None);
130        assert_eq!(decode(&STANDARD, "Zh"), None);
131        assert_eq!(decode(&STANDARD, "Zm8=="), None);
132        assert_eq!(decode(&STANDARD, "Zg="), None);
133        assert_eq!(decode(&STANDARD, "Zg==").unwrap(), b"f");
134        assert_eq!(decode(&STANDARD, "Zm9v===="), None);
135        assert_eq!(decode(&STANDARD, "Zg======"), None);
136        assert_eq!(decode(&STANDARD, "Zm9v="), None);
137    }
138
139    #[test]
140    fn padded_encoding_matches_the_rfc_4648_test_vectors() {
141        let vectors = [
142            ("", ""),
143            ("f", "Zg=="),
144            ("fo", "Zm8="),
145            ("foo", "Zm9v"),
146            ("foob", "Zm9vYg=="),
147            ("fooba", "Zm9vYmE="),
148            ("foobar", "Zm9vYmFy"),
149        ];
150        for (plain, encoded) in vectors {
151            assert_eq!(encode_padded(&STANDARD, plain.as_bytes()), encoded);
152            assert_eq!(decode(&STANDARD, encoded).unwrap(), plain.as_bytes());
153        }
154        let mut text = String::from("x");
155        push_padded(&mut text, &STANDARD, &[0xff, 0xfe]);
156        assert_eq!(text, "x//4=");
157    }
158}