Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ PHP 8.7 UPGRADE NOTES
the last one.

- Standard:
. Improved performance of base64_decode() on short input and on input with
whitespace or line breaks: the character loop now decodes whole groups of
four.
. Improved performance of strcspn() and strpbrk() when searching longer strings
with a single-byte character mask.
. Improved performance of array_chunk() when not preserving keys.
Expand Down
124 changes: 84 additions & 40 deletions ext/standard/base64.c
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,82 @@ static zend_always_inline size_t neon_base64_decode(const unsigned char *in, siz
}
#endif /* defined(__aarch64__) || defined(_M_ARM64) */

/* Decodes whole groups of four alphabet characters and stops before a group that holds
* anything else; for a caller at a group boundary with no padding seen yet */
static zend_always_inline void php_base64_decode_quads(const unsigned char **in, size_t *inl, unsigned char *out, size_t *outl)
{
const unsigned char *c = *in;
unsigned char *o = out + *outl;
size_t n = *inl;

while (n >= 4) {
int a = base64_reverse_table[c[0]], b = base64_reverse_table[c[1]];
int d = base64_reverse_table[c[2]], e = base64_reverse_table[c[3]];
if ((a | b | d | e) < 0) {
break;
}
uint32_t v = ((uint32_t) a << 18) | ((uint32_t) b << 12) | ((uint32_t) d << 6) | (uint32_t) e;
o[0] = v >> 16;
o[1] = v >> 8;
o[2] = v;
o += 3;
c += 4;
n -= 4;
}
*outl = o - out;
*in = c;
*inl = n;
}

/* Decodes one character. Only i % 4 is read — the position in the current four-character group;
* whole groups decoded by php_base64_decode_quads() keep it aligned. padding counts the '=' seen.
* Returns false when strict mode rejects the character. */
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)
{
if (ch == base64_pad) {
(*padding)++;
return true;
}

ch = base64_reverse_table[ch];
if (!strict) {
/* skip unknown characters and whitespace */
if (ch < 0) {
return true;
}
} else {
/* skip whitespace */
if (ch == -1) {
return true;
}
/* fail on bad characters or if any data follows padding */
if (ch == -2 || *padding) {
return false;
}
}

switch (*i % 4) {
case 0:
out[*outl] = ch << 2;
break;
case 1:
out[(*outl)++] |= ch >> 4;
out[*outl] = (ch & 0x0f) << 4;
break;
case 2:
out[(*outl)++] |= ch >>2;
out[*outl] = (ch & 0x03) << 6;
break;
case 3:
out[(*outl)++] |= ch;
break;
}
(*i)++;
return true;
}

