/src/openssl/crypto/evp/dec_b64_avx2.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. |
3 | | * |
4 | | * Licensed under the Apache License 2.0 (the "License"). You may not use |
5 | | * this file except in compliance with the License. You can obtain a copy |
6 | | * in the file LICENSE in the source distribution or at |
7 | | * https://www.openssl.org/source/license.html |
8 | | * |
9 | | * AVX2 streaming base64 decoder with whitespace handling fallbacks |
10 | | * |
11 | | * EVP_DecodeUpdate receives raw PEM/SMIME input with newlines embedded |
12 | | * in the data. Three loops handle this with decreasing assumptions, |
13 | | * falling through to the next when broken: |
14 | | * |
15 | | * 1. Fast loop: assumes no whitespace at all. The decoder's error |
16 | | * accumulator flags any non-base64 byte, so WS detection is free, |
17 | | * a WS byte simply triggers fallthrough. This is the fastest path |
18 | | * that has zero overhead beyond raw decoding. |
19 | | * 2. Skip-WS loop: assumes WS falls between 64-byte blocks, not within |
20 | | * them. Skips WS scalarly, then decodes a clean 64-byte block. |
21 | | * Handles PEM 64-char lines entirely (newline always between blocks). |
22 | | * Falls through for 76-char lines where the newline lands mid-block. |
23 | | * 3. General loop: handles WS at arbitrary positions. Detects WS via SIMD, |
24 | | * compresses it out into a stack buffer, and decodes from the buffer |
25 | | * when 64 clean bytes accumulate. A single-byte removal fast path |
26 | | * avoids the full compress cost when a chunk has exactly 1 WS byte |
27 | | * (~83% of chunks for 76-char PEM lines). |
28 | | * |
29 | | * EVP_DecodeBlock passes swallow_ws 0, which disables loops 2 and 3 so |
30 | | * that only pure base64 is ever consumed. |
31 | | * |
32 | | * Whitespace detection, compression routines, and lookup tables are derived |
33 | | * from simdutf (https://github.com/simdutf/simdutf), dual-licensed under |
34 | | * Apache-2.0 and MIT, based on: D. Lemire & W. Mula, "Base64 encoding and |
35 | | * decoding at almost the speed of a memory copy", Software: Practice and |
36 | | * Experience 50 (2), 2020. |
37 | | */ |
38 | | |
39 | | #include <limits.h> |
40 | | #include <openssl/evp.h> |
41 | | #include "dec_b64_avx2.h" |
42 | | #include "b64_avx2_common.h" |
43 | | |
44 | | #ifdef HAVE_AVX2 |
45 | | #include "dec_b64_avx2_tables.h" |
46 | | |
47 | | /* |
48 | | * Lanes set to all-ones for bytes that are not standard base64 characters. |
49 | | * A byte is valid when its hi-nibble and lo-nibble class bits intersect. |
50 | | */ |
51 | | OPENSSL_TARGET_AVX2 |
52 | | static ossl_inline __m256i invalid_std(__m256i input) |
53 | 0 | { |
54 | 0 | const __m256i mask_0f = _mm256_set1_epi8(0x0F); |
55 | 0 | const __m256i lo_nib = _mm256_and_si256(input, mask_0f); |
56 | 0 | const __m256i hi_nib = _mm256_and_si256(_mm256_srli_epi32(input, 4), |
57 | 0 | mask_0f); |
58 | |
|
59 | 0 | const __m256i hi_lut = _mm256_setr_epi8( |
60 | 0 | 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08, |
61 | 0 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
62 | 0 | 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08, |
63 | 0 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); |
64 | | |
65 | | /* |
66 | | * lo_lut[n] = union of hi-group bits for valid chars with lo nibble n. |
67 | | * 0:0x0A 1-9:0x0E A:0x0C B:0x05 C-E:0x04 F:0x05 |
68 | | */ |
69 | 0 | const __m256i lo_lut = _mm256_setr_epi8( |
70 | 0 | 0x0A, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, |
71 | 0 | 0x0E, 0x0E, 0x0C, 0x05, 0x04, 0x04, 0x04, 0x05, |
72 | 0 | 0x0A, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, |
73 | 0 | 0x0E, 0x0E, 0x0C, 0x05, 0x04, 0x04, 0x04, 0x05); |
74 | |
|
75 | 0 | const __m256i check = _mm256_and_si256(_mm256_shuffle_epi8(hi_lut, hi_nib), |
76 | 0 | _mm256_shuffle_epi8(lo_lut, lo_nib)); |
77 | |
|
78 | 0 | return _mm256_cmpeq_epi8(check, _mm256_setzero_si256()); |
79 | 0 | } |
80 | | OPENSSL_UNTARGET_AVX2 |
81 | | |
82 | | /* |
83 | | * Decode one 32-byte vector of base64 ASCII to 6-bit values (standard). |
84 | | * Accumulates invalid-byte flags into *err_acc (caller checks once after batch). |
85 | | */ |
86 | | OPENSSL_TARGET_AVX2 |
87 | | static ossl_inline __m256i decode_std(__m256i input, __m256i *err_acc) |
88 | 0 | { |
89 | 0 | const __m256i mask_0f = _mm256_set1_epi8(0x0F); |
90 | 0 | const __m256i hi_nib = _mm256_and_si256(_mm256_srli_epi32(input, 4), |
91 | 0 | mask_0f); |
92 | |
|
93 | 0 | *err_acc = _mm256_or_si256(*err_acc, invalid_std(input)); |
94 | | |
95 | | /* |
96 | | * Translate ASCII to 6-bit value = input + offset. |
97 | | * offset_lut indexed by hi nibble: |
98 | | * hi=2: 16 ('/'=47 -> 47+16=63) |
99 | | * hi=3: 4 ('0'=48 -> 48+4=52) |
100 | | * hi=4,5: -65 ('A'=65 -> 0, 'Z'=90 -> 25) |
101 | | * hi=6,7: -71 ('a'=97 -> 26, 'z'=122 -> 51) |
102 | | * |
103 | | * '+'=43 also has hi=2, but needs offset 19 (43+19=62), not 16. |
104 | | * A separate fixup adds 3 for '+' bytes only (see below). |
105 | | */ |
106 | 0 | const __m256i off_lut = _mm256_setr_epi8( |
107 | 0 | 0, 0, 16, 4, -65, -65, -71, -71, |
108 | 0 | 0, 0, 0, 0, 0, 0, 0, 0, |
109 | 0 | 0, 0, 16, 4, -65, -65, -71, -71, |
110 | 0 | 0, 0, 0, 0, 0, 0, 0, 0); |
111 | |
|
112 | 0 | __m256i offset = _mm256_shuffle_epi8(off_lut, hi_nib); |
113 | | |
114 | | /* |
115 | | * '+' (0x2B) and '/' (0x2F) share hi nibble 2, so off_lut[2] can only |
116 | | * hold one offset. We pick 16 (correct for '/'), then fix up '+' here: |
117 | | * '+' needs 62-43=19, got 16, difference is 3. |
118 | | */ |
119 | 0 | __m256i plus_fix = _mm256_and_si256( |
120 | 0 | _mm256_cmpeq_epi8(input, _mm256_set1_epi8('+')), |
121 | 0 | _mm256_set1_epi8(3)); |
122 | 0 | offset = _mm256_add_epi8(offset, plus_fix); |
123 | |
|
124 | 0 | return _mm256_add_epi8(input, offset); |
125 | 0 | } |
126 | | OPENSSL_UNTARGET_AVX2 |
127 | | |
128 | | /* Lanes set to all-ones for bytes that are not SRP base64 characters. */ |
129 | | OPENSSL_TARGET_AVX2 |
130 | | static ossl_inline __m256i invalid_srp(__m256i input) |
131 | 0 | { |
132 | 0 | const __m256i mask_0f = _mm256_set1_epi8(0x0F); |
133 | 0 | const __m256i lo_nib = _mm256_and_si256(input, mask_0f); |
134 | 0 | const __m256i hi_nib = _mm256_and_si256(_mm256_srli_epi32(input, 4), |
135 | 0 | mask_0f); |
136 | |
|
137 | 0 | const __m256i hi_lut = _mm256_setr_epi8( |
138 | 0 | 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08, |
139 | 0 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
140 | 0 | 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08, |
141 | 0 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); |
142 | | |
143 | | /* SRP lo_lut: differs from standard at indices 0xB (no '+') and 0xE/0xF ('.','/') */ |
144 | 0 | const __m256i lo_lut = _mm256_setr_epi8( |
145 | 0 | 0x0A, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, |
146 | 0 | 0x0E, 0x0E, 0x0C, 0x04, 0x04, 0x04, 0x05, 0x05, |
147 | 0 | 0x0A, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, |
148 | 0 | 0x0E, 0x0E, 0x0C, 0x04, 0x04, 0x04, 0x05, 0x05); |
149 | |
|
150 | 0 | const __m256i check = _mm256_and_si256(_mm256_shuffle_epi8(hi_lut, hi_nib), |
151 | 0 | _mm256_shuffle_epi8(lo_lut, lo_nib)); |
152 | |
|
153 | 0 | return _mm256_cmpeq_epi8(check, _mm256_setzero_si256()); |
154 | 0 | } |
155 | | OPENSSL_UNTARGET_AVX2 |
156 | | |
157 | | /* |
158 | | * SRP alphabet decode (same structure, different LUT values). |
159 | | * 0-9->0-9 A-Z->10-35 a-z->36-61 .->62 /->63 |
160 | | * |
161 | | * No '+' fixup needed: SRP's two hi=2 characters are '.'=46 and '/'=47, |
162 | | * and both map correctly with a single offset of 16 (46+16=62, 47+16=63). |
163 | | */ |
164 | | OPENSSL_TARGET_AVX2 |
165 | | static ossl_inline __m256i decode_srp(__m256i input, __m256i *err_acc) |
166 | 0 | { |
167 | 0 | const __m256i mask_0f = _mm256_set1_epi8(0x0F); |
168 | 0 | const __m256i hi_nib = _mm256_and_si256(_mm256_srli_epi32(input, 4), |
169 | 0 | mask_0f); |
170 | |
|
171 | 0 | *err_acc = _mm256_or_si256(*err_acc, invalid_srp(input)); |
172 | | |
173 | | /* |
174 | | * SRP offsets: |
175 | | * hi=2: 16 ('.'=46->62, '/'=47->63) |
176 | | * hi=3: -48 ('0'=48->0) |
177 | | * hi=4,5: -55 ('A'=65->10) |
178 | | * hi=6,7: -61 ('a'=97->36) |
179 | | */ |
180 | 0 | const __m256i off_lut = _mm256_setr_epi8( |
181 | 0 | 0, 0, 16, -48, -55, -55, -61, -61, |
182 | 0 | 0, 0, 0, 0, 0, 0, 0, 0, |
183 | 0 | 0, 0, 16, -48, -55, -55, -61, -61, |
184 | 0 | 0, 0, 0, 0, 0, 0, 0, 0); |
185 | |
|
186 | 0 | return _mm256_add_epi8(input, _mm256_shuffle_epi8(off_lut, hi_nib)); |
187 | 0 | } |
188 | | OPENSSL_UNTARGET_AVX2 |
189 | | |
190 | | /* use_srp is loop-invariant at every call site, so this folds away. */ |
191 | | OPENSSL_TARGET_AVX2 |
192 | | static ossl_inline __m256i invalid_b64(__m256i input, int use_srp) |
193 | 0 | { |
194 | 0 | return use_srp ? invalid_srp(input) : invalid_std(input); |
195 | 0 | } |
196 | | OPENSSL_UNTARGET_AVX2 |
197 | | |
198 | | /* |
199 | | * Pack 6-bit values to bytes. Returns a 256-bit register with 12 valid |
200 | | * bytes per lane at [0..11] and 4 garbage bytes at [12..15]. |
201 | | */ |
202 | | OPENSSL_TARGET_AVX2 |
203 | | static ossl_inline __m256i pack(__m256i in) |
204 | 0 | { |
205 | | /* Merge pairs: [00aaaaaa|00bbbbbb] -> [0000aaaa|aabbbbbb] */ |
206 | 0 | __m256i merged = _mm256_maddubs_epi16(in, _mm256_set1_epi32(0x01400140)); |
207 | | |
208 | | /* Merge quads: two 16-bit halves -> one 32-bit [00000000|aaaaaabb|bbbbcccc|ccdddddd] */ |
209 | 0 | __m256i packed = _mm256_madd_epi16(merged, _mm256_set1_epi32(0x00011000)); |
210 | | |
211 | | /* Byte-shuffle within each lane: extract 3 of 4 bytes per dword -> 12 valid bytes per lane */ |
212 | 0 | const __m256i shuf = _mm256_setr_epi8( |
213 | 0 | 2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -1, -1, -1, -1, |
214 | 0 | 2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -1, -1, -1, -1); |
215 | |
|
216 | 0 | return _mm256_shuffle_epi8(packed, shuf); |
217 | 0 | } |
218 | | OPENSSL_UNTARGET_AVX2 |
219 | | |
220 | | /* |
221 | | * Store 48 bytes via 4 overlapping 128-bit stores. The last one is |
222 | | * end-aligned, so nothing is written past out[47]. |
223 | | */ |
224 | | OPENSSL_TARGET_AVX2 |
225 | | static ossl_inline void store_48(unsigned char *out, __m256i p0, __m256i p1) |
226 | 0 | { |
227 | | /* dwords 2,4,5,6 of p1 = decoded bytes [32..47] */ |
228 | 0 | const __m256i last16 = _mm256_setr_epi32(2, 4, 5, 6, 2, 4, 5, 6); |
229 | |
|
230 | 0 | _mm_storeu_si128((__m128i *)out, _mm256_castsi256_si128(p0)); |
231 | 0 | _mm_storeu_si128((__m128i *)(out + 12), _mm256_extracti128_si256(p0, 1)); |
232 | 0 | _mm_storeu_si128((__m128i *)(out + 24), _mm256_castsi256_si128(p1)); |
233 | 0 | _mm_storeu_si128((__m128i *)(out + 32), |
234 | 0 | _mm256_castsi256_si128(_mm256_permutevar8x32_epi32(p1, last16))); |
235 | 0 | } |
236 | | OPENSSL_UNTARGET_AVX2 |
237 | | |
238 | | /* Portable popcount for 32-bit values using BitsSetTable256mul2 */ |
239 | | static ossl_inline int popcount32(uint32_t x) |
240 | 0 | { |
241 | 0 | return (BitsSetTable256mul2[x & 0xFF] + BitsSetTable256mul2[(x >> 8) & 0xFF] + BitsSetTable256mul2[(x >> 16) & 0xFF] + BitsSetTable256mul2[x >> 24]) >> 1; |
242 | 0 | } |
243 | | |
244 | | /* |
245 | | * Compress 16 bytes: remove bytes whose corresponding mask bits are set. |
246 | | * Always stores 16 bytes at output (trailing bytes may be garbage). |
247 | | */ |
248 | | OPENSSL_TARGET_AVX2 |
249 | | static ossl_inline void compress128(__m128i data, uint16_t mask, |
250 | | unsigned char *output) |
251 | 0 | { |
252 | 0 | uint8_t mask1, mask2; |
253 | 0 | __m128i shufmask, pruned, compactmask; |
254 | 0 | int pop1; |
255 | |
|
256 | 0 | if (mask == 0) { |
257 | 0 | _mm_storeu_si128((__m128i *)output, data); |
258 | 0 | return; |
259 | 0 | } |
260 | 0 | mask1 = (uint8_t)mask; |
261 | 0 | mask2 = (uint8_t)(mask >> 8); |
262 | 0 | shufmask = _mm_set_epi64x( |
263 | 0 | (long long)thintable_epi8[mask2], |
264 | 0 | (long long)thintable_epi8[mask1]); |
265 | 0 | shufmask = _mm_add_epi8(shufmask, |
266 | 0 | _mm_set_epi32(0x08080808, 0x08080808, 0, 0)); |
267 | 0 | pruned = _mm_shuffle_epi8(data, shufmask); |
268 | 0 | pop1 = BitsSetTable256mul2[mask1]; |
269 | 0 | compactmask = _mm_loadu_si128( |
270 | 0 | (const __m128i *)(pshufb_combine_table + pop1 * 8)); |
271 | 0 | _mm_storeu_si128((__m128i *)output, _mm_shuffle_epi8(pruned, compactmask)); |
272 | 0 | } |
273 | | OPENSSL_UNTARGET_AVX2 |
274 | | |
275 | | /* |
276 | | * Compress 32 bytes (one __m256i), removing bytes whose mask bits are set. |
277 | | */ |
278 | | OPENSSL_TARGET_AVX2 |
279 | | static ossl_inline void compress256(__m256i data, uint32_t mask, |
280 | | unsigned char *output) |
281 | 0 | { |
282 | 0 | if (mask == 0) { |
283 | 0 | _mm256_storeu_si256((__m256i *)output, data); |
284 | 0 | return; |
285 | 0 | } |
286 | 0 | compress128(_mm256_castsi256_si128(data), (uint16_t)mask, output); |
287 | 0 | compress128(_mm256_extracti128_si256(data, 1), (uint16_t)(mask >> 16), |
288 | 0 | output + 16 - popcount32(mask & 0xFFFF)); |
289 | 0 | } |
290 | | OPENSSL_UNTARGET_AVX2 |
291 | | |
292 | | /* |
293 | | * Remove exactly one whitespace byte from a 64-byte block (v0 || v1). |
294 | | * mask must have exactly one bit set. Writes 63 valid bytes at output |
295 | | * (with up to 1 byte of trailing garbage from overlapping stores). |
296 | | */ |
297 | | OPENSSL_TARGET_AVX2 |
298 | | static ossl_inline void compress_block_single(__m256i v0, __m256i v1, uint64_t mask, |
299 | | unsigned char *output) |
300 | 0 | { |
301 | 0 | int pos64, pos, lane; |
302 | 0 | __m128i threshold, iota, gt, sh; |
303 | |
|
304 | 0 | #ifdef __GNUC__ |
305 | 0 | pos64 = __builtin_ctzll(mask); |
306 | | #else |
307 | | pos64 = 0; |
308 | | { |
309 | | uint64_t tmp = mask; |
310 | | |
311 | | while ((tmp & 1) == 0) { |
312 | | pos64++; |
313 | | tmp >>= 1; |
314 | | } |
315 | | } |
316 | | #endif |
317 | 0 | pos = pos64 & 0xf; |
318 | 0 | lane = pos64 >> 4; |
319 | |
|
320 | 0 | threshold = _mm_set1_epi8((char)(pos - 1)); |
321 | 0 | iota = _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); |
322 | 0 | gt = _mm_cmpgt_epi8(iota, threshold); |
323 | 0 | sh = _mm_sub_epi8(iota, gt); |
324 | | |
325 | | /* |
326 | | * Input: v0 = [L0(16) | L1(16)], v1 = [L2(16) | L3(16)]. |
327 | | * Output: 63 contiguous bytes. sh(Lk) = shuffle that removes |
328 | | * the WS byte, yielding 15 valid bytes + 1 garbage byte. |
329 | | * The next store overlaps by 1 to cover the garbage. |
330 | | */ |
331 | 0 | switch (lane) { |
332 | 0 | case 0: { |
333 | 0 | __m128i lane0 = _mm256_castsi256_si128(v0); |
334 | 0 | __m128i lane1 = _mm256_extracti128_si256(v0, 1); |
335 | 0 | _mm_storeu_si128((__m128i *)output, |
336 | 0 | _mm_shuffle_epi8(lane0, sh)); |
337 | 0 | _mm_storeu_si128((__m128i *)(output + 15), lane1); |
338 | 0 | _mm256_storeu_si256((__m256i *)(output + 31), v1); |
339 | 0 | break; |
340 | 0 | } |
341 | 0 | case 1: { |
342 | 0 | __m128i lane0 = _mm256_castsi256_si128(v0); |
343 | 0 | __m128i lane1 = _mm256_extracti128_si256(v0, 1); |
344 | 0 | _mm_storeu_si128((__m128i *)output, lane0); |
345 | 0 | _mm_storeu_si128((__m128i *)(output + 16), |
346 | 0 | _mm_shuffle_epi8(lane1, sh)); |
347 | 0 | _mm256_storeu_si256((__m256i *)(output + 31), v1); |
348 | 0 | break; |
349 | 0 | } |
350 | 0 | case 2: { |
351 | 0 | __m128i lane2 = _mm256_castsi256_si128(v1); |
352 | 0 | __m128i lane3 = _mm256_extracti128_si256(v1, 1); |
353 | 0 | _mm256_storeu_si256((__m256i *)output, v0); |
354 | 0 | _mm_storeu_si128((__m128i *)(output + 32), |
355 | 0 | _mm_shuffle_epi8(lane2, sh)); |
356 | 0 | _mm_storeu_si128((__m128i *)(output + 47), lane3); |
357 | 0 | break; |
358 | 0 | } |
359 | 0 | case 3: { |
360 | 0 | __m128i lane2 = _mm256_castsi256_si128(v1); |
361 | 0 | __m128i lane3 = _mm256_extracti128_si256(v1, 1); |
362 | 0 | _mm256_storeu_si256((__m256i *)output, v0); |
363 | 0 | _mm_storeu_si128((__m128i *)(output + 32), lane2); |
364 | 0 | _mm_storeu_si128((__m128i *)(output + 48), |
365 | 0 | _mm_shuffle_epi8(lane3, sh)); |
366 | 0 | break; |
367 | 0 | } |
368 | 0 | } |
369 | 0 | } |
370 | | OPENSSL_UNTARGET_AVX2 |
371 | | |
372 | | /* |
373 | | * Decode 64 buffer bytes into 48 output bytes. The general loop only |
374 | | * buffers alphabet characters, so there is nothing to validate here and |
375 | | * the dead error accumulator folds away. |
376 | | */ |
377 | | OPENSSL_TARGET_AVX2 |
378 | | static ossl_inline void decode_buffer_block(const unsigned char *buf, |
379 | | unsigned char *out, |
380 | | int use_srp) |
381 | 0 | { |
382 | 0 | __m256i bv0 = _mm256_loadu_si256((const __m256i *)buf); |
383 | 0 | __m256i bv1 = _mm256_loadu_si256((const __m256i *)(buf + 32)); |
384 | 0 | __m256i err = _mm256_setzero_si256(); |
385 | 0 | __m256i p0, p1; |
386 | |
|
387 | 0 | if (use_srp) { |
388 | 0 | p0 = pack(decode_srp(bv0, &err)); |
389 | 0 | p1 = pack(decode_srp(bv1, &err)); |
390 | 0 | } else { |
391 | 0 | p0 = pack(decode_std(bv0, &err)); |
392 | 0 | p1 = pack(decode_std(bv1, &err)); |
393 | 0 | } |
394 | |
|
395 | 0 | store_48(out, p0, p1); |
396 | 0 | } |
397 | | OPENSSL_UNTARGET_AVX2 |
398 | | |
399 | | /* |
400 | | * AVX2 streaming base64 decoder with whitespace handling. |
401 | | * |
402 | | * Three loops in priority order: |
403 | | * 1. 128-byte fast loop: clean data, no WS/stop detection needed. |
404 | | * decode_std's error accumulator catches everything. 128 bytes |
405 | | * (4 vectors) per iteration with interleaved decode and store. |
406 | | * 2. Skip-WS loop: skips WS scalarly, then decodes 64 clean bytes. |
407 | | * Handles PEM 64-char lines and any remaining clean data after loop 1. |
408 | | * 3. General loop: arbitrary WS patterns. SIMD WS detection + |
409 | | * compression into a stack buffer, decoded on flush. |
410 | | * Three sub-paths: single-WS compress, general compress, copy. |
411 | | * |
412 | | * Returns bytes written to dst and sets *consumed_out. No error |
413 | | * return, undecodable input stays unconsumed for the caller's scalar |
414 | | * loop. Leftovers (< 64) go to ctx->enc_data as scalar would buffer. |
415 | | * With swallow_ws 0 whitespace is never skipped and consumption stops |
416 | | * at the first block holding anything but base64 characters. |
417 | | * |
418 | | * dst may alias src (PEM_read_bio_ex decodes in place). Each loop loads |
419 | | * a chunk before storing, and at most 48 bytes are written per 64 |
420 | | * consumed, so writes stay behind src + consumed and input from |
421 | | * *consumed_out on is intact for the caller's scalar loop. |
422 | | */ |
423 | | OPENSSL_TARGET_AVX2 |
424 | | int decode_base64_avx2(EVP_ENCODE_CTX *ctx, unsigned char *dst, |
425 | | const unsigned char *src, int srclen, int swallow_ws, |
426 | | int *consumed_out) |
427 | 0 | { |
428 | 0 | unsigned char *out = dst; |
429 | 0 | int consumed = 0; |
430 | 0 | const int use_srp = (ctx->flags & EVP_ENCODE_CTX_USE_SRP_ALPHABET) != 0; |
431 | | |
432 | | /* keep consumed + 128 below INT_MAX, the excess stays unconsumed */ |
433 | 0 | if (srclen > INT_MAX - 128) |
434 | 0 | srclen = INT_MAX - 128; |
435 | | |
436 | | /* |
437 | | * Stack buffer for whitespace-compressed data. |
438 | | * 512 bytes allows a read pointer to advance without frequent |
439 | | * memmove. We decode from buf_rpos whenever 64 bytes are |
440 | | * available and only shift when the read pointer gets high. |
441 | | * |
442 | | * The compressors store up to 64 bytes at bufferptr even when they |
443 | | * report fewer. With buf_rpos <= 255 and buf_avail <= 63 on entry, |
444 | | * bufferptr <= buffer+318, so the highest byte touched is +381. |
445 | | */ |
446 | 0 | unsigned char buffer[512]; |
447 | 0 | unsigned char *bufferptr = buffer; |
448 | 0 | int buf_rpos = 0; |
449 | | |
450 | | /* |
451 | | * Tight inner loop for clean data (no WS, no stops). |
452 | | * Processes 128 bytes (4 vectors) per iteration. |
453 | | * |
454 | | * decode_std/decode_srp already flag WS, '=', '-', and all other |
455 | | * non-base64 bytes via the error accumulator, so no separate |
456 | | * WS/stop detection is needed. We just decode and check the |
457 | | * error. Zero overhead for the common clean path. |
458 | | * |
459 | | * We check errors per 64-byte half: if the first half has an |
460 | | * error, we don't store anything and fall through. If only the |
461 | | * second half has an error, we commit the first half's output. |
462 | | */ |
463 | 0 | while (consumed + 128 <= srclen) { |
464 | 0 | __m256i v0, v1, v2, v3; |
465 | 0 | __m256i err01 = _mm256_setzero_si256(); |
466 | 0 | __m256i err23 = _mm256_setzero_si256(); |
467 | 0 | __m256i d0, d1, d2, d3, p0, p1, p2, p3; |
468 | | |
469 | | /* |
470 | | * Interleaved load-decode: give the OoO engine independent |
471 | | * work chains across load and execution ports. |
472 | | */ |
473 | 0 | v0 = _mm256_loadu_si256((const __m256i *)(src + consumed)); |
474 | 0 | v1 = _mm256_loadu_si256((const __m256i *)(src + consumed + 32)); |
475 | 0 | if (use_srp) { |
476 | 0 | d0 = decode_srp(v0, &err01); |
477 | 0 | d1 = decode_srp(v1, &err01); |
478 | 0 | } else { |
479 | 0 | d0 = decode_std(v0, &err01); |
480 | 0 | d1 = decode_std(v1, &err01); |
481 | 0 | } |
482 | 0 | p0 = pack(d0); |
483 | 0 | p1 = pack(d1); |
484 | | |
485 | | /* Check first 64 bytes, bail out on error */ |
486 | 0 | if (_mm256_movemask_epi8(err01) != 0) |
487 | 0 | break; |
488 | | |
489 | 0 | v2 = _mm256_loadu_si256((const __m256i *)(src + consumed + 64)); |
490 | 0 | v3 = _mm256_loadu_si256((const __m256i *)(src + consumed + 96)); |
491 | | |
492 | | /* Store first half while second half decodes */ |
493 | 0 | store_48(out, p0, p1); |
494 | |
|
495 | 0 | if (use_srp) { |
496 | 0 | d2 = decode_srp(v2, &err23); |
497 | 0 | d3 = decode_srp(v3, &err23); |
498 | 0 | } else { |
499 | 0 | d2 = decode_std(v2, &err23); |
500 | 0 | d3 = decode_std(v3, &err23); |
501 | 0 | } |
502 | 0 | p2 = pack(d2); |
503 | 0 | p3 = pack(d3); |
504 | | |
505 | | /* Check second 64 bytes, if error commit first half only */ |
506 | 0 | if (_mm256_movemask_epi8(err23) != 0) { |
507 | 0 | out += 48; |
508 | 0 | consumed += 64; |
509 | 0 | break; |
510 | 0 | } |
511 | | |
512 | 0 | store_48(out + 48, p2, p3); |
513 | 0 | out += 96; |
514 | 0 | consumed += 128; |
515 | 0 | } |
516 | | |
517 | | /* |
518 | | * Skip-WS-then-decode loop for PEM 64-char lines. |
519 | | * Skip WS scalarly, then decode 64 clean bytes directly. |
520 | | * No buffer, no compress. Falls through to the general loop if |
521 | | * WS appears mid-block. |
522 | | */ |
523 | 0 | while (bufferptr == buffer && consumed < srclen) { |
524 | 0 | __m256i v0, v1, err, d0, d1, p0, p1; |
525 | | |
526 | | /* Skip inter-line whitespace (typically 1 LF per PEM line) */ |
527 | 0 | while (swallow_ws && consumed < srclen) { |
528 | 0 | unsigned char c = src[consumed]; |
529 | |
|
530 | 0 | if (c != '\n' && c != '\r' && c != ' ' && c != '\t') |
531 | 0 | break; |
532 | 0 | consumed++; |
533 | 0 | } |
534 | |
|
535 | 0 | if (consumed + 64 > srclen) |
536 | 0 | break; |
537 | | |
538 | 0 | v0 = _mm256_loadu_si256((const __m256i *)(src + consumed)); |
539 | 0 | v1 = _mm256_loadu_si256((const __m256i *)(src + consumed + 32)); |
540 | 0 | err = _mm256_setzero_si256(); |
541 | |
|
542 | 0 | if (use_srp) { |
543 | 0 | d0 = decode_srp(v0, &err); |
544 | 0 | p0 = pack(d0); |
545 | 0 | d1 = decode_srp(v1, &err); |
546 | 0 | p1 = pack(d1); |
547 | 0 | } else { |
548 | 0 | d0 = decode_std(v0, &err); |
549 | 0 | p0 = pack(d0); |
550 | 0 | d1 = decode_std(v1, &err); |
551 | 0 | p1 = pack(d1); |
552 | 0 | } |
553 | | |
554 | | /* Non-base64 byte in block, either WS mid-block or stop char */ |
555 | 0 | if (_mm256_movemask_epi8(err) != 0) |
556 | 0 | break; |
557 | | |
558 | 0 | store_48(out, p0, p1); |
559 | 0 | out += 48; |
560 | 0 | consumed += 64; |
561 | 0 | } |
562 | | |
563 | | /* |
564 | | * General loop: handles whitespace and buffer management. Entered |
565 | | * when WS appears mid-block or after the skip-WS loop encounters |
566 | | * non-standard formatting. |
567 | | */ |
568 | 0 | { |
569 | | /* WS detection via low nibble (each WS char has a unique one) */ |
570 | 0 | const __m256i ws_tbl = _mm256_setr_epi8( |
571 | 0 | 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0x09, 0x0A, 0, 0, 0x0D, 0, 0, |
572 | 0 | 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0x09, 0x0A, 0, 0, 0x0D, 0, 0); |
573 | |
|
574 | 0 | while (swallow_ws && consumed + 64 <= srclen) { |
575 | 0 | __m256i v0 = _mm256_loadu_si256((const __m256i *)(src + consumed)); |
576 | 0 | __m256i v1 = _mm256_loadu_si256((const __m256i *)(src + consumed + 32)); |
577 | 0 | __m256i bad0, bad1; |
578 | | |
579 | | /* Detect whitespace */ |
580 | 0 | __m256i ws0 = _mm256_cmpeq_epi8(_mm256_shuffle_epi8(ws_tbl, v0), v0); |
581 | 0 | __m256i ws1 = _mm256_cmpeq_epi8(_mm256_shuffle_epi8(ws_tbl, v1), v1); |
582 | 0 | uint32_t wsmask0 = (uint32_t)_mm256_movemask_epi8(ws0); |
583 | 0 | uint32_t wsmask1 = (uint32_t)_mm256_movemask_epi8(ws1); |
584 | 0 | uint64_t wsmask = (uint64_t)wsmask0 | ((uint64_t)wsmask1 << 32); |
585 | | |
586 | | /* |
587 | | * Reject the chunk before consuming it, as compression drops |
588 | | * the offsets needed to report an error later and src cannot |
589 | | * be replayed once the output has overwritten it. |
590 | | */ |
591 | 0 | bad0 = _mm256_andnot_si256(ws0, invalid_b64(v0, use_srp)); |
592 | 0 | bad1 = _mm256_andnot_si256(ws1, invalid_b64(v1, use_srp)); |
593 | 0 | if ((_mm256_movemask_epi8(bad0) | _mm256_movemask_epi8(bad1)) != 0) |
594 | 0 | break; |
595 | | |
596 | 0 | if (wsmask != 0) { |
597 | 0 | if ((wsmask & (wsmask - 1)) == 0) { |
598 | | /* Exactly 1 WS byte (PEM hot path) */ |
599 | 0 | compress_block_single(v0, v1, wsmask, bufferptr); |
600 | 0 | bufferptr += 63; |
601 | 0 | } else { |
602 | | /* General compress */ |
603 | 0 | compress256(v0, wsmask0, bufferptr); |
604 | 0 | bufferptr += 32 - popcount32(wsmask0); |
605 | 0 | compress256(v1, wsmask1, bufferptr); |
606 | 0 | bufferptr += 32 - popcount32(wsmask1); |
607 | 0 | } |
608 | 0 | } else { |
609 | | /* No WS, but buffer has pending data from earlier WS blocks. |
610 | | * Must go through the buffer to keep output in input order. */ |
611 | 0 | _mm256_storeu_si256((__m256i *)bufferptr, v0); |
612 | 0 | _mm256_storeu_si256((__m256i *)(bufferptr + 32), v1); |
613 | 0 | bufferptr += 64; |
614 | 0 | } |
615 | |
|
616 | 0 | consumed += 64; |
617 | | |
618 | | /* |
619 | | * Flush: decode every complete 64-byte block immediately. |
620 | | * This interleaves compress and decode, keeping buffer data |
621 | | * hot in L1 rather than accumulating a large cold batch. |
622 | | */ |
623 | 0 | { |
624 | 0 | int buf_avail = (int)(bufferptr - buffer) - buf_rpos; |
625 | |
|
626 | 0 | while (buf_avail >= 64) { |
627 | 0 | decode_buffer_block(buffer + buf_rpos, out, use_srp); |
628 | 0 | out += 48; |
629 | 0 | buf_rpos += 64; |
630 | 0 | buf_avail -= 64; |
631 | 0 | } |
632 | | /* Shift down when read pointer gets high */ |
633 | 0 | if (buf_rpos >= 256) { |
634 | 0 | memmove(buffer, buffer + buf_rpos, buf_avail); |
635 | 0 | bufferptr = buffer + buf_avail; |
636 | 0 | buf_rpos = 0; |
637 | 0 | } |
638 | 0 | } |
639 | 0 | } |
640 | 0 | } /* end general loop scope */ |
641 | | |
642 | | /* Handle leftover < 64 bytes remaining in the compress buffer */ |
643 | 0 | { |
644 | 0 | int buf_avail = (int)(bufferptr - buffer) - buf_rpos; |
645 | |
|
646 | 0 | if (buf_avail > 0) { |
647 | | /* stash whole since decoding any of it here would diverge |
648 | | * from scalar per-call *outl and ctx->num */ |
649 | 0 | memcpy(ctx->enc_data, buffer + buf_rpos, buf_avail); |
650 | 0 | ctx->num = buf_avail; |
651 | 0 | } else { |
652 | 0 | ctx->num = 0; |
653 | 0 | } |
654 | 0 | } |
655 | |
|
656 | 0 | *consumed_out = consumed; |
657 | 0 | return (int)(out - dst); |
658 | 0 | } |
659 | | OPENSSL_UNTARGET_AVX2 |
660 | | #endif /* HAVE_AVX2 */ |