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
30pub static STANDARD: Alphabet =
32 Alphabet::new(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
33
34pub 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#[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#[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}