Skip to main content

base64/engine/general_purpose/
decode.rs

1use 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    /// input len % 4
10    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/// Helper to avoid duplicating `num_chunks` calculation, which is costly on short inputs.
31/// Returns the decode metadata, or an error.
32// We're on the fragile edge of compiler heuristics here. If this is not inlined, slow. If this is
33// inlined(always), a different slow. plain ol' inline makes the benchmarks happiest at the moment,
34// but this is fragile and the best setting changes with only minor code modifications.
35#[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    // A SIMD backend, when applicable, decodes a leading prefix; the scalar loops decode the rest.
53    // `simd_prefix` returns `(input_consumed, output_written)` and must consume only whole, valid
54    // quads: `input_consumed % 4 == 0` and `<= input_complete_nonterminal_quads_len` (the terminal
55    // quad is left to `decode_suffix`), `output_written == input_consumed / 4 * 3`, and only those
56    // output bytes are written. It stops on the first invalid/ambiguous quad so the scalar decoder
57    // reports the precise error offset. `(0, 0)` (pure scalar) is always valid.
58    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/// Decode the complete non-terminal quads in `input[input_index_start..input_index_end]` (both
93/// bounds multiples of 4), writing to `output` starting at `output_index_start`, which must equal
94/// `input_index_start / 4 * 3`. Error offsets are reported in absolute input coordinates.
95#[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    // chunks of 32 bytes
122    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    // remaining quads, except for the last possibly partial one, as it may have padding
157    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
176/// Returns the length of complete quads, except for the last one, even if it is complete.
177///
178/// Returns an error if the output len is not big enough for decoding those complete quads, or if
179/// the input % 4 == 1, and that last byte is an invalid value other than a pad byte.
180///
181/// - `input` is the base64 input
182/// - `input_len_rem` is input len % 4
183/// - `output_len` is the length of the output slice
184pub(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    // detect a trailing invalid byte, like a newline, as a user convenience
194    if input_len_rem == 1 {
195        let last_byte = input[input.len() - 1];
196        // exclude pad bytes; might be part of padding that extends from earlier in the input
197        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    // skip last quad, even if it's complete, as it may have padding
203    let input_complete_nonterminal_quads_len = input
204        .len()
205        .saturating_sub(input_len_rem)
206        // if rem was 0, subtract 4 to avoid padding
207        .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    // check that everything except the last quad handled by decode_suffix will fit
213    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/// Decode 8 bytes of input into 6 bytes of output.
220///
221/// `input` is the 8 bytes to decode.
222/// `index_at_start_of_input` is the offset in the overall input (used for reporting errors
223/// accurately)
224/// `decode_table` is the lookup table for the particular base64 alphabet.
225/// `output` will have its first 6 bytes overwritten
226// yes, really inline (worth 30-50% speedup)
227#[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/// Like [`decode_chunk_8`] but for 4 bytes of input and 3 bytes of output.
309#[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"; // "foobar"
363        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"; // "foobar"
372        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        // cover both ends of usize
398        (0..1000)
399            .chain(usize::MAX - 1000..=usize::MAX)
400            .for_each(|encoded_len| {
401                // inflate to 128 bit type to be able to safely use the easy formulas
402                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}