Coverage Report

Created: 2026-10-16 07:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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 */