/src/simdutf/src/icelake/icelake_base64.inl.cpp
Line | Count | Source |
1 | | // file included directly |
2 | | /** |
3 | | * References and further reading: |
4 | | * |
5 | | * Wojciech Muła, Daniel Lemire, Base64 encoding and decoding at almost the |
6 | | * speed of a memory copy, Software: Practice and Experience 50 (2), 2020. |
7 | | * https://arxiv.org/abs/1910.05109 |
8 | | * |
9 | | * Wojciech Muła, Daniel Lemire, Faster Base64 Encoding and Decoding using AVX2 |
10 | | * Instructions, ACM Transactions on the Web 12 (3), 2018. |
11 | | * https://arxiv.org/abs/1704.00605 |
12 | | * |
13 | | * Simon Josefsson. 2006. The Base16, Base32, and Base64 Data Encodings. |
14 | | * https://tools.ietf.org/html/rfc4648. (2006). Internet Engineering Task Force, |
15 | | * Request for Comments: 4648. |
16 | | * |
17 | | * Alfred Klomp. 2014a. Fast Base64 encoding/decoding with SSE vectorization. |
18 | | * http://www.alfredklomp.com/programming/sse-base64/. (2014). |
19 | | * |
20 | | * Alfred Klomp. 2014b. Fast Base64 stream encoder/decoder in C99, with SIMD |
21 | | * acceleration. https://github.com/aklomp/base64. (2014). |
22 | | * |
23 | | * Hanson Char. 2014. A Fast and Correct Base 64 Codec. (2014). |
24 | | * https://aws.amazon.com/blogs/developer/a-fast-and-correct-base-64-codec/ |
25 | | * |
26 | | * Nick Kopp. 2013. Base64 Encoding on a GPU. |
27 | | * https://www.codeproject.com/Articles/276993/Base-Encoding-on-a-GPU. (2013). |
28 | | */ |
29 | | |
30 | | struct block64 { |
31 | | __m512i chunks[1]; |
32 | | }; |
33 | | |
34 | | static inline size_t write_multi_lf_m256i(__m256i chunk, uint8_t *out, |
35 | | size_t output_len, size_t line_length, |
36 | 0 | size_t &offset) { |
37 | 0 | if (line_length >= 32) { |
38 | 0 | if (offset + output_len > line_length) { |
39 | 0 | __m512i expanded = _mm512_mask_expand_epi8( |
40 | 0 | _mm512_set1_epi8('\n'), ~(1ULL << (line_length - offset)), |
41 | 0 | _mm512_castsi256_si512(chunk)); |
42 | 0 | _mm512_mask_storeu_epi8(reinterpret_cast<__m512i *>(out), |
43 | 0 | (1ULL << (output_len + 1)) - 1, expanded); |
44 | 0 | offset = output_len - (line_length - offset); |
45 | 0 | return output_len + 1; |
46 | 0 | } else { |
47 | 0 | __mmask32 write_mask = |
48 | 0 | output_len == 32 ? 0xffffffff : ((__mmask32)1 << output_len) - 1; |
49 | 0 | _mm256_mask_storeu_epi8(reinterpret_cast<__m256i *>(out), write_mask, |
50 | 0 | chunk); |
51 | 0 | offset += output_len; |
52 | 0 | return output_len; |
53 | 0 | } |
54 | 0 | } else { |
55 | | // minimum line_length starts from 4 |
56 | 0 | static const uint64_t masks[28] = { |
57 | 0 | 0x2700000842108421, 0x2600001041041041, 0x2500000810204081, |
58 | 0 | 0x2400000101010101, 0x2300000008040201, 0x2300000040100401, |
59 | 0 | 0x2300000200400801, 0x2200000001001001, 0x2200000004002001, |
60 | 0 | 0x2200000010004001, 0x2200000040008001, 0x2200000100010001, |
61 | 0 | 0x2100000000020001, 0x2100000000040001, 0x2100000000080001, |
62 | 0 | 0x2100000000100001, 0x2100000000200001, 0x2100000000400001, |
63 | 0 | 0x2100000000800001, 0x2100000001000001, 0x2100000002000001, |
64 | 0 | 0x2100000004000001, 0x2100000008000001, 0x2100000010000001, |
65 | 0 | 0x2100000020000001, 0x2100000040000001, 0x2100000080000001, |
66 | 0 | 0x2100000100000001, |
67 | 0 | }; |
68 | 0 | uint64_t mask = masks[line_length - 4]; |
69 | 0 | uint64_t max_width; |
70 | 0 | uint64_t num_lf; |
71 | 0 | if (output_len == 32) { |
72 | | // use pre-computed width to avoid integer division in main loop |
73 | 0 | max_width = mask >> 56; |
74 | 0 | mask = (mask << (line_length - offset)) & ((1ULL << max_width) - 1); |
75 | 0 | num_lf = _mm_popcnt_u64(mask); |
76 | 0 | } else { |
77 | 0 | if (output_len <= line_length - offset) { |
78 | 0 | num_lf = 0; |
79 | 0 | } else { |
80 | 0 | num_lf = 1 + (output_len - (line_length - offset) - 1) / line_length; |
81 | 0 | } |
82 | 0 | max_width = num_lf + output_len; |
83 | 0 | mask = (mask << (line_length - offset)) & ((1ULL << max_width) - 1); |
84 | 0 | } |
85 | 0 | __mmask64 write_mask = (1ULL << (num_lf + output_len)) - 1; |
86 | 0 | __m512i expanded = |
87 | 0 | _mm512_mask_expand_epi8(_mm512_set1_epi8('\n'), ~((uint64_t)mask), |
88 | 0 | _mm512_castsi256_si512(chunk)); |
89 | 0 | _mm512_mask_storeu_epi8(reinterpret_cast<__m512i *>(out), write_mask, |
90 | 0 | expanded); |
91 | 0 | offset = _lzcnt_u64(mask) - _lzcnt_u64(write_mask); |
92 | 0 | return num_lf + output_len; |
93 | 0 | } |
94 | 0 | } |
95 | | |
96 | | template <bool base64_url, bool use_lines> |
97 | | size_t encode_base64_impl(char *dst, const char *src, size_t srclen, |
98 | | base64_options options, |
99 | 0 | size_t line_length = simdutf::default_line_length) { |
100 | 0 | size_t offset = 0; |
101 | 0 | if (line_length < 4) { |
102 | 0 | line_length = 4; // We do not support line_length less than 4 |
103 | 0 | } |
104 | | // credit: Wojciech Muła |
105 | 0 | const uint8_t *input = (const uint8_t *)src; |
106 | |
|
107 | 0 | uint8_t *out = (uint8_t *)dst; |
108 | 0 | static const char *lookup_tbl = |
109 | 0 | base64_url |
110 | 0 | ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" |
111 | 0 | : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
112 | 0 | const __m512i shuffle_input = _mm512_setr_epi32( |
113 | 0 | 0x01020001, 0x04050304, 0x07080607, 0x0a0b090a, 0x0d0e0c0d, 0x10110f10, |
114 | 0 | 0x13141213, 0x16171516, 0x191a1819, 0x1c1d1b1c, 0x1f201e1f, 0x22232122, |
115 | 0 | 0x25262425, 0x28292728, 0x2b2c2a2b, 0x2e2f2d2e); |
116 | 0 | const __m512i lookup = |
117 | 0 | _mm512_loadu_si512(reinterpret_cast<const __m512i *>(lookup_tbl)); |
118 | 0 | const __m512i multi_shifts = _mm512_set1_epi64(UINT64_C(0x3036242a1016040a)); |
119 | 0 | size_t size = srclen; |
120 | 0 | __mmask64 input_mask = 0xffffffffffff; // (1 << 48) - 1 |
121 | | // We want that input == end_input means that we must stop. |
122 | 0 | const uint8_t *end_input = input + (size - (size % 48)); |
123 | 0 | while (input != end_input) { |
124 | 0 | const __m512i v = _mm512_maskz_loadu_epi8( |
125 | 0 | input_mask, reinterpret_cast<const __m512i *>(input)); |
126 | 0 | const __m512i in = _mm512_permutexvar_epi8(shuffle_input, v); |
127 | 0 | const __m512i indices = _mm512_multishift_epi64_epi8(multi_shifts, in); |
128 | 0 | const __m512i result = _mm512_permutexvar_epi8(indices, lookup); |
129 | 0 | if (use_lines) { |
130 | 0 | if (offset + 64 > line_length) { |
131 | 0 | if (line_length >= 64) { |
132 | 0 | __m512i expanded = _mm512_mask_expand_epi8( |
133 | 0 | _mm512_set1_epi8('\n'), ~(1ULL << ((line_length - offset))), |
134 | 0 | result); |
135 | 0 | _mm512_storeu_si512(reinterpret_cast<__m512i *>(out), expanded); |
136 | 0 | __m128i last_lane = |
137 | 0 | _mm512_extracti32x4_epi32(result, 3); // Lane 3 (bytes 48-63) |
138 | 0 | uint8_t last_byte = |
139 | 0 | static_cast<uint8_t>(_mm_extract_epi8(last_lane, 15)); |
140 | 0 | out[64] = last_byte; |
141 | 0 | out += 65; |
142 | 0 | offset = 64 - (line_length - offset); |
143 | 0 | } else { // slow path |
144 | 0 | __m256i lo = _mm512_extracti64x4_epi64(result, 0); |
145 | 0 | __m256i hi = _mm512_extracti64x4_epi64(result, 1); |
146 | 0 | out += write_multi_lf_m256i(lo, out, 32, line_length, offset); |
147 | 0 | out += write_multi_lf_m256i(hi, out, 32, line_length, offset); |
148 | 0 | } |
149 | 0 | } else { |
150 | 0 | _mm512_storeu_si512(reinterpret_cast<__m512i *>(out), result); |
151 | 0 | offset += 64; |
152 | 0 | out += 64; |
153 | 0 | } |
154 | 0 | } else { |
155 | 0 | _mm512_storeu_si512(reinterpret_cast<__m512i *>(out), result); |
156 | 0 | out += 64; |
157 | 0 | } |
158 | 0 | input += 48; |
159 | 0 | } |
160 | 0 | size = size % 48; |
161 | |
|
162 | 0 | input_mask = ((__mmask64)1 << size) - 1; |
163 | 0 | const __m512i v = _mm512_maskz_loadu_epi8( |
164 | 0 | input_mask, reinterpret_cast<const __m512i *>(input)); |
165 | 0 | const __m512i in = _mm512_permutexvar_epi8(shuffle_input, v); |
166 | 0 | const __m512i indices = _mm512_multishift_epi64_epi8(multi_shifts, in); |
167 | 0 | bool padding_needed = |
168 | 0 | (((options & base64_url) == 0) ^ |
169 | 0 | ((options & base64_reverse_padding) == base64_reverse_padding)); |
170 | 0 | size_t padding_amount = ((size % 3) > 0) ? (3 - (size % 3)) : 0; |
171 | 0 | size_t output_len = ((size + 2) / 3) * 4; |
172 | 0 | size_t non_padded_output_len = output_len - padding_amount; |
173 | 0 | if (!padding_needed) { |
174 | 0 | output_len = non_padded_output_len; |
175 | 0 | } |
176 | | // If no output, we are done. |
177 | 0 | if (output_len == 0) { |
178 | 0 | return (size_t)(out - (uint8_t *)dst); |
179 | 0 | } |
180 | 0 | __mmask64 output_mask = 0xFFFFFFFFFFFFFFFF >> (64 - output_len); |
181 | 0 | __m512i result = _mm512_mask_permutexvar_epi8( |
182 | 0 | _mm512_set1_epi8('='), ((__mmask64)1 << non_padded_output_len) - 1, |
183 | 0 | indices, lookup); |
184 | 0 | if (use_lines) { |
185 | 0 | if (offset + output_len > line_length) { |
186 | 0 | if (line_length >= 64) { |
187 | 0 | __m512i expanded = _mm512_mask_expand_epi8( |
188 | 0 | _mm512_set1_epi8('\n'), ~(1ULL << ((line_length - offset))), |
189 | 0 | result); |
190 | 0 | if (output_len == 64) { |
191 | 0 | _mm512_storeu_si512(reinterpret_cast<__m512i *>(out), expanded); |
192 | 0 | out += 64; |
193 | 0 | _mm512_mask_storeu_epi8(reinterpret_cast<__m512i *>(out - 63), |
194 | 0 | 1ULL << 63, result); |
195 | 0 | out++; |
196 | 0 | } else { |
197 | 0 | output_mask = 0xFFFFFFFFFFFFFFFF >> (64 - output_len - 1); |
198 | 0 | _mm512_mask_storeu_epi8(reinterpret_cast<__m512i *>(out), output_mask, |
199 | 0 | expanded); |
200 | 0 | out += output_len + 1; |
201 | 0 | } |
202 | 0 | } else { |
203 | 0 | if (output_len > 32) { |
204 | 0 | __m256i lo = _mm512_extracti64x4_epi64(result, 0); |
205 | 0 | __m256i hi = _mm512_extracti64x4_epi64(result, 1); |
206 | 0 | out += write_multi_lf_m256i(lo, out, 32, line_length, offset); |
207 | 0 | out += write_multi_lf_m256i(hi, out, output_len - 32, line_length, |
208 | 0 | offset); |
209 | 0 | } else { |
210 | 0 | __m256i lo = _mm512_extracti64x4_epi64(result, 0); |
211 | 0 | out += write_multi_lf_m256i(lo, out, output_len, line_length, offset); |
212 | 0 | } |
213 | 0 | } |
214 | 0 | } else { |
215 | 0 | _mm512_mask_storeu_epi8(reinterpret_cast<__m512i *>(out), output_mask, |
216 | 0 | result); |
217 | 0 | out += output_len; |
218 | 0 | } |
219 | 0 | } else { |
220 | 0 | _mm512_mask_storeu_epi8(reinterpret_cast<__m512i *>(out), output_mask, |
221 | 0 | result); |
222 | 0 | out += output_len; |
223 | 0 | } |
224 | 0 | return (size_t)(out - (uint8_t *)dst); |
225 | 0 | } Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::encode_base64_impl<true, false>(char*, char const*, unsigned long, simdutf::base64_options, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::encode_base64_impl<false, false>(char*, char const*, unsigned long, simdutf::base64_options, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::encode_base64_impl<true, true>(char*, char const*, unsigned long, simdutf::base64_options, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::encode_base64_impl<false, true>(char*, char const*, unsigned long, simdutf::base64_options, unsigned long) |
226 | | |
227 | | template <bool base64_url> |
228 | | size_t encode_base64(char *dst, const char *src, size_t srclen, |
229 | 0 | base64_options options) { |
230 | 0 | return encode_base64_impl<base64_url, false>(dst, src, srclen, options); |
231 | 0 | } Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::encode_base64<true>(char*, char const*, unsigned long, simdutf::base64_options) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::encode_base64<false>(char*, char const*, unsigned long, simdutf::base64_options) |
232 | | |
233 | | template <bool base64_url, bool ignore_garbage, bool default_or_url> |
234 | | static inline uint64_t to_base64_mask(block64 *b, uint64_t *error, |
235 | 0 | uint64_t input_mask = UINT64_MAX) { |
236 | 0 | __m512i input = b->chunks[0]; |
237 | 0 | const __m512i ascii_space_tbl = _mm512_set_epi8( |
238 | 0 | 0, 0, 13, 12, 0, 10, 9, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 13, 12, 0, 10, |
239 | 0 | 9, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 13, 12, 0, 10, 9, 0, 0, 0, 0, 0, 0, |
240 | 0 | 0, 0, 32, 0, 0, 13, 12, 0, 10, 9, 0, 0, 0, 0, 0, 0, 0, 0, 32); |
241 | 0 | __m512i lookup0; |
242 | 0 | if (default_or_url) { |
243 | 0 | lookup0 = _mm512_set_epi8( |
244 | 0 | -128, -128, -128, -128, -128, -128, 61, 60, 59, 58, 57, 56, 55, 54, 53, |
245 | 0 | 52, 63, -128, 62, -128, 62, -128, -128, -128, -128, -128, -128, -128, |
246 | 0 | -128, -128, -128, -1, -128, -128, -128, -128, -128, -128, -128, -128, |
247 | 0 | -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -1, -128, |
248 | 0 | -128, -1, -1, -128, -128, -128, -128, -128, -128, -128, -128, -1); |
249 | 0 | } else if (base64_url) { |
250 | 0 | lookup0 = _mm512_set_epi8( |
251 | 0 | -128, -128, -128, -128, -128, -128, 61, 60, 59, 58, 57, 56, 55, 54, 53, |
252 | 0 | 52, -128, -128, 62, -128, -128, -128, -128, -128, -128, -128, -128, |
253 | 0 | -128, -128, -128, -128, -1, -128, -128, -128, -128, -128, -128, -128, |
254 | 0 | -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -1, |
255 | 0 | -128, -128, -1, -1, -128, -128, -128, -128, -128, -128, -128, -128, -1); |
256 | 0 | } else { |
257 | 0 | lookup0 = _mm512_set_epi8( |
258 | 0 | -128, -128, -128, -128, -128, -128, 61, 60, 59, 58, 57, 56, 55, 54, 53, |
259 | 0 | 52, 63, -128, -128, -128, 62, -128, -128, -128, -128, -128, -128, -128, |
260 | 0 | -128, -128, -128, -1, -128, -128, -128, -128, -128, -128, -128, -128, |
261 | 0 | -128, -128, -128, -128, -128, -128, -128, -128, -128, -128, -1, -128, |
262 | 0 | -128, -1, -1, -128, -128, -128, -128, -128, -128, -128, -128, -128); |
263 | 0 | } |
264 | 0 | __m512i lookup1; |
265 | 0 | if (default_or_url) { |
266 | 0 | lookup1 = _mm512_set_epi8( |
267 | 0 | -128, -128, -128, -128, -128, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, |
268 | 0 | 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, -128, |
269 | 0 | 63, -128, -128, -128, -128, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, |
270 | 0 | 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -128); |
271 | 0 | } else if (base64_url) { |
272 | 0 | lookup1 = _mm512_set_epi8( |
273 | 0 | -128, -128, -128, -128, -128, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, |
274 | 0 | 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, -128, |
275 | 0 | 63, -128, -128, -128, -128, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, |
276 | 0 | 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -128); |
277 | 0 | } else { |
278 | 0 | lookup1 = _mm512_set_epi8( |
279 | 0 | -128, -128, -128, -128, -128, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, |
280 | 0 | 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, -128, |
281 | 0 | -128, -128, -128, -128, -128, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, |
282 | 0 | 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -128); |
283 | 0 | } |
284 | |
|
285 | 0 | const __m512i translated = _mm512_permutex2var_epi8(lookup0, input, lookup1); |
286 | 0 | const __m512i combined = _mm512_or_si512(translated, input); |
287 | 0 | const __mmask64 mask = _mm512_movepi8_mask(combined) & input_mask; |
288 | 0 | if (!ignore_garbage && mask) { |
289 | 0 | const __mmask64 spaces = |
290 | 0 | _mm512_cmpeq_epi8_mask(_mm512_shuffle_epi8(ascii_space_tbl, input), |
291 | 0 | input) & |
292 | 0 | input_mask; |
293 | 0 | *error = (mask ^ spaces); |
294 | 0 | } |
295 | 0 | b->chunks[0] = translated; |
296 | |
|
297 | 0 | return mask | (~input_mask); |
298 | 0 | } Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::to_base64_mask<false, true, true>(simdutf::icelake::(anonymous namespace)::block64*, unsigned long*, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::to_base64_mask<false, false, true>(simdutf::icelake::(anonymous namespace)::block64*, unsigned long*, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::to_base64_mask<true, true, false>(simdutf::icelake::(anonymous namespace)::block64*, unsigned long*, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::to_base64_mask<true, false, false>(simdutf::icelake::(anonymous namespace)::block64*, unsigned long*, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::to_base64_mask<false, true, false>(simdutf::icelake::(anonymous namespace)::block64*, unsigned long*, unsigned long) Unexecuted instantiation: simdutf.cpp:unsigned long simdutf::icelake::(anonymous namespace)::to_base64_mask<false, false, false>(simdutf::icelake::(anonymous namespace)::block64*, unsigned long*, unsigned long) |
299 | | |
300 | 0 | static inline void copy_block(block64 *b, char *output) { |
301 | 0 | _mm512_storeu_si512(reinterpret_cast<__m512i *>(output), b->chunks[0]); |
302 | 0 | } |
303 | | |
304 | 0 | static inline uint64_t compress_block(block64 *b, uint64_t mask, char *output) { |
305 | 0 | uint64_t nmask = ~mask; |
306 | 0 | __m512i c = _mm512_maskz_compress_epi8(nmask, b->chunks[0]); |
307 | 0 | _mm512_storeu_si512(reinterpret_cast<__m512i *>(output), c); |
308 | 0 | return _mm_popcnt_u64(nmask); |
309 | 0 | } |
310 | | |
311 | | // The caller of this function is responsible to ensure that there are 64 bytes |
312 | | // available from reading at src. The data is read into a block64 structure. |
313 | 0 | static inline void load_block(block64 *b, const char *src) { |
314 | 0 | b->chunks[0] = _mm512_loadu_si512(reinterpret_cast<const __m512i *>(src)); |
315 | 0 | } |
316 | | |
317 | | static inline void load_block_partial(block64 *b, const char *src, |
318 | 0 | __mmask64 input_mask) { |
319 | 0 | b->chunks[0] = _mm512_maskz_loadu_epi8( |
320 | 0 | input_mask, reinterpret_cast<const __m512i *>(src)); |
321 | 0 | } |
322 | | |
323 | | // The caller of this function is responsible to ensure that there are 128 bytes |
324 | | // available from reading at src. The data is read into a block64 structure. |
325 | 0 | static inline void load_block(block64 *b, const char16_t *src) { |
326 | 0 | __m512i m1 = _mm512_loadu_si512(reinterpret_cast<const __m512i *>(src)); |
327 | 0 | __m512i m2 = _mm512_loadu_si512(reinterpret_cast<const __m512i *>(src + 32)); |
328 | 0 | __m512i p = _mm512_packus_epi16(m1, m2); |
329 | 0 | b->chunks[0] = |
330 | 0 | _mm512_permutexvar_epi64(_mm512_setr_epi64(0, 2, 4, 6, 1, 3, 5, 7), p); |
331 | 0 | } |
332 | | |
333 | | static inline void load_block_partial(block64 *b, const char16_t *src, |
334 | 0 | __mmask64 input_mask) { |
335 | 0 | __m512i m1 = _mm512_maskz_loadu_epi16((__mmask32)input_mask, |
336 | 0 | reinterpret_cast<const __m512i *>(src)); |
337 | 0 | __m512i m2 = |
338 | 0 | _mm512_maskz_loadu_epi16((__mmask32)(input_mask >> 32), |
339 | 0 | reinterpret_cast<const __m512i *>(src + 32)); |
340 | 0 | __m512i p = _mm512_packus_epi16(m1, m2); |
341 | 0 | b->chunks[0] = |
342 | 0 | _mm512_permutexvar_epi64(_mm512_setr_epi64(0, 2, 4, 6, 1, 3, 5, 7), p); |
343 | 0 | } |
344 | | |
345 | 0 | static inline void base64_decode(char *out, __m512i str) { |
346 | 0 | const __m512i merge_ab_and_bc = |
347 | 0 | _mm512_maddubs_epi16(str, _mm512_set1_epi32(0x01400140)); |
348 | 0 | const __m512i merged = |
349 | 0 | _mm512_madd_epi16(merge_ab_and_bc, _mm512_set1_epi32(0x00011000)); |
350 | 0 | const __m512i pack = _mm512_set_epi8( |
351 | 0 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 61, 62, 56, 57, 58, |
352 | 0 | 52, 53, 54, 48, 49, 50, 44, 45, 46, 40, 41, 42, 36, 37, 38, 32, 33, 34, |
353 | 0 | 28, 29, 30, 24, 25, 26, 20, 21, 22, 16, 17, 18, 12, 13, 14, 8, 9, 10, 4, |
354 | 0 | 5, 6, 0, 1, 2); |
355 | 0 | const __m512i shuffled = _mm512_permutexvar_epi8(pack, merged); |
356 | 0 | _mm512_mask_storeu_epi8( |
357 | 0 | (__m512i *)out, 0xffffffffffff, |
358 | 0 | shuffled); // mask would be 0xffffffffffff since we write 48 bytes. |
359 | 0 | } |
360 | | // decode 64 bytes and output 48 bytes |
361 | 0 | static inline void base64_decode_block(char *out, const char *src) { |
362 | 0 | base64_decode(out, |
363 | 0 | _mm512_loadu_si512(reinterpret_cast<const __m512i *>(src))); |
364 | 0 | } |
365 | 0 | static inline void base64_decode_block(char *out, block64 *b) { |
366 | 0 | base64_decode(out, b->chunks[0]); |
367 | 0 | } |
368 | | |
369 | | template <bool base64_url, bool ignore_garbage, bool default_or_url, |
370 | | typename chartype> |
371 | | full_result |
372 | | compress_decode_base64(char *dst, const chartype *src, size_t srclen, |
373 | | base64_options options, |
374 | 0 | last_chunk_handling_options last_chunk_options) { |
375 | 0 | (void)options; |
376 | 0 | const uint8_t *to_base64 = |
377 | 0 | default_or_url ? tables::base64::to_base64_default_or_url_value |
378 | 0 | : (base64_url ? tables::base64::to_base64_url_value |
379 | 0 | : tables::base64::to_base64_value); |
380 | 0 | auto ri = simdutf::scalar::base64::find_end(src, srclen, options); |
381 | 0 | size_t equallocation = ri.equallocation; |
382 | 0 | size_t padding_characters = ri.equalsigns; |
383 | 0 | srclen = ri.srclen; |
384 | 0 | size_t full_input_length = ri.full_input_length; |
385 | 0 | if (srclen == 0) { |
386 | 0 | if (!ignore_garbage && padding_characters > 0) { |
387 | 0 | return {INVALID_BASE64_CHARACTER, equallocation, 0, true}; |
388 | 0 | } |
389 | 0 | return {SUCCESS, full_input_length, 0}; |
390 | 0 | } |
391 | 0 | const chartype *const srcinit = src; |
392 | 0 | const char *const dstinit = dst; |
393 | 0 | const chartype *const srcend = src + srclen; |
394 | | |
395 | | // figure out why block_size == 2 is sometimes best??? |
396 | 0 | constexpr size_t block_size = 6; |
397 | 0 | char buffer[block_size * 64]; |
398 | 0 | char *bufferptr = buffer; |
399 | 0 | if (srclen >= 64) { |
400 | 0 | const chartype *const srcend64 = src + srclen - 64; |
401 | 0 | while (src <= srcend64) { |
402 | 0 | block64 b; |
403 | 0 | load_block(&b, src); |
404 | 0 | src += 64; |
405 | 0 | uint64_t error = 0; |
406 | 0 | uint64_t badcharmask = |
407 | 0 | to_base64_mask<base64_url, ignore_garbage, default_or_url>(&b, |
408 | 0 | &error); |
409 | 0 | if (!ignore_garbage && error) { |
410 | 0 | src -= 64; |
411 | 0 | size_t error_offset = _tzcnt_u64(error); |
412 | 0 | return {error_code::INVALID_BASE64_CHARACTER, |
413 | 0 | size_t(src - srcinit + error_offset), size_t(dst - dstinit)}; |
414 | 0 | } |
415 | 0 | if (badcharmask != 0) { |
416 | | // optimization opportunity: check for simple masks like those made of |
417 | | // continuous 1s followed by continuous 0s. And masks containing a |
418 | | // single bad character. |
419 | 0 | bufferptr += compress_block(&b, badcharmask, bufferptr); |
420 | 0 | } else if (bufferptr != buffer) { |
421 | 0 | copy_block(&b, bufferptr); |
422 | 0 | bufferptr += 64; |
423 | 0 | } else { |
424 | 0 | base64_decode_block(dst, &b); |
425 | 0 | dst += 48; |
426 | 0 | } |
427 | 0 | if (bufferptr >= (block_size - 1) * 64 + buffer) { |
428 | 0 | for (size_t i = 0; i < (block_size - 1); i++) { |
429 | 0 | base64_decode_block(dst, buffer + i * 64); |
430 | 0 | dst += 48; |
431 | 0 | } |
432 | 0 | std::memcpy(buffer, buffer + (block_size - 1) * 64, |
433 | 0 | 64); // 64 might be too much |
434 | 0 | bufferptr -= (block_size - 1) * 64; |
435 | 0 | } |
436 | 0 | } |
437 | 0 | } |
438 | | |
439 | 0 | int last_block_len = (int)(srcend - src); |
440 | 0 | if (last_block_len != 0) { |
441 | 0 | __mmask64 input_mask = ((__mmask64)1 << last_block_len) - 1; |
442 | 0 | block64 b; |
443 | 0 | load_block_partial(&b, src, input_mask); |
444 | 0 | uint64_t error = 0; |
445 | 0 | uint64_t badcharmask = |
446 | 0 | to_base64_mask<base64_url, ignore_garbage, default_or_url>(&b, &error, |
447 | 0 | input_mask); |
448 | 0 | if (!ignore_garbage && error) { |
449 | 0 | size_t error_offset = _tzcnt_u64(error); |
450 | 0 | return {error_code::INVALID_BASE64_CHARACTER, |
451 | 0 | size_t(src - srcinit + error_offset), size_t(dst - dstinit)}; |
452 | 0 | } |
453 | 0 | src += last_block_len; |
454 | 0 | bufferptr += compress_block(&b, badcharmask, bufferptr); |
455 | 0 | } |
456 | | |
457 | 0 | char *buffer_start = buffer; |
458 | 0 | for (; buffer_start + 64 <= bufferptr; buffer_start += 64) { |
459 | 0 | base64_decode_block(dst, buffer_start); |
460 | 0 | dst += 48; |
461 | 0 | } |
462 | 0 | if ((bufferptr - buffer_start) != 0) { |
463 | | // For efficiency reasons, we end up reproducing much of the code |
464 | | // in base64_tail_decode_impl. Better engineering would be to |
465 | | // refactor the code so that we can call it without a performance hit. |
466 | 0 | size_t rem = (bufferptr - buffer_start); |
467 | 0 | int idx = rem % 4; |
468 | 0 | __mmask64 mask = ((__mmask64)1 << rem) - 1; |
469 | 0 | __m512i input = _mm512_maskz_loadu_epi8(mask, buffer_start); |
470 | 0 | size_t output_len = (rem / 4) * 3; |
471 | 0 | __mmask64 output_mask = mask >> (rem - output_len); |
472 | 0 | const __m512i merge_ab_and_bc = |
473 | 0 | _mm512_maddubs_epi16(input, _mm512_set1_epi32(0x01400140)); |
474 | 0 | const __m512i merged = |
475 | 0 | _mm512_madd_epi16(merge_ab_and_bc, _mm512_set1_epi32(0x00011000)); |
476 | 0 | const __m512i pack = _mm512_set_epi8( |
477 | 0 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 61, 62, 56, 57, 58, |
478 | 0 | 52, 53, 54, 48, 49, 50, 44, 45, 46, 40, 41, 42, 36, 37, 38, 32, 33, 34, |
479 | 0 | 28, 29, 30, 24, 25, 26, 20, 21, 22, 16, 17, 18, 12, 13, 14, 8, 9, 10, 4, |
480 | 0 | 5, 6, 0, 1, 2); |
481 | 0 | const __m512i shuffled = _mm512_permutexvar_epi8(pack, merged); |
482 | | // We never should have that the number of base64 characters + the |
483 | | // number of padding characters is more than 4. |
484 | 0 | if (!ignore_garbage && (idx + padding_characters > 4)) { |
485 | 0 | return {INVALID_BASE64_CHARACTER, equallocation, size_t(dst - dstinit), |
486 | 0 | true}; |
487 | 0 | } |
488 | | // The idea here is that in loose mode, |
489 | | // if there is padding at all, it must be used |
490 | | // to form 4-wise chunk. However, in loose mode, |
491 | | // we do accept no padding at all. |
492 | 0 | if (!ignore_garbage && |
493 | 0 | last_chunk_options == last_chunk_handling_options::loose && |
494 | 0 | (idx >= 2) && padding_characters > 0 && |
495 | 0 | ((idx + padding_characters) & 3) != 0) { |
496 | 0 | return {INVALID_BASE64_CHARACTER, equallocation, size_t(dst - dstinit), |
497 | 0 | true}; |
498 | 0 | } else |
499 | | // The idea here is that in strict mode, we do not want to accept |
500 | | // incomplete base64 chunks. So if the chunk was otherwise valid, we |
501 | | // return BASE64_INPUT_REMAINDER. |
502 | 0 | if (!ignore_garbage && |
503 | 0 | last_chunk_options == last_chunk_handling_options::strict && |
504 | 0 | (idx >= 2) && ((idx + padding_characters) & 3) != 0) { |
505 | | // The partial chunk was at src - idx |
506 | 0 | if (output_len > 0) { |
507 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
508 | 0 | dst += output_len; |
509 | 0 | } |
510 | 0 | return {BASE64_INPUT_REMAINDER, equallocation, size_t(dst - dstinit), |
511 | 0 | true}; |
512 | 0 | } else |
513 | | // If there is a partial chunk with insufficient padding, with |
514 | | // stop_before_partial, we need to just ignore it. In "only full" mode, |
515 | | // skip the minute there are padding characters. |
516 | 0 | if ((last_chunk_options == |
517 | 0 | last_chunk_handling_options::stop_before_partial && |
518 | 0 | (padding_characters + idx < 4) && (idx != 0) && |
519 | 0 | (idx >= 2 || padding_characters == 0)) || |
520 | 0 | (last_chunk_options == |
521 | 0 | last_chunk_handling_options::only_full_chunks && |
522 | 0 | (idx >= 2 || padding_characters == 0))) { |
523 | 0 | if (output_len > 0) { |
524 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
525 | 0 | dst += output_len; |
526 | 0 | } |
527 | | // we need to rewind src to before the partial chunk |
528 | 0 | size_t characters_to_skip = idx; |
529 | 0 | while (characters_to_skip > 0) { |
530 | 0 | src--; |
531 | 0 | auto c = *src; |
532 | 0 | uint8_t code = to_base64[uint8_t(c)]; |
533 | 0 | if (simdutf::scalar::base64::is_eight_byte(c) && code <= 63) { |
534 | 0 | characters_to_skip--; |
535 | 0 | } |
536 | 0 | } |
537 | | // And then we need to skip ignored characters |
538 | | // See https://github.com/simdutf/simdutf/issues/824 |
539 | 0 | while (src > srcinit) { |
540 | 0 | auto c = *(src - 1); |
541 | 0 | uint8_t code = to_base64[uint8_t(c)]; |
542 | 0 | if (simdutf::scalar::base64::is_eight_byte(c) && code <= 63) { |
543 | 0 | break; |
544 | 0 | } |
545 | 0 | src--; |
546 | 0 | } |
547 | 0 | return {SUCCESS, size_t(src - srcinit), size_t(dst - dstinit)}; |
548 | 0 | } else { |
549 | 0 | if (idx == 2) { |
550 | 0 | if (!ignore_garbage && |
551 | 0 | last_chunk_options == last_chunk_handling_options::strict) { |
552 | 0 | uint32_t triple = (uint32_t(bufferptr[-2]) << 3 * 6) + |
553 | 0 | (uint32_t(bufferptr[-1]) << 2 * 6); |
554 | 0 | if (triple & 0xffff) { |
555 | 0 | if (output_len > 0) { |
556 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, |
557 | 0 | shuffled); |
558 | 0 | dst += output_len; |
559 | 0 | } |
560 | 0 | return {BASE64_EXTRA_BITS, size_t(src - srcinit), |
561 | 0 | size_t(dst - dstinit)}; |
562 | 0 | } |
563 | 0 | } |
564 | 0 | output_mask = (output_mask << 1) | 1; |
565 | 0 | output_len += 1; |
566 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
567 | 0 | dst += output_len; |
568 | 0 | } else if (idx == 3) { |
569 | 0 | if (!ignore_garbage && |
570 | 0 | last_chunk_options == last_chunk_handling_options::strict) { |
571 | 0 | uint32_t triple = (uint32_t(bufferptr[-3]) << 3 * 6) + |
572 | 0 | (uint32_t(bufferptr[-2]) << 2 * 6) + |
573 | 0 | (uint32_t(bufferptr[-1]) << 1 * 6); |
574 | 0 | if (triple & 0xff) { |
575 | 0 | if (output_len > 0) { |
576 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, |
577 | 0 | shuffled); |
578 | 0 | dst += output_len; |
579 | 0 | } |
580 | 0 | return {BASE64_EXTRA_BITS, size_t(src - srcinit), |
581 | 0 | size_t(dst - dstinit)}; |
582 | 0 | } |
583 | 0 | } |
584 | 0 | output_mask = (output_mask << 2) | 3; |
585 | 0 | output_len += 2; |
586 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
587 | 0 | dst += output_len; |
588 | 0 | } else if (!ignore_garbage && idx == 1 && |
589 | 0 | (!is_partial(last_chunk_options) || |
590 | 0 | (is_partial(last_chunk_options) && |
591 | 0 | padding_characters > 0))) { |
592 | 0 | if (output_len > 0) { |
593 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
594 | 0 | dst += output_len; |
595 | 0 | } |
596 | 0 | return {BASE64_INPUT_REMAINDER, size_t(src - srcinit), |
597 | 0 | size_t(dst - dstinit)}; |
598 | 0 | } else if (!ignore_garbage && idx == 0 && padding_characters > 0) { |
599 | 0 | if (output_len > 0) { |
600 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
601 | 0 | dst += output_len; |
602 | 0 | } |
603 | 0 | return {INVALID_BASE64_CHARACTER, equallocation, |
604 | 0 | size_t(dst - dstinit), true}; |
605 | 0 | } else { |
606 | 0 | if (output_len > 0) { |
607 | 0 | _mm512_mask_storeu_epi8((__m512i *)dst, output_mask, shuffled); |
608 | 0 | dst += output_len; |
609 | 0 | } |
610 | 0 | } |
611 | 0 | } |
612 | 0 | if (!ignore_garbage && !is_partial(last_chunk_options) && |
613 | 0 | padding_characters > 0) { |
614 | 0 | size_t output_count = size_t(dst - dstinit); |
615 | 0 | if ((output_count % 3 == 0) || |
616 | 0 | ((output_count % 3) + 1 + padding_characters != 4)) { |
617 | 0 | return {INVALID_BASE64_CHARACTER, equallocation, output_count, true}; |
618 | 0 | } |
619 | 0 | } |
620 | 0 | return {SUCCESS, full_input_length, size_t(dst - dstinit)}; |
621 | 0 | } |
622 | | |
623 | 0 | if (!ignore_garbage && padding_characters > 0) { |
624 | 0 | if ((size_t(dst - dstinit) % 3 == 0) || |
625 | 0 | ((size_t(dst - dstinit) % 3) + 1 + padding_characters != 4)) { |
626 | 0 | return {INVALID_BASE64_CHARACTER, equallocation, size_t(dst - dstinit), |
627 | 0 | true}; |
628 | 0 | } |
629 | 0 | } |
630 | 0 | return {SUCCESS, srclen, size_t(dst - dstinit)}; |
631 | 0 | } Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, true, true, char>(char*, char const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, false, true, char>(char*, char const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<true, true, false, char>(char*, char const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<true, false, false, char>(char*, char const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, true, false, char>(char*, char const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, false, false, char>(char*, char const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, true, true, char16_t>(char*, char16_t const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, false, true, char16_t>(char*, char16_t const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<true, true, false, char16_t>(char*, char16_t const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<true, false, false, char16_t>(char*, char16_t const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, true, false, char16_t>(char*, char16_t const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) Unexecuted instantiation: simdutf.cpp:simdutf::full_result simdutf::icelake::(anonymous namespace)::compress_decode_base64<false, false, false, char16_t>(char*, char16_t const*, unsigned long, simdutf::base64_options, simdutf::last_chunk_handling_options) |
632 | | |
633 | | simdutf_warn_unused size_t icelake_binary_length_from_base64(const char *input, |
634 | 0 | size_t length) { |
635 | 0 | size_t count = 0; |
636 | 0 | const char *ptr = input; |
637 | 0 | const char *end = input + length; |
638 | |
|
639 | 0 | __m512i spaces = _mm512_set1_epi8(0x20); |
640 | 0 | while (ptr + 64 <= end) { |
641 | 0 | __m512i data = _mm512_loadu_si512(reinterpret_cast<const __m512i *>(ptr)); |
642 | 0 | uint64_t mask = _mm512_cmpgt_epi8_mask(data, spaces); |
643 | 0 | count += count_ones(mask); |
644 | 0 | ptr += 64; |
645 | 0 | } |
646 | |
|
647 | 0 | if (ptr < end) { |
648 | 0 | size_t len = end - ptr; |
649 | 0 | __mmask64 input_mask = ((__mmask64)1 << len) - 1; |
650 | 0 | __m512i data = _mm512_maskz_loadu_epi8( |
651 | 0 | input_mask, reinterpret_cast<const __m512i *>(ptr)); |
652 | 0 | uint64_t mask = _mm512_cmpgt_epi8_mask(data, spaces); |
653 | 0 | count += count_ones(mask); |
654 | 0 | ptr += len; |
655 | 0 | } |
656 | |
|
657 | 0 | size_t padding = 0; |
658 | 0 | size_t pos = length; |
659 | 0 | while (pos > 0 && padding < 2) { |
660 | 0 | char c = input[--pos]; |
661 | 0 | if (c == '=') { |
662 | 0 | padding++; |
663 | 0 | } else if (c > ' ') { |
664 | 0 | break; |
665 | 0 | } |
666 | 0 | } |
667 | 0 | return ((count - padding) * 3) / 4; |
668 | 0 | } |
669 | | |
670 | | simdutf_warn_unused size_t |
671 | 0 | icelake_binary_length_from_base64(const char16_t *input, size_t length) { |
672 | 0 | size_t count = 0; |
673 | 0 | const char16_t *ptr = input; |
674 | 0 | const char16_t *end = input + length; |
675 | |
|
676 | 0 | __m512i spaces = _mm512_set1_epi16(0x20); |
677 | 0 | while (ptr + 32 <= end) { |
678 | 0 | __m512i data = _mm512_loadu_si512(reinterpret_cast<const __m512i *>(ptr)); |
679 | 0 | __mmask32 mask = _mm512_cmpgt_epi16_mask(data, spaces); |
680 | 0 | count += _mm_popcnt_u32(mask); |
681 | 0 | ptr += 32; |
682 | 0 | } |
683 | |
|
684 | 0 | if (ptr < end) { |
685 | 0 | size_t len = end - ptr; |
686 | 0 | __mmask32 input_mask = ((__mmask32)1 << len) - 1; |
687 | 0 | __m512i data = _mm512_maskz_loadu_epi16( |
688 | 0 | input_mask, reinterpret_cast<const __m512i *>(ptr)); |
689 | 0 | uint32_t mask = _mm512_cmpgt_epi16_mask(data, spaces); |
690 | 0 | count += _mm_popcnt_u32(mask); |
691 | 0 | ptr += len; |
692 | 0 | } |
693 | |
|
694 | 0 | size_t padding = 0; |
695 | 0 | size_t pos = length; |
696 | 0 | while (pos > 0 && padding < 2) { |
697 | 0 | char16_t c = input[--pos]; |
698 | 0 | if (c == '=') { |
699 | 0 | padding++; |
700 | 0 | } else if (c > ' ') { |
701 | 0 | break; |
702 | 0 | } |
703 | 0 | } |
704 | 0 | return ((count - padding) * 3) / 4; |
705 | 0 | } |