simdutf 9.3.3
Unicode at GB/s.
Loading...
Searching...
No Matches
base64.h
1#ifndef SIMDUTF_BASE64_H
2#define SIMDUTF_BASE64_H
3
4#include <cstddef>
5#include <cstdint>
6#include <array>
7#include <cstring>
8#include <type_traits>
9
10namespace simdutf {
11namespace scalar {
12namespace {
13namespace base64 {
14
15simdutf_really_inline simdutf_constexpr23 void
16copy_encode_pair(char *dst, const std::array<char, 2> &pair) {
17#if SIMDUTF_CPLUSPLUS23
18 if (std::is_constant_evaluated()) {
19 dst[0] = pair[0];
20 dst[1] = pair[1];
21 return;
22 }
23#endif
24 std::memcpy(dst, pair.data(), pair.size());
25}
26
27// This function is not expected to be fast. Do not use in long loops.
28// In most instances you should be using is_ignorable.
29template <class char_type> bool is_ascii_white_space(char_type c) {
30 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
31}
32
33template <class char_type> simdutf_constexpr23 bool is_eight_byte(char_type c) {
34 if constexpr (sizeof(char_type) == 1) {
35 return true;
36 }
37 return uint8_t(c) == c;
38}
39
40template <class char_type>
41simdutf_constexpr23 bool is_ignorable(char_type c,
42 simdutf::base64_options options) {
43 const uint8_t *to_base64 =
44 (options & base64_default_or_url)
45 ? tables::base64::to_base64_default_or_url_value
46 : ((options & base64_url) ? tables::base64::to_base64_url_value
47 : tables::base64::to_base64_value);
48 const bool ignore_garbage =
49 (options == base64_options::base64_url_accept_garbage) ||
50 (options == base64_options::base64_default_accept_garbage) ||
51 (options == base64_options::base64_default_or_url_accept_garbage);
52 uint8_t code = to_base64[uint8_t(c)];
53 if (is_eight_byte(c) && code <= 63) {
54 return false;
55 }
56 if (is_eight_byte(c) && code == 64) {
57 return true;
58 }
59 return ignore_garbage;
60}
61template <class char_type>
62simdutf_constexpr23 bool is_base64(char_type c,
63 simdutf::base64_options options) {
64 const uint8_t *to_base64 =
65 (options & base64_default_or_url)
66 ? tables::base64::to_base64_default_or_url_value
67 : ((options & base64_url) ? tables::base64::to_base64_url_value
68 : tables::base64::to_base64_value);
69 uint8_t code = to_base64[uint8_t(c)];
70 if (is_eight_byte(c) && code <= 63) {
71 return true;
72 }
73 return false;
74}
75
76template <class char_type>
77simdutf_constexpr23 bool is_base64_or_padding(char_type c,
78 simdutf::base64_options options) {
79 const uint8_t *to_base64 =
80 (options & base64_default_or_url)
81 ? tables::base64::to_base64_default_or_url_value
82 : ((options & base64_url) ? tables::base64::to_base64_url_value
83 : tables::base64::to_base64_value);
84 if (c == '=') {
85 return true;
86 }
87 uint8_t code = to_base64[uint8_t(c)];
88 if (is_eight_byte(c) && code <= 63) {
89 return true;
90 }
91 return false;
92}
93
94template <class char_type>
95bool is_ignorable_or_padding(char_type c, simdutf::base64_options options) {
96 return is_ignorable(c, options) || c == '=';
97}
98
99struct reduced_input {
100 size_t equalsigns; // number of padding characters '=', typically 0, 1, 2.
101 size_t equallocation; // location of the first padding character if any
102 size_t srclen; // length of the input buffer before padding
103 size_t full_input_length; // length of the input buffer with padding but
104 // without ignorable characters
105};
106
107// find the end of the base64 input buffer
108// It returns the number of padding characters, the location of the first
109// padding character if any, the length of the input buffer before padding
110// and the length of the input buffer with padding. The input buffer is not
111// modified. The function assumes that there are at most two padding characters.
112template <class char_type>
113simdutf_constexpr23 reduced_input find_end(const char_type *src, size_t srclen,
114 simdutf::base64_options options) {
115 const uint8_t *to_base64 =
116 (options & base64_default_or_url)
117 ? tables::base64::to_base64_default_or_url_value
118 : ((options & base64_url) ? tables::base64::to_base64_url_value
119 : tables::base64::to_base64_value);
120 const bool ignore_garbage =
121 (options == base64_options::base64_url_accept_garbage) ||
122 (options == base64_options::base64_default_accept_garbage) ||
123 (options == base64_options::base64_default_or_url_accept_garbage);
124
125 size_t equalsigns = 0;
126 // We intentionally include trailing spaces in the full input length.
127 // See https://github.com/simdutf/simdutf/issues/824
128 size_t full_input_length = srclen;
129 // skip trailing spaces
130 while (!ignore_garbage && srclen > 0 &&
131 scalar::base64::is_eight_byte(src[srclen - 1]) &&
132 to_base64[uint8_t(src[srclen - 1])] == 64) {
133 srclen--;
134 }
135 size_t equallocation =
136 srclen; // location of the first padding character if any
137 if (ignore_garbage) {
138 // Technically, we don't need to find the first padding character, we can
139 // just change our algorithms, but it adds substantial complexity.
140 auto it = simdutf::find(src, src + srclen, '=');
141 if (it != src + srclen) {
142 equallocation = it - src;
143 equalsigns = 1;
144 srclen = equallocation;
145 full_input_length = equallocation + 1;
146 }
147 return {equalsigns, equallocation, srclen, full_input_length};
148 }
149 if (!ignore_garbage && srclen > 0 && src[srclen - 1] == '=') {
150 // This is the last '=' sign.
151 equallocation = srclen - 1;
152 srclen--;
153 equalsigns = 1;
154 // skip trailing spaces
155 while (srclen > 0 && scalar::base64::is_eight_byte(src[srclen - 1]) &&
156 to_base64[uint8_t(src[srclen - 1])] == 64) {
157 srclen--;
158 }
159 if (srclen > 0 && src[srclen - 1] == '=') {
160 // This is the second '=' sign.
161 equallocation = srclen - 1;
162 srclen--;
163 equalsigns = 2;
164 }
165 }
166 return {equalsigns, equallocation, srclen, full_input_length};
167}
168
169// Returns true upon success. The destination buffer must be large enough.
170// This functions assumes that the padding (=) has been removed.
171// if check_capacity is true, it will check that the destination buffer is
172// large enough. If it is not, it will return OUTPUT_BUFFER_TOO_SMALL.
173template <bool check_capacity, class char_type>
174simdutf_constexpr23 full_result base64_tail_decode_impl(
175 char *dst, size_t outlen, const char_type *src, size_t length,
176 size_t padding_characters, // number of padding characters
177 // '=', typically 0, 1, 2.
178 base64_options options, last_chunk_handling_options last_chunk_options) {
179 char *dstend = dst + outlen;
180 (void)dstend;
181 // This looks like 10 branches, but we expect the compiler to resolve this to
182 // two branches (easily predicted):
183 const uint8_t *to_base64 =
184 (options & base64_default_or_url)
185 ? tables::base64::to_base64_default_or_url_value
186 : ((options & base64_url) ? tables::base64::to_base64_url_value
187 : tables::base64::to_base64_value);
188 const uint32_t *d0 =
189 (options & base64_default_or_url)
190 ? tables::base64::base64_default_or_url::d0
191 : ((options & base64_url) ? tables::base64::base64_url::d0
192 : tables::base64::base64_default::d0);
193 const uint32_t *d1 =
194 (options & base64_default_or_url)
195 ? tables::base64::base64_default_or_url::d1
196 : ((options & base64_url) ? tables::base64::base64_url::d1
197 : tables::base64::base64_default::d1);
198 const uint32_t *d2 =
199 (options & base64_default_or_url)
200 ? tables::base64::base64_default_or_url::d2
201 : ((options & base64_url) ? tables::base64::base64_url::d2
202 : tables::base64::base64_default::d2);
203 const uint32_t *d3 =
204 (options & base64_default_or_url)
205 ? tables::base64::base64_default_or_url::d3
206 : ((options & base64_url) ? tables::base64::base64_url::d3
207 : tables::base64::base64_default::d3);
208 const bool ignore_garbage =
209 (options == base64_options::base64_url_accept_garbage) ||
210 (options == base64_options::base64_default_accept_garbage) ||
211 (options == base64_options::base64_default_or_url_accept_garbage);
212
213 const char_type *srcend = src + length;
214 const char_type *srcinit = src;
215 const char *dstinit = dst;
216
217 uint32_t x;
218 size_t idx;
219 uint8_t buffer[4];
220 while (true) {
221 while (srcend - src >= 12 && (!check_capacity || dstend - dst >= 9) &&
222 is_eight_byte(src[0]) && is_eight_byte(src[1]) &&
223 is_eight_byte(src[2]) && is_eight_byte(src[3]) &&
224 is_eight_byte(src[4]) && is_eight_byte(src[5]) &&
225 is_eight_byte(src[6]) && is_eight_byte(src[7]) &&
226 is_eight_byte(src[8]) && is_eight_byte(src[9]) &&
227 is_eight_byte(src[10]) && is_eight_byte(src[11])) {
228 const uint32_t x0 = d0[uint8_t(src[0])] | d1[uint8_t(src[1])] |
229 d2[uint8_t(src[2])] | d3[uint8_t(src[3])];
230 const uint32_t x1 = d0[uint8_t(src[4])] | d1[uint8_t(src[5])] |
231 d2[uint8_t(src[6])] | d3[uint8_t(src[7])];
232 const uint32_t x2 = d0[uint8_t(src[8])] | d1[uint8_t(src[9])] |
233 d2[uint8_t(src[10])] | d3[uint8_t(src[11])];
234 if ((x0 | x1 | x2) >= 0x01FFFFFF) {
235 break;
236 }
237#if SIMDUTF_IS_BIG_ENDIAN
238 dst[0] = static_cast<char>(x0);
239 dst[1] = static_cast<char>(x0 >> 8);
240 dst[2] = static_cast<char>(x0 >> 16);
241 dst[3] = static_cast<char>(x1);
242 dst[4] = static_cast<char>(x1 >> 8);
243 dst[5] = static_cast<char>(x1 >> 16);
244 dst[6] = static_cast<char>(x2);
245 dst[7] = static_cast<char>(x2 >> 8);
246 dst[8] = static_cast<char>(x2 >> 16);
247#else
248 const uint64_t word =
249 uint64_t(x0) | (uint64_t(x1) << 24) | (uint64_t(x2) << 48);
250 #if SIMDUTF_CPLUSPLUS23
251 if (std::is_constant_evaluated()) {
252 dst[0] = static_cast<char>(x0);
253 dst[1] = static_cast<char>(x0 >> 8);
254 dst[2] = static_cast<char>(x0 >> 16);
255 dst[3] = static_cast<char>(x1);
256 dst[4] = static_cast<char>(x1 >> 8);
257 dst[5] = static_cast<char>(x1 >> 16);
258 dst[6] = static_cast<char>(x2);
259 dst[7] = static_cast<char>(x2 >> 8);
260 } else
261 #endif
262 {
263 std::memcpy(dst, &word, sizeof(word));
264 }
265 dst[8] = static_cast<char>(x2 >> 16);
266#endif
267 src += 12;
268 dst += 9;
269 }
270 while (srcend - src >= 4 && is_eight_byte(src[0]) &&
271 is_eight_byte(src[1]) && is_eight_byte(src[2]) &&
272 is_eight_byte(src[3]) &&
273 (x = d0[uint8_t(src[0])] | d1[uint8_t(src[1])] |
274 d2[uint8_t(src[2])] | d3[uint8_t(src[3])]) < 0x01FFFFFF) {
275 if (check_capacity && dstend - dst < 3) {
276 return {OUTPUT_BUFFER_TOO_SMALL, size_t(src - srcinit),
277 size_t(dst - dstinit)};
278 }
279 *dst++ = static_cast<char>(x & 0xFF);
280 *dst++ = static_cast<char>((x >> 8) & 0xFF);
281 *dst++ = static_cast<char>((x >> 16) & 0xFF);
282 src += 4;
283 }
284 const char_type *srccur = src;
285 idx = 0;
286 // we need at least four characters.
287#ifdef __clang__
288 // If possible, we read four characters at a time. (It is an optimization.)
289 if (ignore_garbage && src + 4 <= srcend) {
290 char_type c0 = src[0];
291 char_type c1 = src[1];
292 char_type c2 = src[2];
293 char_type c3 = src[3];
294
295 uint8_t code0 = to_base64[uint8_t(c0)];
296 uint8_t code1 = to_base64[uint8_t(c1)];
297 uint8_t code2 = to_base64[uint8_t(c2)];
298 uint8_t code3 = to_base64[uint8_t(c3)];
299
300 buffer[idx] = code0;
301 idx += (is_eight_byte(c0) && code0 <= 63);
302 buffer[idx] = code1;
303 idx += (is_eight_byte(c1) && code1 <= 63);
304 buffer[idx] = code2;
305 idx += (is_eight_byte(c2) && code2 <= 63);
306 buffer[idx] = code3;
307 idx += (is_eight_byte(c3) && code3 <= 63);
308 src += 4;
309 }
310#endif
311 while ((idx < 4) && (src < srcend)) {
312 char_type c = *src;
313
314 uint8_t code = to_base64[uint8_t(c)];
315 buffer[idx] = uint8_t(code);
316 if (is_eight_byte(c) && code <= 63) {
317 idx++;
318 } else if (!ignore_garbage &&
319 (code > 64 || !scalar::base64::is_eight_byte(c))) {
320 return {INVALID_BASE64_CHARACTER, size_t(src - srcinit),
321 size_t(dst - dstinit)};
322 } else {
323 // We have a space or a newline or garbage. We ignore it.
324 }
325 src++;
326 }
327 if (idx != 4) {
328 simdutf_log_assert(idx < 4, "idx should be less than 4");
329 // We never should have that the number of base64 characters + the
330 // number of padding characters is more than 4.
331 if (!ignore_garbage && (idx + padding_characters > 4)) {
332 return {INVALID_BASE64_CHARACTER, size_t(src - srcinit),
333 size_t(dst - dstinit), true};
334 }
335
336 // The idea here is that in loose mode,
337 // if there is padding at all, it must be used
338 // to form 4-wise chunk. However, in loose mode,
339 // we do accept no padding at all.
340 if (!ignore_garbage &&
341 last_chunk_options == last_chunk_handling_options::loose &&
342 (idx >= 2) && padding_characters > 0 &&
343 ((idx + padding_characters) & 3) != 0) {
344 return {INVALID_BASE64_CHARACTER, size_t(src - srcinit),
345 size_t(dst - dstinit), true};
346 } else
347
348 // The idea here is that in strict mode, we do not want to accept
349 // incomplete base64 chunks. So if the chunk was otherwise valid, we
350 // return BASE64_INPUT_REMAINDER.
351 if (!ignore_garbage &&
352 last_chunk_options == last_chunk_handling_options::strict &&
353 (idx >= 2) && ((idx + padding_characters) & 3) != 0) {
354 // The partial chunk was at src - idx
355 return {BASE64_INPUT_REMAINDER, size_t(src - srcinit),
356 size_t(dst - dstinit), true};
357 } else
358 // If there is a partial chunk with insufficient padding, with
359 // stop_before_partial, we need to just ignore it. In "only full"
360 // mode, skip the minute there are padding characters.
361 if ((last_chunk_options ==
362 last_chunk_handling_options::stop_before_partial &&
363 (padding_characters + idx < 4) && (idx != 0) &&
364 (idx >= 2 || padding_characters == 0)) ||
365 (last_chunk_options ==
366 last_chunk_handling_options::only_full_chunks &&
367 (idx >= 2 || padding_characters == 0))) {
368 // partial means that we are *not* going to consume the read
369 // characters. We need to rewind the src pointer.
370 src = srccur;
371 return {SUCCESS, size_t(src - srcinit), size_t(dst - dstinit)};
372 } else {
373 if (idx == 2) {
374 uint32_t triple = (uint32_t(buffer[0]) << 3 * 6) +
375 (uint32_t(buffer[1]) << 2 * 6);
376 if (!ignore_garbage &&
377 (last_chunk_options == last_chunk_handling_options::strict) &&
378 (triple & 0xffff)) {
379 return {BASE64_EXTRA_BITS, size_t(src - srcinit),
380 size_t(dst - dstinit)};
381 }
382 if (check_capacity && dstend - dst < 1) {
383 return {OUTPUT_BUFFER_TOO_SMALL, size_t(srccur - srcinit),
384 size_t(dst - dstinit)};
385 }
386 *dst++ = static_cast<char>((triple >> 16) & 0xFF);
387 } else if (idx == 3) {
388 uint32_t triple = (uint32_t(buffer[0]) << 3 * 6) +
389 (uint32_t(buffer[1]) << 2 * 6) +
390 (uint32_t(buffer[2]) << 1 * 6);
391 if (!ignore_garbage &&
392 (last_chunk_options == last_chunk_handling_options::strict) &&
393 (triple & 0xff)) {
394 return {BASE64_EXTRA_BITS, size_t(src - srcinit),
395 size_t(dst - dstinit)};
396 }
397 if (check_capacity && dstend - dst < 2) {
398 return {OUTPUT_BUFFER_TOO_SMALL, size_t(srccur - srcinit),
399 size_t(dst - dstinit)};
400 }
401 *dst++ = static_cast<char>((triple >> 16) & 0xFF);
402 *dst++ = static_cast<char>((triple >> 8) & 0xFF);
403 } else if (!ignore_garbage && idx == 1 &&
404 (!is_partial(last_chunk_options) ||
405 (is_partial(last_chunk_options) &&
406 padding_characters > 0))) {
407 return {BASE64_INPUT_REMAINDER, size_t(src - srcinit),
408 size_t(dst - dstinit)};
409 } else if (!ignore_garbage && idx == 0 && padding_characters > 0) {
410 return {INVALID_BASE64_CHARACTER, size_t(src - srcinit),
411 size_t(dst - dstinit), true};
412 }
413 return {SUCCESS, size_t(src - srcinit), size_t(dst - dstinit)};
414 }
415 }
416 if (check_capacity && dstend - dst < 3) {
417 return {OUTPUT_BUFFER_TOO_SMALL, size_t(srccur - srcinit),
418 size_t(dst - dstinit)};
419 }
420 uint32_t triple =
421 (uint32_t(buffer[0]) << 3 * 6) + (uint32_t(buffer[1]) << 2 * 6) +
422 (uint32_t(buffer[2]) << 1 * 6) + (uint32_t(buffer[3]) << 0 * 6);
423 *dst++ = static_cast<char>((triple >> 16) & 0xFF);
424 *dst++ = static_cast<char>((triple >> 8) & 0xFF);
425 *dst++ = static_cast<char>(triple & 0xFF);
426 }
427}
428
429template <class char_type>
430simdutf_constexpr23 full_result base64_tail_decode(
431 char *dst, const char_type *src, size_t length,
432 size_t padding_characters, // number of padding characters
433 // '=', typically 0, 1, 2.
434 base64_options options, last_chunk_handling_options last_chunk_options) {
435 return base64_tail_decode_impl<false>(dst, 0, src, length, padding_characters,
436 options, last_chunk_options);
437}
438
439// like base64_tail_decode, but it will not write past the end of the output
440// buffer. The outlen parameter is modified to reflect the number of bytes
441// written. This functions assumes that the padding (=) has been removed.
442//
443template <class char_type>
444simdutf_constexpr23 full_result base64_tail_decode_safe(
445 char *dst, size_t outlen, const char_type *src, size_t length,
446 size_t padding_characters, // number of padding characters
447 // '=', typically 0, 1, 2.
448 base64_options options, last_chunk_handling_options last_chunk_options) {
449 return base64_tail_decode_impl<true>(dst, outlen, src, length,
450 padding_characters, options,
451 last_chunk_options);
452}
453
454inline simdutf_constexpr23 full_result
455patch_tail_result(full_result r, size_t previous_input, size_t previous_output,
456 size_t equallocation, size_t full_input_length,
457 last_chunk_handling_options last_chunk_options) {
458 r.input_count += previous_input;
459 r.output_count += previous_output;
460 if (r.padding_error) {
461 r.input_count = equallocation;
462 }
463
464 if (r.error == error_code::SUCCESS) {
465 if (!is_partial(last_chunk_options)) {
466 // A success when we are not in stop_before_partial mode.
467 // means that we have consumed the whole input buffer.
468 r.input_count = full_input_length;
469 } else if (r.output_count % 3 != 0) {
470 r.input_count = full_input_length;
471 }
472 }
473 return r;
474}
475
476simdutf_really_inline simdutf_constexpr23 void
477tail_encode_base64_quantum(char *dst, const char *src,
478 const std::array<char, 2> *pairs) {
479 const uint32_t value = (uint32_t(uint8_t(src[0])) << 16) |
480 (uint32_t(uint8_t(src[1])) << 8) | uint8_t(src[2]);
481 copy_encode_pair(dst, pairs[value >> 12]);
482 copy_encode_pair(dst + 2, pairs[value & 0xFFF]);
483}
484
485simdutf_really_inline simdutf_constexpr23 size_t tail_encode_base64_short_impl(
486 char *dst, const char *src, size_t srclen, base64_options options) {
487 const auto *pairs = (options & base64_url)
488 ? tables::base64::base64_url::encode_pairs.data()
489 : tables::base64::base64_default::encode_pairs.data();
490 const bool use_padding =
491 ((options & base64_url) == 0) ^
492 ((options & base64_reverse_padding) == base64_reverse_padding);
493
494#define SIMDUTF_BASE64_ENCODE_QUANTUM(index) \
495 tail_encode_base64_quantum(dst + (index) * 4, src + (index) * 3, pairs)
496#define SIMDUTF_BASE64_ENCODE_TAIL1(index) \
497 do { \
498 const uint32_t value = uint32_t(uint8_t(src[(index) * 3])) << 16; \
499 copy_encode_pair(dst + (index) * 4, pairs[value >> 12]); \
500 if (use_padding) { \
501 dst[(index) * 4 + 2] = '='; \
502 dst[(index) * 4 + 3] = '='; \
503 } \
504 } while (false)
505#define SIMDUTF_BASE64_ENCODE_TAIL2(index) \
506 do { \
507 const uint32_t value = (uint32_t(uint8_t(src[(index) * 3])) << 16) | \
508 (uint32_t(uint8_t(src[(index) * 3 + 1])) << 8); \
509 copy_encode_pair(dst + (index) * 4, pairs[value >> 12]); \
510 dst[(index) * 4 + 2] = pairs[value & 0xFFF][0]; \
511 if (use_padding) { \
512 dst[(index) * 4 + 3] = '='; \
513 } \
514 } while (false)
515
516 switch (srclen) {
517 case 11:
518 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
519 SIMDUTF_BASE64_ENCODE_QUANTUM(1);
520 SIMDUTF_BASE64_ENCODE_QUANTUM(2);
521 SIMDUTF_BASE64_ENCODE_TAIL2(3);
522 return 15 + size_t(use_padding);
523 case 10:
524 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
525 SIMDUTF_BASE64_ENCODE_QUANTUM(1);
526 SIMDUTF_BASE64_ENCODE_QUANTUM(2);
527 SIMDUTF_BASE64_ENCODE_TAIL1(3);
528 return 14 + 2 * size_t(use_padding);
529 case 9:
530 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
531 SIMDUTF_BASE64_ENCODE_QUANTUM(1);
532 SIMDUTF_BASE64_ENCODE_QUANTUM(2);
533 return 12;
534 case 8:
535 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
536 SIMDUTF_BASE64_ENCODE_QUANTUM(1);
537 SIMDUTF_BASE64_ENCODE_TAIL2(2);
538 return 11 + size_t(use_padding);
539 case 7:
540 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
541 SIMDUTF_BASE64_ENCODE_QUANTUM(1);
542 SIMDUTF_BASE64_ENCODE_TAIL1(2);
543 return 10 + 2 * size_t(use_padding);
544 case 6:
545 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
546 SIMDUTF_BASE64_ENCODE_QUANTUM(1);
547 return 8;
548 case 5:
549 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
550 SIMDUTF_BASE64_ENCODE_TAIL2(1);
551 return 7 + size_t(use_padding);
552 case 4:
553 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
554 SIMDUTF_BASE64_ENCODE_TAIL1(1);
555 return 6 + 2 * size_t(use_padding);
556 case 3:
557 SIMDUTF_BASE64_ENCODE_QUANTUM(0);
558 return 4;
559 case 2:
560 SIMDUTF_BASE64_ENCODE_TAIL2(0);
561 return 3 + size_t(use_padding);
562 case 1:
563 SIMDUTF_BASE64_ENCODE_TAIL1(0);
564 return 2 + 2 * size_t(use_padding);
565 default:
566 return 0;
567 }
568
569#undef SIMDUTF_BASE64_ENCODE_TAIL2
570#undef SIMDUTF_BASE64_ENCODE_TAIL1
571#undef SIMDUTF_BASE64_ENCODE_QUANTUM
572}
573
574simdutf_never_inline simdutf_constexpr23 size_t tail_encode_base64_long_impl(
575 char *dst, const char *src, size_t srclen, base64_options options) {
576 const auto *pairs = (options & base64_url)
577 ? tables::base64::base64_url::encode_pairs.data()
578 : tables::base64::base64_default::encode_pairs.data();
579 char *out = dst;
580 size_t i = 0;
581 for (; i + 11 < srclen; i += 12) {
582 const uint32_t v0 = (uint32_t(uint8_t(src[i])) << 16) |
583 (uint32_t(uint8_t(src[i + 1])) << 8) |
584 uint8_t(src[i + 2]);
585 const uint32_t v1 = (uint32_t(uint8_t(src[i + 3])) << 16) |
586 (uint32_t(uint8_t(src[i + 4])) << 8) |
587 uint8_t(src[i + 5]);
588 const uint32_t v2 = (uint32_t(uint8_t(src[i + 6])) << 16) |
589 (uint32_t(uint8_t(src[i + 7])) << 8) |
590 uint8_t(src[i + 8]);
591 const uint32_t v3 = (uint32_t(uint8_t(src[i + 9])) << 16) |
592 (uint32_t(uint8_t(src[i + 10])) << 8) |
593 uint8_t(src[i + 11]);
594 copy_encode_pair(out, pairs[v0 >> 12]);
595 copy_encode_pair(out + 2, pairs[v0 & 0xFFF]);
596 copy_encode_pair(out + 4, pairs[v1 >> 12]);
597 copy_encode_pair(out + 6, pairs[v1 & 0xFFF]);
598 copy_encode_pair(out + 8, pairs[v2 >> 12]);
599 copy_encode_pair(out + 10, pairs[v2 & 0xFFF]);
600 copy_encode_pair(out + 12, pairs[v3 >> 12]);
601 copy_encode_pair(out + 14, pairs[v3 & 0xFFF]);
602 out += 16;
603 }
604 return size_t(out - dst);
605}
606
607// Returns the number of bytes written. The destination buffer must be large
608// enough. It will add padding (=) if needed.
609template <bool use_lines = false>
610simdutf_constexpr23 size_t tail_encode_base64_impl(
611 char *dst, const char *src, size_t srclen, base64_options options,
612 size_t line_length = simdutf::default_line_length, size_t line_offset = 0) {
613 if constexpr (!use_lines) {
614 if (srclen < 12) {
615 return tail_encode_base64_short_impl(dst, src, srclen, options);
616 }
617 const size_t i = srclen - srclen % 12;
618 const size_t written = tail_encode_base64_long_impl(dst, src, i, options);
619 return written + tail_encode_base64_short_impl(dst + written, src + i,
620 srclen - i, options);
621 } else {
622 // sanitize line_length and starting_line_offset.
623 // line_length must be greater than 3.
624 if (line_length < 4) {
625 line_length = 4;
626 }
627 simdutf_log_assert(line_offset <= line_length,
628 "line_offset should be less than line_length");
629 // By default, we use padding if we are not using the URL variant.
630 // This is check with ((options & base64_url) == 0) which returns true if we
631 // are not using the URL variant. However, we also allow 'inversion' of the
632 // convention with the base64_reverse_padding option. If the
633 // base64_reverse_padding option is set, we use padding if we are using the
634 // URL variant, and we omit it if we are not using the URL variant. This is
635 // checked with
636 // ((options & base64_reverse_padding) == base64_reverse_padding).
637 bool use_padding =
638 ((options & base64_url) == 0) ^
639 ((options & base64_reverse_padding) == base64_reverse_padding);
640 // This looks like 3 branches, but we expect the compiler to resolve this to
641 // a single branch:
642 const char *e0 = (options & base64_url)
643 ? tables::base64::base64_url::e0
644 : tables::base64::base64_default::e0;
645 const char *e1 = (options & base64_url)
646 ? tables::base64::base64_url::e1
647 : tables::base64::base64_default::e1;
648 const char *e2 = (options & base64_url)
649 ? tables::base64::base64_url::e2
650 : tables::base64::base64_default::e2;
651 char *out = dst;
652 size_t i = 0;
653 uint8_t t1, t2, t3;
654 for (; i + 2 < srclen; i += 3) {
655 t1 = uint8_t(src[i]);
656 t2 = uint8_t(src[i + 1]);
657 t3 = uint8_t(src[i + 2]);
658 if (line_offset + 3 >= line_length) {
659 if (line_offset == line_length) {
660 *out++ = '\n';
661 *out++ = e0[t1];
662 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
663 *out++ = e1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)];
664 *out++ = e2[t3];
665 line_offset = 4;
666 } else if (line_offset + 1 == line_length) {
667 *out++ = e0[t1];
668 *out++ = '\n';
669 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
670 *out++ = e1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)];
671 *out++ = e2[t3];
672 line_offset = 3;
673 } else if (line_offset + 2 == line_length) {
674 *out++ = e0[t1];
675 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
676 *out++ = '\n';
677 *out++ = e1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)];
678 *out++ = e2[t3];
679 line_offset = 2;
680 } else if (line_offset + 3 == line_length) {
681 *out++ = e0[t1];
682 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
683 *out++ = e1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)];
684 *out++ = '\n';
685 *out++ = e2[t3];
686 line_offset = 1;
687 }
688 } else {
689 *out++ = e0[t1];
690 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
691 *out++ = e1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)];
692 *out++ = e2[t3];
693 line_offset += 4;
694 }
695 }
696 switch (srclen - i) {
697 case 0:
698 break;
699 case 1:
700 t1 = uint8_t(src[i]);
701 if (use_padding) {
702 if (line_offset + 3 >= line_length) {
703 if (line_offset == line_length) {
704 *out++ = '\n';
705 *out++ = e0[t1];
706 *out++ = e1[(t1 & 0x03) << 4];
707 *out++ = '=';
708 *out++ = '=';
709 } else if (line_offset + 1 == line_length) {
710 *out++ = e0[t1];
711 *out++ = '\n';
712 *out++ = e1[(t1 & 0x03) << 4];
713 *out++ = '=';
714 *out++ = '=';
715 } else if (line_offset + 2 == line_length) {
716 *out++ = e0[t1];
717 *out++ = e1[(t1 & 0x03) << 4];
718 *out++ = '\n';
719 *out++ = '=';
720 *out++ = '=';
721 } else if (line_offset + 3 == line_length) {
722 *out++ = e0[t1];
723 *out++ = e1[(t1 & 0x03) << 4];
724 *out++ = '=';
725 *out++ = '\n';
726 *out++ = '=';
727 }
728 } else {
729 *out++ = e0[t1];
730 *out++ = e1[(t1 & 0x03) << 4];
731 *out++ = '=';
732 *out++ = '=';
733 }
734 } else {
735 if (line_offset + 2 >= line_length) {
736 if (line_offset == line_length) {
737 *out++ = '\n';
738 *out++ = e0[uint8_t(src[i])];
739 *out++ = e1[(uint8_t(src[i]) & 0x03) << 4];
740 } else if (line_offset + 1 == line_length) {
741 *out++ = e0[uint8_t(src[i])];
742 *out++ = '\n';
743 *out++ = e1[(uint8_t(src[i]) & 0x03) << 4];
744 } else {
745 *out++ = e0[uint8_t(src[i])];
746 *out++ = e1[(uint8_t(src[i]) & 0x03) << 4];
747 // *out++ = '\n'; ==> no newline at the end of the output
748 }
749 } else {
750 *out++ = e0[uint8_t(src[i])];
751 *out++ = e1[(uint8_t(src[i]) & 0x03) << 4];
752 }
753 }
754 break;
755 default: /* case 2 */
756 t1 = uint8_t(src[i]);
757 t2 = uint8_t(src[i + 1]);
758 if (use_padding) {
759 if (line_offset + 3 >= line_length) {
760 if (line_offset == line_length) {
761 *out++ = '\n';
762 *out++ = e0[t1];
763 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
764 *out++ = e2[(t2 & 0x0F) << 2];
765 *out++ = '=';
766 } else if (line_offset + 1 == line_length) {
767 *out++ = e0[t1];
768 *out++ = '\n';
769 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
770 *out++ = e2[(t2 & 0x0F) << 2];
771 *out++ = '=';
772 } else if (line_offset + 2 == line_length) {
773 *out++ = e0[t1];
774 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
775 *out++ = '\n';
776 *out++ = e2[(t2 & 0x0F) << 2];
777 *out++ = '=';
778 } else if (line_offset + 3 == line_length) {
779 *out++ = e0[t1];
780 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
781 *out++ = e2[(t2 & 0x0F) << 2];
782 *out++ = '\n';
783 *out++ = '=';
784 }
785 } else {
786 *out++ = e0[t1];
787 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
788 *out++ = e2[(t2 & 0x0F) << 2];
789 *out++ = '=';
790 }
791 } else {
792 if (line_offset + 3 >= line_length) {
793 if (line_offset == line_length) {
794 *out++ = '\n';
795 *out++ = e0[t1];
796 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
797 *out++ = e2[(t2 & 0x0F) << 2];
798 } else if (line_offset + 1 == line_length) {
799 *out++ = e0[t1];
800 *out++ = '\n';
801 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
802 *out++ = e2[(t2 & 0x0F) << 2];
803 } else if (line_offset + 2 == line_length) {
804 *out++ = e0[t1];
805 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
806 *out++ = '\n';
807 *out++ = e2[(t2 & 0x0F) << 2];
808 } else {
809 *out++ = e0[t1];
810 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
811 *out++ = e2[(t2 & 0x0F) << 2];
812 // *out++ = '\n'; ==> no newline at the end of the output
813 }
814 } else {
815 *out++ = e0[t1];
816 *out++ = e1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)];
817 *out++ = e2[(t2 & 0x0F) << 2];
818 }
819 }
820 }
821 return (size_t)(out - dst);
822 }
823}
824
825// Returns the number of bytes written. The destination buffer must be large
826// enough. It will add padding (=) if needed.
827simdutf_unused inline simdutf_constexpr23 size_t tail_encode_base64(
828 char *dst, const char *src, size_t srclen, base64_options options) {
829 return tail_encode_base64_impl(dst, src, srclen, options);
830}
831
832template <class InputPtr>
833simdutf_warn_unused simdutf_constexpr23 size_t
834maximal_binary_length_from_base64(InputPtr input, size_t length) noexcept {
835 // We process the padding characters ('=') at the end to make sure
836 // that we return an exact result when the input has no ignorable characters
837 // (e.g., spaces).
838 size_t padding = 0;
839 if (length > 0) {
840 if (input[length - 1] == '=') {
841 padding++;
842 if (length > 1 && input[length - 2] == '=') {
843 padding++;
844 }
845 }
846 }
847 // The input is not otherwise processed for ignorable characters or
848 // validation, so that the function runs in constant time (very fast). In
849 // practice, base64 inputs without ignorable characters are common and the
850 // common case are line separated inputs with relatively long lines (e.g., 76
851 // characters) which leads this function to a slight (1%) overestimation of
852 // the output size.
853 //
854 // Of course, some inputs might contain an arbitrary number of spaces or
855 // newlines, which would make this function return a very pessimistic output
856 // size but systems that produce base64 outputs typically do not do that and
857 // if they do, they do not care much about minimizing memory usage.
858 //
859 // In specialized applications, users may know that their input is line
860 // separated, which can be checked very quickly by by iterating (e.g., over 76
861 // character chunks, looking for the linefeed characters only). We could
862 // provide a specialized function for that, but it is not clear that the added
863 // complexity is worth it for us.
864 //
865 size_t actual_length = length - padding;
866 if (actual_length % 4 <= 1) {
867 return actual_length / 4 * 3;
868 }
869 // if we have a valid input, then the remainder must be 2 or 3 adding one or
870 // two extra bytes.
871 return actual_length / 4 * 3 + (actual_length % 4) - 1;
872}
873
874// This function computes the binary length by iterating through the input
875// and counting non-whitespace characters (excluding padding characters).
876// We use a simple check (c > ' ') which is easy to parallelize and matches
877// SIMD behavior. Only the last few characters are checked for padding '='.
878template <class char_type>
879simdutf_warn_unused simdutf_constexpr23 size_t
880binary_length_from_base64(const char_type *input, size_t length) noexcept {
881 // Count non-whitespace characters (c > ' ') with loop unrolling
882 size_t count = 0;
883 for (size_t i = 0; i < length; i++) {
884 count += (input[i] > ' ');
885 }
886
887 // Check for padding '=' at the end (at most 2 padding characters)
888 // Scan backwards, skipping whitespace, to find padding
889 size_t padding = 0;
890 size_t pos = length;
891 // Skip trailing whitespace
892 while (pos > 0 && padding < 2) {
893 char_type c = input[--pos];
894 if (c == '=') {
895 padding++;
896 } else if (c > ' ') {
897 break;
898 }
899 }
900 return ((count - padding) * 3) / 4;
901}
902
903template <typename char_type>
904simdutf_warn_unused simdutf_constexpr23 full_result
905base64_to_binary_details_impl(
906 const char_type *input, size_t length, char *output, base64_options options,
907 last_chunk_handling_options last_chunk_options) noexcept {
908 const bool ignore_garbage =
909 (options == base64_options::base64_url_accept_garbage) ||
910 (options == base64_options::base64_default_accept_garbage) ||
911 (options == base64_options::base64_default_or_url_accept_garbage);
912 auto ri = simdutf::scalar::base64::find_end(input, length, options);
913 size_t equallocation = ri.equallocation;
914 size_t equalsigns = ri.equalsigns;
915 length = ri.srclen;
916 size_t full_input_length = ri.full_input_length;
917 if (length == 0) {
918 if (!ignore_garbage && equalsigns > 0) {
919 return {INVALID_BASE64_CHARACTER, equallocation, 0, true};
920 }
921 return {SUCCESS, full_input_length, 0};
922 }
923 full_result r = scalar::base64::base64_tail_decode(
924 output, input, length, equalsigns, options, last_chunk_options);
925 r = scalar::base64::patch_tail_result(r, 0, 0, equallocation,
926 full_input_length, last_chunk_options);
927 if (!is_partial(last_chunk_options) && r.error == error_code::SUCCESS &&
928 equalsigns > 0 && !ignore_garbage) {
929 // additional checks
930 if ((r.output_count % 3 == 0) ||
931 ((r.output_count % 3) + 1 + equalsigns != 4)) {
932 return {INVALID_BASE64_CHARACTER, equallocation, r.output_count, true};
933 }
934 }
935 // When is_partial(last_chunk_options) is true, we must either end with
936 // the end of the stream (beyond whitespace) or right after a non-ignorable
937 // character or at the very beginning of the stream.
938 // See https://tc39.es/proposal-arraybuffer-base64/spec/#sec-frombase64
939 if (is_partial(last_chunk_options) && r.error == error_code::SUCCESS &&
940 r.input_count < full_input_length) {
941 // First check if we can extend the input to the end of the stream
942 while (r.input_count < full_input_length &&
943 base64_ignorable(*(input + r.input_count), options)) {
944 r.input_count++;
945 }
946 // If we are still not at the end of the stream, then we must backtrack
947 // to the last non-ignorable character.
948 if (r.input_count < full_input_length) {
949 while (r.input_count > 0 &&
950 base64_ignorable(*(input + r.input_count - 1), options)) {
951 r.input_count--;
952 }
953 }
954 }
955 return r;
956}
957
958template <typename char_type>
959simdutf_constexpr23 simdutf_warn_unused full_result
960base64_to_binary_details_safe_impl(
961 const char_type *input, size_t length, char *output, size_t outlen,
962 base64_options options,
963 last_chunk_handling_options last_chunk_options) noexcept {
964 const bool ignore_garbage =
965 (options == base64_options::base64_url_accept_garbage) ||
966 (options == base64_options::base64_default_accept_garbage) ||
967 (options == base64_options::base64_default_or_url_accept_garbage);
968 auto ri = simdutf::scalar::base64::find_end(input, length, options);
969 size_t equallocation = ri.equallocation;
970 size_t equalsigns = ri.equalsigns;
971 length = ri.srclen;
972 size_t full_input_length = ri.full_input_length;
973 if (length == 0) {
974 if (!ignore_garbage && equalsigns > 0) {
975 return {INVALID_BASE64_CHARACTER, equallocation, 0};
976 }
977 return {SUCCESS, full_input_length, 0};
978 }
979 full_result r = scalar::base64::base64_tail_decode_safe(
980 output, outlen, input, length, equalsigns, options, last_chunk_options);
981 r = scalar::base64::patch_tail_result(r, 0, 0, equallocation,
982 full_input_length, last_chunk_options);
983 if (!is_partial(last_chunk_options) && r.error == error_code::SUCCESS &&
984 equalsigns > 0 && !ignore_garbage) {
985 // additional checks
986 if ((r.output_count % 3 == 0) ||
987 ((r.output_count % 3) + 1 + equalsigns != 4)) {
988 return {INVALID_BASE64_CHARACTER, equallocation, r.output_count};
989 }
990 }
991
992 // When is_partial(last_chunk_options) is true, we must either end with
993 // the end of the stream (beyond whitespace) or right after a non-ignorable
994 // character or at the very beginning of the stream.
995 // See https://tc39.es/proposal-arraybuffer-base64/spec/#sec-frombase64
996 if (is_partial(last_chunk_options) && r.error == error_code::SUCCESS &&
997 r.input_count < full_input_length) {
998 // First check if we can extend the input to the end of the stream
999 while (r.input_count < full_input_length &&
1000 base64_ignorable(*(input + r.input_count), options)) {
1001 r.input_count++;
1002 }
1003 // If we are still not at the end of the stream, then we must backtrack
1004 // to the last non-ignorable character.
1005 if (r.input_count < full_input_length) {
1006 while (r.input_count > 0 &&
1007 base64_ignorable(*(input + r.input_count - 1), options)) {
1008 r.input_count--;
1009 }
1010 }
1011 }
1012 return r;
1013}
1014
1015simdutf_warn_unused simdutf_constexpr23 size_t
1016base64_length_from_binary(size_t length, base64_options options) noexcept {
1017 // By default, we use padding if we are not using the URL variant.
1018 // This is check with ((options & base64_url) == 0) which returns true if we
1019 // are not using the URL variant. However, we also allow 'inversion' of the
1020 // convention with the base64_reverse_padding option. If the
1021 // base64_reverse_padding option is set, we use padding if we are using the
1022 // URL variant, and we omit it if we are not using the URL variant. This is
1023 // checked with
1024 // ((options & base64_reverse_padding) == base64_reverse_padding).
1025 bool use_padding =
1026 ((options & base64_url) == 0) ^
1027 ((options & base64_reverse_padding) == base64_reverse_padding);
1028 if (!use_padding) {
1029 return length / 3 * 4 + ((length % 3) ? (length % 3) + 1 : 0);
1030 }
1031 return (length + 2) / 3 *
1032 4; // We use padding to make the length a multiple of 4.
1033}
1034
1035simdutf_warn_unused simdutf_constexpr23 size_t
1036base64_length_from_binary_with_lines(size_t length, base64_options options,
1037 size_t line_length) noexcept {
1038 if (length == 0) {
1039 return 0;
1040 }
1041 size_t base64_length =
1042 scalar::base64::base64_length_from_binary(length, options);
1043 if (line_length < 4) {
1044 line_length = 4;
1045 }
1046 // number of lines, rounded up without computing base64_length + line_length,
1047 // which wraps around when line_length is close to SIZE_MAX
1048 size_t lines = base64_length / line_length;
1049 if (base64_length % line_length != 0) {
1050 lines++;
1051 }
1052 return base64_length + lines - 1;
1053}
1054
1055// Return the length of the prefix that contains count base64 characters.
1056// Thus, if count is 3, the function returns the length of the prefix
1057// that contains 3 base64 characters.
1058// The function returns (size_t)-1 if there is not enough base64 characters in
1059// the input.
1060template <typename char_type>
1061simdutf_warn_unused size_t prefix_length(size_t count,
1062 simdutf::base64_options options,
1063 const char_type *input,
1064 size_t length) noexcept {
1065 size_t i = 0;
1066 while (i < length && is_ignorable(input[i], options)) {
1067 i++;
1068 }
1069 if (count == 0) {
1070 return i; // duh!
1071 }
1072 for (; i < length; i++) {
1073 if (is_ignorable(input[i], options)) {
1074 continue;
1075 }
1076 // We have a base64 character or a padding character.
1077 count--;
1078 if (count == 0) {
1079 return i + 1;
1080 }
1081 }
1082 simdutf_log_assert(false, "You never get here");
1083
1084 return -1; // should never happen
1085}
1086
1087} // namespace base64
1088} // unnamed namespace
1089} // namespace scalar
1090} // namespace simdutf
1091
1092#endif
helpers placed in namespace detail are not a part of the public API
simdutf_warn_unused simdutf_constexpr23 size_t base64_length_from_binary(size_t length, base64_options options=base64_default) noexcept
Provide the base64 length in bytes given the length of a binary input.
simdutf_warn_unused simdutf_really_inline simdutf_constexpr23 bool base64_ignorable(char input, base64_options options=base64_default) noexcept
Check if a character is an ignorable base64 character.
simdutf_warn_unused simdutf_really_inline simdutf_constexpr23 const char * find(const char *start, const char *end, char character) noexcept
Find the first occurrence of a character in a string.
constexpr size_t default_line_length
default line length for base64 encoding with lines
simdutf_warn_unused size_t maximal_binary_length_from_base64(const char *input, size_t length) noexcept
Provide the maximal binary length in bytes given the base64 input.
simdutf_warn_unused size_t binary_length_from_base64(const char *input, size_t length) noexcept
Compute the binary length from a base64 input.
simdutf_warn_unused simdutf_constexpr23 size_t base64_length_from_binary_with_lines(size_t length, base64_options options=base64_default, size_t line_length=default_line_length) noexcept
Provide the base64 length in bytes given the length of a binary input, taking into account line break...