Skip to content

Commit 4517a35

Browse files
ext/standard: Check for UTF-8 first when resolving the charset of html functions
determine_charset() ran strlen() and walked the charset map with case-insensitive compares on every call, while "UTF-8" (explicit or from default_charset) is what nearly every call asks for. It now checks for "utf-8" in any case first and moves the walk to a separate function, which skips strlen() and compares only the entries that start with the same letter as the hint, so other charsets resolve faster too.
1 parent e256041 commit 4517a35

4 files changed

Lines changed: 75 additions & 15 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ PHP NEWS
2121
- Standard:
2222
. Improved performance of array_splice() when inserting without removing
2323
elements. (mehmetcansahin)
24+
. Improved performance of HTML functions when the charset is UTF-8.
25+
(Nicolas Grekas)
2426

2527
<<< NOTE: Insert NEWS from last stable release here prior to actual release! >>>

‎UPGRADING‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ PHP 8.7 UPGRADE NOTES
7474
- Standard:
7575
. Improved performance of array_splice() when inserting without removing
7676
elements.
77+
. Improved performance of htmlspecialchars(), htmlentities(),
78+
html_entity_decode() and get_html_translation_table() with UTF-8.

‎ext/standard/html.c‎

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -356,32 +356,56 @@ PHPAPI unsigned int php_next_utf8_char(
356356
}
357357
/* }}} */
358358

359+
/* {{{ entity_charset lookup_charset
360+
* Returns the charset identifier of a non-empty charset name, or UTF-8 if it is not supported. */
361+
static zend_never_inline enum entity_charset lookup_charset(const char *charset_hint, bool quiet)
362+
{
363+
const unsigned char first = zend_tolower_ascii(charset_hint[0]);
364+
365+
/* now walk the charset map and look for the codeset */
366+
for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) {
367+
const char *codeset = charset_map[i].codeset;
368+
369+
if (zend_tolower_ascii(codeset[0]) != first) {
370+
continue;
371+
}
372+
/* compare up to the NUL of codeset: a mismatch stops the loop at the NUL of charset_hint at the latest */
373+
for (size_t j = 1; zend_tolower_ascii(charset_hint[j]) == zend_tolower_ascii(codeset[j]); j++) {
374+
if (codeset[j] == '\0') {
375+
return charset_map[i].charset;
376+
}
377+
}
378+
}
379+
380+
if (!quiet) {
381+
php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
382+
charset_hint);
383+
}
384+
385+
return cs_utf_8;
386+
}
387+
/* }}} */
388+
359389
/* {{{ entity_charset determine_charset
360390
* Returns the charset identifier based on an explicitly provided charset,
361391
* the internal_encoding and default_charset ini settings, or UTF-8 by default. */
362392
static enum entity_charset determine_charset(const char *charset_hint, bool quiet)
363393
{
364394
if (!charset_hint || !*charset_hint) {
365395
charset_hint = get_default_charset();
366-
}
367-
368-
if (charset_hint && *charset_hint) {
369-
size_t len = strlen(charset_hint);
370-
/* now walk the charset map and look for the codeset */
371-
for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) {
372-
if (len == charset_map[i].codeset_len &&
373-
zend_binary_strcasecmp(charset_hint, len, charset_map[i].codeset, len) == 0) {
374-
return charset_map[i].charset;
375-
}
396+
if (!charset_hint) {
397+
return cs_utf_8;
376398
}
399+
}
377400

378-
if (!quiet) {
379-
php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
380-
charset_hint);
381-
}
401+
/* Check the most used charset first, as lookup_charset() would: "utf-8" in any case */
402+
if (zend_tolower_ascii(charset_hint[0]) == 'u' && zend_tolower_ascii(charset_hint[1]) == 't'
403+
&& zend_tolower_ascii(charset_hint[2]) == 'f' && charset_hint[3] == '-' && charset_hint[4] == '8'
404+
&& charset_hint[5] == '\0') {
405+
return cs_utf_8;
382406
}
383407

384-
return cs_utf_8;
408+
return lookup_charset(charset_hint, quiet);
385409
}
386410
/* }}} */
387411

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Charset names that are UTF-8 or close to it
3+
--FILE--
4+
<?php
5+
foreach (['UTF-8', 'utf-8', 'uTf-8', 'UTF8', 'UTF-', 'UTF-8 ', 'UTF-88', 'UTF_8'] as $charset) {
6+
$escaped = htmlspecialchars("\xC3\xA9<", ENT_QUOTES, $charset);
7+
echo var_export($charset, true), ": $escaped\n";
8+
}
9+
10+
ini_set('default_charset', 'uTF-8');
11+
echo htmlspecialchars("\xC3\xA9<", ENT_QUOTES), "\n";
12+
?>
13+
--EXPECTF--
14+
'UTF-8': é&lt;
15+
'utf-8': é&lt;
16+
'uTf-8': é&lt;
17+
18+
Warning: htmlspecialchars(): Charset "UTF8" is not supported, assuming UTF-8 in %s on line %d
19+
'UTF8': é&lt;
20+
21+
Warning: htmlspecialchars(): Charset "UTF-" is not supported, assuming UTF-8 in %s on line %d
22+
'UTF-': é&lt;
23+
24+
Warning: htmlspecialchars(): Charset "UTF-8 " is not supported, assuming UTF-8 in %s on line %d
25+
'UTF-8 ': é&lt;
26+
27+
Warning: htmlspecialchars(): Charset "UTF-88" is not supported, assuming UTF-8 in %s on line %d
28+
'UTF-88': é&lt;
29+
30+
Warning: htmlspecialchars(): Charset "UTF_8" is not supported, assuming UTF-8 in %s on line %d
31+
'UTF_8': é&lt;
32+
é&lt;

0 commit comments

Comments
 (0)