static zend_always_inline int php_base64_decode_impl(const unsigned char *in, size_t inl, unsigned char *out, size_t *outl, bool strict) /* {{{ */
{
int ch;
size_t i = 0, padding = 0, j = *outl;

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

/* run through the whole string, converting as we go */
while (inl-- > 0) {
ch = *in++;
if (ch == base64_pad) {
padding++;
continue;
}

ch = base64_reverse_table[ch];
if (!strict) {
/* skip unknown characters and whitespace */
if (ch < 0) {
continue;
}
} else {
/* skip whitespace */
if (ch == -1) {
continue;
}
/* fail on bad characters or if any data follows padding */
if (ch == -2 || padding) {
goto fail;
/* whole groups of four while the input is clean; one character at a time around
* whitespace, padding and invalid bytes, and for the last partial group */
while (inl > 0) {
if (i % 4 == 0 && !padding) {
php_base64_decode_quads(&in, &inl, out, &j);
if (!inl) {
break;
}
}

switch (i % 4) {
case 0:
out[j] = ch << 2;
break;
case 1:
out[j++] |= ch >> 4;
out[j] = (ch & 0x0f) << 4;
break;
case 2:
out[j++] |= ch >>2;
out[j] = (ch & 0x03) << 6;
break;
case 3:
out[j++] |= ch;
break;
if (!php_base64_decode_char(*in++, out, &i, &j, &padding, strict)) {
goto fail;
}
i++;
inl--;
}

/* fail if the input is truncated (only one char in last group) */
Expand Down
93 changes: 93 additions & 0 deletions ext/standard/tests/url/base64_decode_whitespace.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
--TEST--
base64_decode() with whitespace, padding and invalid bytes inside the input, in both modes
--FILE--
<?php
$clean = base64_encode("The quick brown fox jumps over the lazy dog, 0123456789.");

$inputs = [
'clean' => $clean,
'leading and trailing whitespace' => " \t\r\n" . $clean . "\n\n",
'space inside a group' => substr($clean, 0, 2) . ' ' . substr($clean, 2),
'space between groups' => substr($clean, 0, 4) . ' ' . substr($clean, 4),
'a space after every character' => implode(' ', str_split($clean)),
'MIME lines' => chunk_split($clean, 76, "\r\n"),
'PEM lines' => chunk_split($clean, 64, "\n"),
'short lines' => chunk_split($clean, 7, "\n"),
'line break before the padding' => substr($clean, 0, -2) . "\n" . substr($clean, -2),
'line break inside the padding' => substr($clean, 0, -1) . "\n" . substr($clean, -1),
'line break after the padding' => $clean . "\n",
'no padding' => rtrim($clean, '='),
'no padding, trailing whitespace' => rtrim($clean, '=') . " \n",
'data after the padding' => $clean . 'QUJD',
'padding in the middle' => substr($clean, 0, 8) . '==' . substr($clean, 8),
'invalid byte inside a group' => substr($clean, 0, 2) . '*' . substr($clean, 2),
'invalid byte between groups' => substr($clean, 0, 4) . '-' . substr($clean, 4),
'high byte' => substr($clean, 0, 4) . "\xC3\xA9" . substr($clean, 4),
'one character' => 'Q',
'two characters' => 'QU',
'three characters' => 'QUJ',
'five characters' => 'QUJDR',
'only whitespace' => " \n\t",
'only padding' => '====',
'empty' => '',
];

function show($result) {
if ($result === false) {
return 'false';
}
$plain = ' .,' . implode('', range('0', '9')) . implode('', range('a', 'z')) . implode('', range('A', 'Z'));
return strspn($result, $plain) === strlen($result) ? "'$result'" : 'bytes ' . bin2hex($result);
}

foreach ($inputs as $name => $input) {
printf("%-34s %s | %s\n", $name, show(base64_decode($input)), show(base64_decode($input, true)));
}

// every length around the group and vector block boundaries, with a line break at every position
$ok = true;
for ($bytes = 0; $bytes <= 100; $bytes++) {
$raw = $bytes ? random_bytes($bytes) : "";
$encoded = base64_encode($raw);
for ($at = 0; $at <= strlen($encoded); $at++) {
$broken = substr($encoded, 0, $at) . "\r\n" . substr($encoded, $at);
if (base64_decode($broken) !== $raw || base64_decode($broken, true) !== $raw) {
echo "mismatch: $bytes bytes, line break at $at\n";
$ok = false;
}
}
$mime = chunk_split($encoded, 76, "\r\n");
if (base64_decode($mime) !== $raw || base64_decode($mime, true) !== $raw) {
echo "mismatch: $bytes bytes, MIME\n";
$ok = false;
}
}
var_dump($ok);
?>
--EXPECT--
clean 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
leading and trailing whitespace 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
space inside a group 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
space between groups 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
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.'
MIME lines 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
PEM lines 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
short lines 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
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.'
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.'
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.'
no padding 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
no padding, trailing whitespace 'The quick brown fox jumps over the lazy dog, 0123456789.' | 'The quick brown fox jumps over the lazy dog, 0123456789.'
data after the padding bytes 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f672c20303132333435363738392e105090 | false
padding in the middle 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
invalid byte inside a group 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
invalid byte between groups 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
high byte 'The quick brown fox jumps over the lazy dog, 0123456789.' | false
one character '' | false
two characters 'A' | 'A'
three characters 'AB' | 'AB'
five characters 'ABC' | false
only whitespace '' | ''
only padding '' | false
empty '' | ''
bool(true)
Loading