Skip to main content

hammer/
base64.rs

1//! Binary content, such as an exported file or an image, which operations carry as base64 text.
2
3use serde::{Deserialize, Deserializer, Serializer};
4
5const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6
7pub fn encode(bytes: &[u8]) -> String {
8    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
9    for chunk in bytes.chunks(3) {
10        let n = chunk.iter().enumerate().fold(0u32, |n, (i, b)| n | (*b as u32) << (16 - 8 * i));
11        for i in 0..4 {
12            out.push(if i <= chunk.len() { ALPHABET[(n >> (18 - 6 * i) & 63) as usize] as char } else { '=' });
13        }
14    }
15    out
16}
17
18const INVALID: u8 = 0xFF;
19
20/// Each byte's six bits, or [INVALID] for one outside the alphabet.
21const VALUES: [u8; 256] = {
22    let mut values = [INVALID; 256];
23    let mut i = 0;
24    while i < 64 {
25        values[ALPHABET[i] as usize] = i as u8;
26        i += 1;
27    }
28    values
29};
30
31/// The bytes `text` holds, or none for text that is not base64: a byte outside the alphabet, `=` anywhere
32/// but the end, more than two `=` or too few to fill the last group of four, or a last group of one
33/// character. Padding may be left out altogether.
34pub fn decode(text: &str) -> Option<Vec<u8>> {
35    let padded = text.as_bytes();
36    let text = padded.strip_suffix(b"==").or_else(|| padded.strip_suffix(b"=")).unwrap_or(padded);
37    if text.len() % 4 == 1 || (text.len() < padded.len() && padded.len() % 4 != 0) {
38        return None;
39    }
40    let mut out = Vec::with_capacity(text.len() * 3 / 4);
41    let (mut n, mut bits) = (0u32, 0);
42    for &c in text {
43        let value = VALUES[c as usize];
44        if value == INVALID {
45            return None;
46        }
47        n = n << 6 | value as u32;
48        bits += 6;
49        if bits >= 8 {
50            bits -= 8;
51            out.push((n >> bits) as u8);
52        }
53    }
54    Some(out)
55}
56
57pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
58    serializer.serialize_str(&encode(bytes))
59}
60
61pub mod bytes {
62    use super::*;
63
64    pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
65        super::serialize(bytes, serializer)
66    }
67
68    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
69        let text = String::deserialize(deserializer)?;
70        decode(&text).ok_or_else(|| serde::de::Error::custom("invalid base64"))
71    }
72}
73
74pub mod option {
75    use super::*;
76
77    pub fn serialize<S: Serializer>(bytes: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error> {
78        match bytes {
79            Some(bytes) => super::serialize(bytes, serializer),
80            None => serializer.serialize_none(),
81        }
82    }
83
84    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error> {
85        Option::<String>::deserialize(deserializer)?
86            .map(|text| decode(&text).ok_or_else(|| serde::de::Error::custom("invalid base64")))
87            .transpose()
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn round_trips() {
97        for bytes in [&b""[..], b"f", b"fo", b"foo", b"foob", b"fooba", b"foobar", &[0, 255, 128, 7]] {
98            assert_eq!(decode(&encode(bytes)).unwrap(), bytes);
99        }
100        assert_eq!(encode(b"foobar"), "Zm9vYmFy");
101        assert_eq!(encode(b"fo"), "Zm8=");
102    }
103
104    #[test]
105    fn decodes_padded_and_unpadded() {
106        for (text, bytes) in [
107            ("", &b""[..]),
108            ("Zg==", b"f"),
109            ("Zm8=", b"fo"),
110            ("Zm9v", b"foo"),
111            ("Zm9vYg", b"foob"),
112            ("Zm8", b"fo"),
113            ("iVBORwD/", b"\x89PNG\0\xFF"),
114            ("+/+/", b"\xFB\xFF\xBF"),
115        ] {
116            assert_eq!(decode(text).as_deref(), Some(bytes), "{text}");
117        }
118    }
119
120    #[test]
121    fn refuses_what_is_not_base64() {
122        for text in
123            ["Z", "Zg=", "Zg===", "Zm9vY", "Zm9v=", "Zm9v==", "Z=g=", "Zm 9v", "Zm9v\n", "Zm-_", "=", "==", "Zm\u{e9}"]
124        {
125            assert_eq!(decode(text), None, "{text:?}");
126        }
127    }
128
129    #[test]
130    fn round_trips_pseudo_random_bytes() {
131        let mut state = 0x2545_f491_4f6c_dd1du64;
132        let bytes: Vec<u8> = (0..4099)
133            .map(|_| {
134                state ^= state << 13;
135                state ^= state >> 7;
136                state ^= state << 17;
137                state as u8
138            })
139            .collect();
140        assert_eq!(decode(&encode(&bytes)).unwrap(), bytes);
141    }
142}