base64/engine/general_purpose/
decode.rs1use crate::alphabet::Symbol;
2use crate::{
3 engine::{general_purpose::INVALID_VALUE, DecodeEstimate, DecodeMetadata, DecodePaddingMode},
4 DecodeError, DecodeSliceError,
5};
6
7#[doc(hidden)]
8pub struct GeneralPurposeEstimate {
9 rem: usize,
11 conservative_decoded_len: usize,
12}
13
14impl GeneralPurposeEstimate {
15 pub(crate) fn new(encoded_len: usize) -> Self {
16 let rem = encoded_len % 4;
17 Self {
18 rem,
19 conservative_decoded_len: (encoded_len / 4 + usize::from(rem > 0)) * 3,
20 }
21 }
22}
23
24impl DecodeEstimate for GeneralPurposeEstimate {
25 fn decoded_len_estimate(&self) -> usize {
26 self.conservative_decoded_len
27 }
28}
29
30#[allow(clippy::too_many_arguments)]
36#[inline]
37pub(crate) fn decode_helper(
38 input: &[u8],
39 estimate: &GeneralPurposeEstimate,
40 output: &mut [u8],
41 decode_table: &[u8; 256],
42 decode_allow_trailing_bits: bool,
43 padding: Symbol,
44 padding_mode: DecodePaddingMode,
45 simd_prefix: impl FnOnce(&[u8], usize, &mut [u8]) -> (usize, usize),
46) -> Result<DecodeMetadata, DecodeSliceError> {
47 let input_complete_nonterminal_quads_len =
48 complete_quads_len(input, estimate.rem, output.len(), decode_table, padding)?;
49
50 let output_complete_quad_len = input_complete_nonterminal_quads_len / 4 * 3;
51
52 let (input_index, output_index) =
59 simd_prefix(input, input_complete_nonterminal_quads_len, output);
60
61 debug_assert!(input_index % 4 == 0, "prefix must consume whole quads");
62 debug_assert!(
63 input_index <= input_complete_nonterminal_quads_len,
64 "prefix must not consume the terminal quad"
65 );
66 debug_assert!(
67 output_index == input_index / 4 * 3,
68 "prefix output must match consumed input"
69 );
70
71 decode_complete_quads(
72 input,
73 input_index,
74 input_complete_nonterminal_quads_len,
75 decode_table,
76 output,
77 output_index,
78 )?;
79
80 super::decode_suffix::decode_suffix(
81 input,
82 input_complete_nonterminal_quads_len,
83 output,
84 output_complete_quad_len,
85 decode_table,
86 decode_allow_trailing_bits,
87 padding,
88 padding_mode,
89 )
90}
91
92#[inline]
96fn decode_complete_quads(
97 input: &[u8],
98 input_index_start: usize,
99 input_index_end: usize,
100 decode_table: &[u8; 256],
101 output: &mut [u8],
102 output_index_start: usize,
103) -> Result<(), DecodeSliceError> {
104 debug_assert!(
105 input_index_start % 4 == 0,
106 "quad start must be quad-aligned"
107 );
108 debug_assert!(input_index_end % 4 == 0, "quad end must be quad-aligned");
109 debug_assert!(
110 output_index_start == input_index_start / 4 * 3,
111 "output start must match consumed input"
112 );
113
114 const UNROLLED_INPUT_CHUNK_SIZE: usize = 32;
115 const UNROLLED_OUTPUT_CHUNK_SIZE: usize = UNROLLED_INPUT_CHUNK_SIZE / 4 * 3;
116
117 let quads_len = input_index_end - input_index_start;
118 let unrolled_loop_len = quads_len - quads_len % UNROLLED_INPUT_CHUNK_SIZE;
119 let input_unrolled_loop_end = input_index_start + unrolled_loop_len;
120
121 for (chunk_index, chunk) in input[input_index_start..input_unrolled_loop_end]
123 .chunks_exact(UNROLLED_INPUT_CHUNK_SIZE)
124 .enumerate()
125 {
126 let input_index = input_index_start + chunk_index * UNROLLED_INPUT_CHUNK_SIZE;
127 let output_base = output_index_start + chunk_index * UNROLLED_OUTPUT_CHUNK_SIZE;
128 let chunk_output = &mut output[output_base..output_base + UNROLLED_OUTPUT_CHUNK_SIZE];
129
130 decode_chunk_8(
131 &chunk[0..8],
132 input_index,
133 decode_table,
134 &mut chunk_output[0..6],
135 )?;
136 decode_chunk_8(
137 &chunk[8..16],
138 input_index + 8,
139 decode_table,
140 &mut chunk_output[6..12],
141 )?;
142 decode_chunk_8(
143 &chunk[16..24],
144 input_index + 16,
145 decode_table,
146 &mut chunk_output[12..18],
147 )?;
148 decode_chunk_8(
149 &chunk[24..32],
150 input_index + 24,
151 decode_table,
152 &mut chunk_output[18..24],
153 )?;
154 }
155
156 let output_after_unroll_start = output_index_start + unrolled_loop_len / 4 * 3;
158 for (chunk_index, chunk) in input[input_unrolled_loop_end..input_index_end]
159 .chunks_exact(4)
160 .enumerate()
161 {
162 let output_base = output_after_unroll_start + chunk_index * 3;
163 let chunk_output = &mut output[output_base..output_base + 3];
164
165 decode_chunk_4(
166 chunk,
167 input_unrolled_loop_end + chunk_index * 4,
168 decode_table,
169 chunk_output,
170 )?;
171 }
172
173 Ok(())
174}
175
176pub(crate) fn complete_quads_len(
185 input: &[u8],
186 input_len_rem: usize,
187 output_len: usize,
188 decode_table: &[u8; 256],
189 padding: Symbol,
190) -> Result<usize, DecodeSliceError> {
191 debug_assert!(input.len() % 4 == input_len_rem);
192
193 if input_len_rem == 1 {
195 let last_byte = input[input.len() - 1];
196 if last_byte != padding.as_u8() && decode_table[usize::from(last_byte)] == INVALID_VALUE {
198 return Err(DecodeError::InvalidByte(input.len() - 1, last_byte).into());
199 }
200 };
201
202 let input_complete_nonterminal_quads_len = input
204 .len()
205 .saturating_sub(input_len_rem)
206 .saturating_sub(usize::from(input_len_rem == 0) * 4);
208 debug_assert!(
209 input.is_empty() || (1..=4).contains(&(input.len() - input_complete_nonterminal_quads_len))
210 );
211
212 if output_len < input_complete_nonterminal_quads_len / 4 * 3 {
214 return Err(DecodeSliceError::OutputSliceTooSmall);
215 };
216 Ok(input_complete_nonterminal_quads_len)
217}
218
219#[inline(always)]
228fn decode_chunk_8(
229 input: &[u8],
230 index_at_start_of_input: usize,
231 decode_table: &[u8; 256],
232 output: &mut [u8],
233) -> Result<(), DecodeError> {
234 let morsel = decode_table[usize::from(input[0])];
235 if morsel == INVALID_VALUE {
236 return Err(DecodeError::InvalidByte(index_at_start_of_input, input[0]));
237 }
238 let mut accum = u64::from(morsel) << 58;
239
240 let morsel = decode_table[usize::from(input[1])];
241 if morsel == INVALID_VALUE {
242 return Err(DecodeError::InvalidByte(
243 index_at_start_of_input + 1,
244 input[1],
245 ));
246 }
247 accum |= u64::from(morsel) << 52;
248
249 let morsel = decode_table[usize::from(input[2])];
250 if morsel == INVALID_VALUE {
251 return Err(DecodeError::InvalidByte(
252 index_at_start_of_input + 2,
253 input[2],
254 ));
255 }
256 accum |= u64::from(morsel) << 46;
257
258 let morsel = decode_table[usize::from(input[3])];
259 if morsel == INVALID_VALUE {
260 return Err(DecodeError::InvalidByte(
261 index_at_start_of_input + 3,
262 input[3],
263 ));
264 }
265 accum |= u64::from(morsel) << 40;
266
267 let morsel = decode_table[usize::from(input[4])];
268 if morsel == INVALID_VALUE {
269 return Err(DecodeError::InvalidByte(
270 index_at_start_of_input + 4,
271 input[4],
272 ));
273 }
274 accum |= u64::from(morsel) << 34;
275
276 let morsel = decode_table[usize::from(input[5])];
277 if morsel == INVALID_VALUE {
278 return Err(DecodeError::InvalidByte(
279 index_at_start_of_input + 5,
280 input[5],
281 ));
282 }
283 accum |= u64::from(morsel) << 28;
284
285 let morsel = decode_table[usize::from(input[6])];
286 if morsel == INVALID_VALUE {
287 return Err(DecodeError::InvalidByte(
288 index_at_start_of_input + 6,
289 input[6],
290 ));
291 }
292 accum |= u64::from(morsel) << 22;
293
294 let morsel = decode_table[usize::from(input[7])];
295 if morsel == INVALID_VALUE {
296 return Err(DecodeError::InvalidByte(
297 index_at_start_of_input + 7,
298 input[7],
299 ));
300 }
301 accum |= u64::from(morsel) << 16;
302
303 output[..6].copy_from_slice(&accum.to_be_bytes()[..6]);
304
305 Ok(())
306}
307
308#[inline(always)]
310fn decode_chunk_4(
311 input: &[u8],
312 index_at_start_of_input: usize,
313 decode_table: &[u8; 256],
314 output: &mut [u8],
315) -> Result<(), DecodeError> {
316 let morsel = decode_table[usize::from(input[0])];
317 if morsel == INVALID_VALUE {
318 return Err(DecodeError::InvalidByte(index_at_start_of_input, input[0]));
319 }
320 let mut accum = u32::from(morsel) << 26;
321
322 let morsel = decode_table[usize::from(input[1])];
323 if morsel == INVALID_VALUE {
324 return Err(DecodeError::InvalidByte(
325 index_at_start_of_input + 1,
326 input[1],
327 ));
328 }
329 accum |= u32::from(morsel) << 20;
330
331 let morsel = decode_table[usize::from(input[2])];
332 if morsel == INVALID_VALUE {
333 return Err(DecodeError::InvalidByte(
334 index_at_start_of_input + 2,
335 input[2],
336 ));
337 }
338 accum |= u32::from(morsel) << 14;
339
340 let morsel = decode_table[usize::from(input[3])];
341 if morsel == INVALID_VALUE {
342 return Err(DecodeError::InvalidByte(
343 index_at_start_of_input + 3,
344 input[3],
345 ));
346 }
347 accum |= u32::from(morsel) << 8;
348
349 output[..3].copy_from_slice(&accum.to_be_bytes()[..3]);
350
351 Ok(())
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 use crate::engine::general_purpose::STANDARD;
359
360 #[test]
361 fn decode_chunk_8_writes_only_6_bytes() {
362 let input = b"Zm9vYmFy"; let mut output = [0_u8, 1, 2, 3, 4, 5, 6, 7];
364
365 decode_chunk_8(&input[..], 0, &STANDARD.decode_table, &mut output).unwrap();
366 assert_eq!(&vec![b'f', b'o', b'o', b'b', b'a', b'r', 6, 7], &output);
367 }
368
369 #[test]
370 fn decode_chunk_4_writes_only_3_bytes() {
371 let input = b"Zm9v"; let mut output = [0_u8, 1, 2, 3];
373
374 decode_chunk_4(&input[..], 0, &STANDARD.decode_table, &mut output).unwrap();
375 assert_eq!(&vec![b'f', b'o', b'o', 3], &output);
376 }
377
378 #[test]
379 fn estimate_short_lengths() {
380 for (range, decoded_len_estimate) in [
381 (0..=0, 0),
382 (1..=4, 3),
383 (5..=8, 6),
384 (9..=12, 9),
385 (13..=16, 12),
386 (17..=20, 15),
387 ] {
388 for encoded_len in range {
389 let estimate = GeneralPurposeEstimate::new(encoded_len);
390 assert_eq!(decoded_len_estimate, estimate.decoded_len_estimate());
391 }
392 }
393 }
394
395 #[test]
396 fn estimate_via_u128_inflation() {
397 (0..1000)
399 .chain(usize::MAX - 1000..=usize::MAX)
400 .for_each(|encoded_len| {
401 let len_128 = encoded_len as u128;
403
404 let estimate = GeneralPurposeEstimate::new(encoded_len);
405 assert_eq!(
406 (len_128 + 3) / 4 * 3,
407 estimate.conservative_decoded_len as u128
408 );
409 })
410 }
411}