Skip to content

Commit 527f18f

Browse files
committed
ext/standard: Decode base64 four characters at a time in the character loop
The character loop of base64_decode() handled one character per iteration with a switch on its position in the group, and it is the whole decoder for input shorter than 45 bytes and for everything after the first whitespace or line break, where the vector loops stop. Decode whole groups of four alphabet characters at once while the input is clean, and keep the one-character step for whitespace, padding, invalid bytes and the last partial group. Output is unchanged. Short input runs with about 40-50% fewer instructions, base64 with MIME line breaks about 70% fewer.
1 parent 3b8e988 commit 527f18f

3 files changed

Lines changed: 180 additions & 40 deletions

File tree

‎UPGRADING‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ PHP 8.7 UPGRADE NOTES
174174
the last one.
175175

176176
- Standard:
177+
. Improved performance of base64_decode() on short input and on input with
178+
whitespace or line breaks: the character loop now decodes whole groups of
179+
four.
177180
. Improved performance of strcspn() and strpbrk() when searching longer strings
178181
with a single-byte character mask.
179182
. Improved performance of array_chunk() when not preserving keys.

‎ext/standard/base64.c‎

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,82 @@ static zend_always_inline size_t neon_base64_decode(const unsigned char *in, siz
245245
}
246246
#endif /* defined(__aarch64__) || defined(_M_ARM64) */
247247

