1use base64::engine::general_purpose::STANDARD_NO_PAD;
16use base64::Engine as _;
17
18#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
20pub enum DecodeError {
21 #[error("invalid character {0:?}")]
25 InvalidCharacter(char),
26 #[error("truncated: {0} leftover bits encode no whole byte")]
29 Truncated(u32),
30 #[error("the final character has {0} non-zero unused bits")]
34 UnusedBits(u32),
35}
36
37const BASE32: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
39
40fn decode(text: &str, alphabet: &[u8], bits: u32) -> Result<Vec<u8>, DecodeError> {
44 let mut out = Vec::with_capacity(text.len() * bits as usize / 8);
45 let mut buffer: u32 = 0;
46 let mut held: u32 = 0;
47 for ch in text.chars() {
48 let value = alphabet
49 .iter()
50 .position(|c| char::from(*c) == ch)
51 .ok_or(DecodeError::InvalidCharacter(ch))?;
52 buffer = (buffer << bits) | value as u32;
53 held += bits;
54 if held >= 8 {
55 held -= 8;
56 out.push((buffer >> held) as u8);
57 }
58 }
59 if held >= bits {
62 return Err(DecodeError::Truncated(held));
63 }
64 let leftover = buffer & ((1 << held) - 1);
65 if leftover != 0 {
66 return Err(DecodeError::UnusedBits(leftover.count_ones()));
67 }
68 Ok(out)
69}
70
71fn encode(bytes: &[u8], alphabet: &[u8], bits: u32) -> String {
73 let mut out = String::with_capacity(bytes.len() * 8 / bits as usize + 1);
74 let mut buffer: u32 = 0;
75 let mut held: u32 = 0;
76 for byte in bytes {
77 buffer = (buffer << 8) | u32::from(*byte);
78 held += 8;
79 while held >= bits {
80 held -= bits;
81 out.push(alphabet[((buffer >> held) & ((1 << bits) - 1)) as usize] as char);
82 }
83 }
84 if held > 0 {
85 let index = (buffer << (bits - held)) & ((1 << bits) - 1);
86 out.push(alphabet[index as usize] as char);
87 }
88 out
89}
90
91pub fn base32_decode(text: &str) -> Result<Vec<u8>, DecodeError> {
93 decode(text, BASE32, 5)
94}
95
96pub fn base32_encode(bytes: &[u8]) -> String {
98 encode(bytes, BASE32, 5)
99}
100
101pub fn base64_decode(text: &str) -> Result<Vec<u8>, base64::DecodeError> {
103 STANDARD_NO_PAD.decode(text)
104}
105
106pub fn base64_encode(bytes: &[u8]) -> String {
108 STANDARD_NO_PAD.encode(bytes)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn base64_round_trips_every_tail_length() {
117 for len in 0..8usize {
118 let bytes: Vec<u8> = (0..len)
119 .map(|i| (i as u8).wrapping_mul(37).wrapping_add(9))
120 .collect();
121 let text = base64_encode(&bytes);
122 assert!(!text.contains('='), "{text:?} is padded");
123 assert_eq!(base64_decode(&text), Ok(bytes));
124 }
125 }
126
127 #[test]
128 fn base32_round_trips_every_tail_length() {
129 for len in 0..8usize {
130 let bytes: Vec<u8> = (0..len)
131 .map(|i| (i as u8).wrapping_mul(53).wrapping_add(3))
132 .collect();
133 let text = base32_encode(&bytes);
134 assert_eq!(base32_decode(&text), Ok(bytes));
135 }
136 }
137
138 #[test]
139 fn padding_is_refused() {
140 assert_eq!(
141 base64_decode("YWJjZA=="),
142 Err(base64::DecodeError::InvalidPadding)
143 );
144 }
145
146 #[test]
147 fn a_set_bit_beyond_the_last_byte_is_refused() {
148 assert_eq!(base64_decode("QQ"), Ok(vec![0x41]));
152 assert_eq!(
153 base64_decode("QR"),
154 Err(base64::DecodeError::InvalidLastSymbol(1, b'R'))
155 );
156 }
157
158 #[test]
159 fn a_lone_trailing_character_encodes_no_byte() {
160 assert_eq!(
161 base64_decode("QQQQQ"),
162 Err(base64::DecodeError::InvalidLength(5))
163 );
164 }
165
166 #[test]
167 fn the_url_safe_alphabet_is_not_the_one_atproto_uses() {
168 assert_eq!(
169 base64_decode("-_"),
170 Err(base64::DecodeError::InvalidByte(0, b'-'))
171 );
172 }
173
174 #[test]
176 fn base32_refuses_padding_and_set_unused_bits() {
177 assert_eq!(
178 base32_decode("mfrgg==="),
179 Err(DecodeError::InvalidCharacter('='))
180 );
181 assert_eq!(base32_decode("ae"), Ok(vec![0x01]));
183 assert_eq!(base32_decode("af"), Err(DecodeError::UnusedBits(1)));
184 assert_eq!(base32_decode("a"), Err(DecodeError::Truncated(5)));
185 }
186
187 #[test]
188 fn uppercase_base32_is_a_different_multibase() {
189 assert_eq!(
190 base32_decode("MFRGG"),
191 Err(DecodeError::InvalidCharacter('M'))
192 );
193 }
194}