base64/engine/general_purpose/decode_suffix.rs
1use crate::alphabet::Symbol;
2use crate::{
3 engine::{general_purpose::INVALID_VALUE, DecodeMetadata, DecodePaddingMode},
4 DecodeError, DecodeSliceError,
5};
6
7/// Decode the last 0-4 bytes, checking for trailing set bits and padding per the provided
8/// parameters.
9///
10/// Returns the decode metadata representing the total number of bytes decoded, including the ones
11/// indicated as already written by `output_index`.
12#[allow(clippy::too_many_arguments)]
13pub(crate) fn decode_suffix(
14 input: &[u8],
15 input_index: usize,
16 output: &mut [u8],
17 mut output_index: usize,
18 decode_table: &[u8; 256],
19 decode_allow_trailing_bits: bool,
20 padding: Symbol,
21 padding_mode: DecodePaddingMode,
22) -> Result<DecodeMetadata, DecodeSliceError> {
23 debug_assert!((input.len() - input_index) <= 4);
24
25 // Decode any leftovers that might not be a complete input chunk of 4 bytes.
26 // Use a u32 as a stack-resident 4 byte buffer.
27 let mut morsels_in_leftover = 0;
28 let mut padding_bytes_count = 0;
29 // offset from input_index
30 let mut first_padding_offset: usize = 0;
31 let mut last_symbol = 0_u8;
32 let mut last_symbol_value = 0_u8;
33 let mut morsels = [0_u8; 4];
34
35 for (leftover_index, &b) in input[input_index..].iter().enumerate() {
36 // '=' padding
37 if b == padding.as_u8() {
38 // There can be bad padding bytes in a few ways:
39 // 1 - Padding with non-padding characters after it
40 // 2 - Padding after zero or one characters in the current quad (should only
41 // be after 2 or 3 chars)
42 // 3 - More than two characters of padding. If 3 or 4 padding chars
43 // are in the same quad, that implies it will be caught by #2.
44 // If it spreads from one quad to another, it will be an invalid byte
45 // in the first quad.
46 // 4 - Non-canonical padding -- 1 byte when it should be 2, etc.
47 // Per config, non-canonical but still functional non- or partially-padded base64
48 // may be treated as an error condition.
49
50 if leftover_index < 2 {
51 // Check for error #2.
52 // Either the previous byte was padding, in which case we would have already hit
53 // this case, or it wasn't, in which case this is the first such error.
54 debug_assert!(
55 leftover_index == 0 || (leftover_index == 1 && padding_bytes_count == 0)
56 );
57 let bad_padding_index = input_index + leftover_index;
58 return Err(DecodeError::InvalidByte(bad_padding_index, b).into());
59 }
60
61 if padding_bytes_count == 0 {
62 first_padding_offset = leftover_index;
63 }
64
65 padding_bytes_count += 1;
66 continue;
67 }
68
69 // Check for case #1.
70 // To make '=' handling consistent with the main loop, don't allow
71 // non-suffix '=' in trailing chunk either. Report error as first
72 // erroneous padding.
73 if padding_bytes_count > 0 {
74 return Err(DecodeError::InvalidByte(
75 input_index + first_padding_offset,
76 padding.as_u8(),
77 )
78 .into());
79 }
80
81 last_symbol = b;
82
83 // can use up to 8 * 6 = 48 bits of the u64, if last chunk has no padding.
84 // Pack the leftovers from left to right.
85 let morsel = decode_table[b as usize];
86 last_symbol_value = morsel;
87 if morsel == INVALID_VALUE {
88 return Err(DecodeError::InvalidByte(input_index + leftover_index, b).into());
89 }
90
91 morsels[morsels_in_leftover] = morsel;
92 morsels_in_leftover += 1;
93 }
94
95 // If there was 1 trailing byte, and it was valid, and we got to this point without hitting
96 // an invalid byte, now we can report invalid length
97 if !input.is_empty() && morsels_in_leftover < 2 {
98 return Err(DecodeError::InvalidLength(input_index + morsels_in_leftover).into());
99 }
100
101 match padding_mode {
102 DecodePaddingMode::Indifferent => { /* everything we care about was already checked */ }
103 DecodePaddingMode::RequireCanonical => {
104 // allow empty input
105 if (padding_bytes_count + morsels_in_leftover) % 4 != 0 {
106 return Err(DecodeError::InvalidPadding.into());
107 }
108 }
109 DecodePaddingMode::RequireNone => {
110 if padding_bytes_count > 0 {
111 // check at the end to make sure we let the cases of padding that should be InvalidByte
112 // get hit
113 return Err(DecodeError::InvalidPadding.into());
114 }
115 }
116 }
117
118 // When encoding 1 trailing byte (e.g. 0xFF), 2 base64 bytes ("/w") are needed.
119 // / is the symbol for 63 (0x3F, bottom 6 bits all set) and w is 48 (0x30, top 2 bits
120 // of bottom 6 bits set).
121 // When decoding two symbols back to one trailing byte, any final symbol higher than
122 // w would still decode to the original byte because we only care about the top two
123 // bits in the bottom 6, but would be a non-canonical encoding. So, we calculate a
124 // mask based on how many bits are used for just the canonical encoding, and optionally
125 // error if any other bits are set. In the example of one encoded byte -> 2 symbols,
126 // 2 symbols can technically encode 12 bits, but the last 4 are non-canonical, and
127 // useless since there are no more symbols to provide the necessary 4 additional bits
128 // to finish the second original byte.
129
130 let leftover_bytes_to_append = morsels_in_leftover * 6 / 8;
131 // Put the up to 6 complete bytes as the high bytes.
132 // Gain a couple percent speedup from nudging these ORs to use more ILP with a two-way split.
133 let mut leftover_num = (u32::from(morsels[0]) << 26)
134 | (u32::from(morsels[1]) << 20)
135 | (u32::from(morsels[2]) << 14)
136 | (u32::from(morsels[3]) << 8);
137
138 // if there are bits set outside the bits we care about, last symbol encodes trailing bits that
139 // will not be included in the output
140 let mask = !0_u32 >> (leftover_bytes_to_append * 8);
141 if !decode_allow_trailing_bits && (leftover_num & mask) != 0 {
142 // last morsel is at `morsels_in_leftover` - 1
143 return Err(DecodeError::InvalidLastSymbol {
144 offset: input_index + morsels_in_leftover - 1,
145 symbol: last_symbol,
146 symbol_value: last_symbol_value,
147 }
148 .into());
149 }
150
151 // Strangely, this approach benchmarks better than writing bytes one at a time,
152 // or copy_from_slice into output.
153 for _ in 0..leftover_bytes_to_append {
154 let hi_byte = (leftover_num >> 24) as u8;
155 leftover_num <<= 8;
156 *output
157 .get_mut(output_index)
158 .ok_or(DecodeSliceError::OutputSliceTooSmall)? = hi_byte;
159 output_index += 1;
160 }
161
162 Ok(DecodeMetadata::new(
163 output_index,
164 if padding_bytes_count > 0 {
165 Some(input_index + first_padding_offset)
166 } else {
167 None
168 },
169 ))
170}