248+
/* Decodes whole groups of four alphabet characters and stops before a group that holds
249+
* anything else; for a caller at a group boundary with no padding seen yet */
250+
static zend_always_inline void php_base64_decode_quads(const unsigned char **in, size_t *inl, unsigned char *out, size_t *outl)
251+
{
252+
const unsigned char *c = *in;
253+
unsigned char *o = out + *outl;
254+
size_t n = *inl;
255+
256+
while (n >= 4) {
257+
int a = base64_reverse_table[c[0]], b = base64_reverse_table[c[1]];
258+
int d = base64_reverse_table[c[2]], e = base64_reverse_table[c[3]];
259+
if ((a | b | d | e) < 0) {
260+
break;
261+
}
262+
uint32_t v = ((uint32_t) a << 18) | ((uint32_t) b << 12) | ((uint32_t) d << 6) | (uint32_t) e;
263+
o[0] = v >> 16;
264+
o[1] = v >> 8;
265+
o[2] = v;
266+
o += 3;
267+
c += 4;
268+
n -= 4;
269+
}
270+
*outl = o - out;
271+
*in = c;
272+
*inl = n;
273+
}
274+
275+
/* Decodes one character. Only i % 4 is read — the position in the current four-character group;
276+
* whole groups decoded by php_base64_decode_quads() keep it aligned. padding counts the '=' seen.
277+
* Returns false when strict mode rejects the character. */
278+
static zend_always_inline bool php_base64_decode_char(int ch, unsigned char *out, size_t *i, size_t *outl, size_t *padding, bool strict)
279+
{
280+
if (ch == base64_pad) {
281+
(*padding)++;
282+
return true;
283+
}
284+
285+
ch = base64_reverse_table[ch];
286+
if (!strict) {
287+
/* skip unknown characters and whitespace */
288+
if (ch < 0) {
289+
return true;
290+
}
291+
} else {
292+
/* skip whitespace */
293+
if (ch == -1) {
294+
return true;
295+
}
296+
/* fail on bad characters or if any data follows padding */
297+
if (ch == -2 || *padding) {
298+
return false;
299+
}
300+
}
301+
302+
switch (*i % 4) {
303+
case 0:
304+
out[*outl] = ch << 2;
305+
break;
306+
case 1:
307+
out[(*outl)++] |= ch >> 4;
308+
out[*outl] = (ch & 0x0f) << 4;
309+
break;
310+
case 2:
311+
out[(*outl)++] |= ch >>2;
312+
out[*outl] = (ch & 0x03) << 6;
313+
break;
314+
case 3:
315+
out[(*outl)++] |= ch;
316+
break;
317+
}
318+
(*i)++;
319+
return true;
320+
}
321+
248322
static zend_always_inline int php_base64_decode_impl(const unsigned char *in, size_t inl, unsigned char *out, size_t *outl, bool strict) /* {{{ */
249323
{
250-
int ch;
251324
size_t i = 0, padding = 0, j = *outl;
252325

253326
#if defined(__aarch64__) || defined(_M_ARM64)
@@ -260,48 +333,19 @@ static zend_always_inline int php_base64_decode_impl(const unsigned char *in, si
260333
}
261334
#endif
262335

263-
/* run through the whole string, converting as we go */
264-
while (inl-- > 0) {
265-
ch = *in++;
266-
if (ch == base64_pad) {
267-
padding++;
268-
continue;
269-
}
270-
271-
ch = base64_reverse_table[ch];
272-
if (!strict) {
273-
/* skip unknown characters and whitespace */
274-
if (ch < 0) {
275-
continue;
276-
}
277-
} else {
278-
/* skip whitespace */
279-
if (ch == -1) {
280-
continue;
281-
}
282-
/* fail on bad characters or if any data follows padding */
283-
if (ch == -2 || padding) {
284-
goto fail;
336+
/* whole groups of four while the input is clean; one character at a time around
337+
* whitespace, padding and invalid bytes, and for the last partial group */
338+
while (inl > 0) {
339+
if (i % 4 == 0 && !padding) {
340+
php_base64_decode_quads(&in, &inl, out, &j);
341+
if (!inl) {
342+
break;
285343
}
286344
}
287-
288-
switch (i % 4) {
289-
case 0:
290-
out[j] = ch << 2;
291-
break;
292-
case 1:
293-
out[j++] |= ch >> 4;
294-
out[j] = (ch & 0x0f) << 4;
295-
break;
296-
case 2:
297-
out[j++] |= ch >>2;
298-
out[j] = (ch & 0x03) << 6;
299-
break;
300-
case 3:
301-
out[j++] |= ch;
302-
break;
345+
if (!php_base64_decode_char(*in++, out, &i, &j, &padding, strict)) {
346+
goto fail;
303347
}
304-
i++;
348+
inl--;
305349
}
306350

307351
/* fail if the input is truncated (only one char in last group) */
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
--TEST--
2+
base64_decode() with whitespace, padding and invalid bytes inside the input, in both modes
3+
--FILE--
4+
<?php
5+
$clean = base64_encode("The quick brown fox jumps over the lazy dog, 0123456789.");
6+
7+
$inputs = [
8+
'clean' => $clean,
9+
'leading and trailing whitespace' => " \t\r\n" . $clean . "\n\n",
10+
'space inside a group' => substr($clean, 0, 2) . ' ' . substr($clean, 2),
11+
'space between groups' => substr($clean, 0, 4) . ' ' . substr($clean, 4),
12+
'a space after every character' => implode(' ', str_split($clean)),
13+
'MIME lines' => chunk_split($clean, 76, "\r\n"),
14+
'PEM lines' => chunk_split($clean, 64, "\n"),
15+
'short lines' => chunk_split($clean, 7, "\n"),
16+
'line break before the padding' => substr($clean, 0, -2) . "\n" . substr($clean, -2),
17+
'line break inside the padding' => substr($clean, 0, -1) . "\n" . substr($clean, -1),
18+
'line break after the padding' => $clean . "\n",
19+
'no padding' => rtrim($clean, '='),
20+
'no padding, trailing whitespace' => rtrim($clean, '=') . " \n",
21+
'data after the padding' => $clean . 'QUJD',
22+
'padding in the middle' => substr($clean, 0, 8) . '==' . substr($clean, 8),
23+
'invalid byte inside a group' => substr($clean, 0, 2) . '*' . substr($clean, 2),
24+
'invalid byte between groups' => substr($clean, 0, 4) . '-' . substr($clean, 4),
25+
'high byte' => substr($clean, 0, 4) . "\xC3\xA9" . substr($clean, 4),
26+
'one character' => 'Q',
27+
'two characters' => 'QU',
28+
'three characters' => 'QUJ',
29+
'five characters' => 'QUJDR',
30+
'only whitespace' => " \n\t",
31+
'only padding' => '====',
32+
'empty' => '',
33+
];
34+
35+
function show($result) {
36+
if ($result === false) {
37+
return 'false';
38+
}
39+
$plain = ' .,' . implode('', range('0', '9')) . implode('', range('a', 'z')) . implode('', range('A', 'Z'));
40+
return strspn($result, $plain) === strlen($result) ? "'$result'" : 'bytes ' . bin2hex($result);
41+
}
42+
43+
foreach ($inputs as $name => $input) {
44+
printf("%-34s %s | %s\n", $name, show(base64_decode($input)), show(base64_decode($input, true)));
45+
}
46+
47+
// every length around the group and vector block boundaries, with a line break at every position
48+
$ok = true;
49+
for ($bytes = 0; $bytes <= 100; $bytes++) {
50+
$raw = $bytes ? random_bytes($bytes) : "";
51+
$encoded = base64_encode($raw);
52+
for ($at = 0; $at <= strlen($encoded); $at++) {
53+
$broken = substr($encoded, 0, $at) . "\r\n" . substr($encoded, $at);
54+
if (base64_decode($broken) !== $raw || base64_decode($broken, true) !== $raw) {
55+
echo "mismatch: $bytes bytes, line break at $at\n";
56+
$ok = false;
57+
}
58+
}
59+
$mime = chunk_split($encoded, 76, "\r\n");
60+
if (base64_decode($mime) !== $raw || base64_decode($mime, true) !== $raw) {
61+
echo "mismatch: $bytes bytes, MIME\n";
62+
$ok = false;
63+
}
64+
}
65+
var_dump($ok);
66+
?>
67+
--EXPECT--
68+
clean 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
69+
leading and trailing whitespace 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
70+
space inside a group 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
71+
space between groups 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
72+
a space after every character 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
73+
MIME lines 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
74+
PEM lines 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
75+
short lines 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
76+
line break before the padding 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
77+
line break inside the padding 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
78+
line break after the padding 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
79+
no padding 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
80+
no padding, trailing whitespace 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
81+
data after the padding bytes 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f672c20303132333435363738392e105090 | false
82+
padding in the middle 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
83+
invalid byte inside a group 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
84+
invalid byte between groups 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
85+
high byte 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
86+
one character '' | false
87+
two characters 'A' | 'A'
88+
three characters 'AB' | 'AB'
89+
five characters 'ABC' | false
90+
only whitespace '' | ''
91+
only padding '' | false
92+
empty '' | ''
93+
bool(true)

0 commit comments

Comments
 (0)