1const STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12const URL_SAFE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
13
14pub fn encode(input: &[u8]) -> String {
16 encode_with(input, STANDARD, true)
17}
18
19pub fn encode_url(input: &[u8]) -> String {
21 encode_with(input, URL_SAFE, false)
22}
23
24fn encode_with(input: &[u8], alphabet: &[u8; 64], pad: bool) -> String {
25 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
26
27 for chunk in input.chunks(3) {
28 let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
29 let bits = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
30
31 out.push(alphabet[(bits >> 18 & 0x3f) as usize] as char);
32 out.push(alphabet[(bits >> 12 & 0x3f) as usize] as char);
33
34 match (chunk.len() > 1, pad) {
35 (true, _) => out.push(alphabet[(bits >> 6 & 0x3f) as usize] as char),
36 (false, true) => out.push('='),
37 (false, false) => {}
38 }
39 match (chunk.len() > 2, pad) {
40 (true, _) => out.push(alphabet[(bits & 0x3f) as usize] as char),
41 (false, true) => out.push('='),
42 (false, false) => {}
43 }
44 }
45
46 out
47}
48
49pub fn decode(input: &str) -> Option<Vec<u8>> {
57 let mut out = Vec::with_capacity(input.len() / 4 * 3);
58 let mut buffer = 0u32;
59 let mut bits = 0u32;
60
61 for byte in input.bytes() {
62 let value = match byte {
63 b'A'..=b'Z' => byte - b'A',
64 b'a'..=b'z' => byte - b'a' + 26,
65 b'0'..=b'9' => byte - b'0' + 52,
66 b'+' | b'-' => 62,
67 b'/' | b'_' => 63,
68 b'=' | b'\n' | b'\r' => continue,
69 _ => return None,
70 };
71
72 buffer = (buffer << 6) | u32::from(value);
73 bits += 6;
74 if bits >= 8 {
75 bits -= 8;
76 out.push((buffer >> bits) as u8);
77 }
78 }
79
80 Some(out)
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn standard_encoding_matches_the_rfc_4648_vectors() {
89 for (plain, encoded) in [
90 ("", ""),
91 ("f", "Zg=="),
92 ("fo", "Zm8="),
93 ("foo", "Zm9v"),
94 ("foob", "Zm9vYg=="),
95 ("fooba", "Zm9vYmE="),
96 ("foobar", "Zm9vYmFy"),
97 ] {
98 assert_eq!(encode(plain.as_bytes()), encoded, "encoding {plain:?}");
99 assert_eq!(decode(encoded).unwrap(), plain.as_bytes(), "decoding {encoded:?}");
100 }
101 }
102
103 #[test]
104 fn url_safe_encoding_avoids_the_characters_a_url_would_escape() {
105 let bytes = [0xfb, 0xff, 0xbf];
107 assert_eq!(encode(&bytes), "+/+/");
108 assert_eq!(encode_url(&bytes), "-_-_");
109
110 let unpadded = encode_url(b"f");
111 assert_eq!(unpadded, "Zg");
112 assert_eq!(decode(&unpadded).unwrap(), b"f");
113 }
114
115 #[test]
116 fn both_alphabets_round_trip_arbitrary_bytes() {
117 let bytes: Vec<u8> = (0..=255).collect();
118
119 assert_eq!(decode(&encode(&bytes)).unwrap(), bytes);
120 assert_eq!(decode(&encode_url(&bytes)).unwrap(), bytes);
121 }
122
123 #[test]
124 fn rejects_characters_outside_the_alphabets() {
125 assert!(decode("not base64!").is_none());
126 assert!(decode("abc$def").is_none());
127 }
128}