1use std::fmt;
20
21const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
23
24const PAD: u8 = b'=';
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum DecodeError {
30 Character {
32 at: usize,
34 byte: u8,
36 },
37 Length(usize),
39 Padding(usize),
41}
42
43impl fmt::Display for DecodeError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 DecodeError::Character { at, byte } => {
47 write!(f, "byte {at} is {byte:#04x}, which is not base64")
48 }
49 DecodeError::Length(len) => {
50 write!(
51 f,
52 "{len} base64 characters leave a group of one, which encodes nothing"
53 )
54 }
55 DecodeError::Padding(at) => write!(f, "padding at byte {at} is before the end"),
56 }
57 }
58}
59
60impl std::error::Error for DecodeError {}
61
62pub fn encode(bytes: &[u8]) -> String {
72 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
73 for group in bytes.chunks(3) {
74 let mut packed = 0u32;
75 for (index, byte) in group.iter().enumerate() {
76 packed |= u32::from(*byte) << (16 - 8 * index);
77 }
78 let characters = group.len() + 1;
79 for index in 0..characters {
80 let sextet = (packed >> (18 - 6 * index)) & 0x3F;
81 out.push(char::from(ALPHABET[sextet as usize]));
82 }
83 for _ in characters..4 {
84 out.push(char::from(PAD));
85 }
86 }
87 out
88}
89
90pub fn decode(text: &str) -> Result<Vec<u8>, DecodeError> {
105 let bytes = text.as_bytes();
106 let body = match bytes.iter().position(|byte| *byte == PAD) {
107 Some(at) => {
108 if bytes[at..].iter().any(|byte| *byte != PAD) || bytes.len() - at > 2 {
109 return Err(DecodeError::Padding(at));
110 }
111 &bytes[..at]
112 }
113 None => bytes,
114 };
115 if body.len() % 4 == 1 {
116 return Err(DecodeError::Length(body.len()));
117 }
118
119 let mut out = Vec::with_capacity(body.len() / 4 * 3);
120 for (group, characters) in body.chunks(4).enumerate() {
121 let mut packed = 0u32;
122 for (index, byte) in characters.iter().enumerate() {
123 let sextet = sextet(*byte).ok_or(DecodeError::Character {
124 at: group * 4 + index,
125 byte: *byte,
126 })?;
127 packed |= u32::from(sextet) << (18 - 6 * index);
128 }
129 for index in 0..characters.len() - 1 {
130 out.push(((packed >> (16 - 8 * index)) & 0xFF) as u8);
131 }
132 }
133 Ok(out)
134}
135
136fn sextet(byte: u8) -> Option<u8> {
138 ALPHABET
139 .iter()
140 .position(|character| *character == byte)
141 .map(|value| value as u8)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 const VECTORS: [(&str, &str); 7] = [
150 ("", ""),
151 ("f", "Zg=="),
152 ("fo", "Zm8="),
153 ("foo", "Zm9v"),
154 ("foob", "Zm9vYg=="),
155 ("fooba", "Zm9vYmE="),
156 ("foobar", "Zm9vYmFy"),
157 ];
158
159 #[test]
160 fn the_rfc_vectors_encode_and_decode() {
161 for (plain, encoded) in VECTORS {
162 assert_eq!(encode(plain.as_bytes()), encoded, "encoding {plain:?}");
163 assert_eq!(
164 decode(encoded).expect("the vector decodes"),
165 plain.as_bytes(),
166 "decoding {encoded:?}"
167 );
168 }
169 }
170
171 #[test]
172 fn padding_is_optional_on_the_way_in() {
173 for (plain, encoded) in VECTORS {
174 let stripped = encoded.trim_end_matches('=');
175 assert_eq!(
176 decode(stripped).expect("an unpadded vector decodes"),
177 plain.as_bytes(),
178 "decoding {stripped:?}"
179 );
180 }
181 }
182
183 #[test]
184 fn the_protocols_own_example_payload_decodes() {
185 let heard = decode("VEVTVF9QQUNLRVRfMTIzNA==").expect("the example decodes");
187 assert_eq!(heard, b"TEST_PACKET_1234");
188 }
189
190 #[test]
191 fn every_byte_survives_a_round_trip() {
192 let all: Vec<u8> = (0..=255).collect();
193 for len in 0..=all.len() {
194 let bytes = &all[..len];
195 let round = decode(&encode(bytes)).expect("what we encoded decodes");
196 assert_eq!(round, bytes, "{len} bytes");
197 }
198 }
199
200 #[test]
201 fn what_is_not_base64_is_refused() {
202 assert_eq!(
203 decode("Zm9v!g=="),
204 Err(DecodeError::Character { at: 4, byte: b'!' })
205 );
206 assert_eq!(decode("Zg=a"), Err(DecodeError::Padding(2)));
207 assert_eq!(decode("Zm9vY"), Err(DecodeError::Length(5)));
208 assert!(decode("Zm9vYg==").is_ok(), "a padded group is still good");
209 }
210}