Skip to content

Commit be56f28

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. The "utf-8" entry of the charset map, which the walk can no longer reach, is removed.
1 parent c13fabf commit be56f28

6 files changed

Lines changed: 84 additions & 17 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ PHP NEWS
114114
elements. (mehmetcansahin)
115115
. Enforce max_filter_count: limit the number of filters that can be chained
116116
in a php://filter URL. (Sjoerd Langkemper)
117+
. Improved performance of resolving the charset of HTML functions,
118+
especially UTF-8. (Nicolas Grekas)
117119

118120
- URI:
119121
. Fix casing of enum cases in UriHostType and UrlHostType to match the RFC

‎UPGRADING‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ PHP 8.7 UPGRADE NOTES
100100
. Improved performance of str_rot13().
101101
. Improved performance of pack() for large strings using the a, A and Z
102102
formats.
103+
. Improved performance of htmlspecialchars(), htmlentities(),
104+
html_entity_decode() and get_html_translation_table(), especially with
105+
UTF-8.
103106

104107
- MBString:
105108
. Improved performance of mb_strlen for UTF-8 strings.

‎ext/standard/html.c‎

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

359+
/* {{{ charset_is_utf8
360+
* Returns whether a charset name is "utf-8", in any case */
361+
static zend_always_inline bool charset_is_utf8(const char *charset_hint)
362+
{
363+
return zend_tolower_ascii(charset_hint[0]) == 'u' && zend_tolower_ascii(charset_hint[1]) == 't'
364+
&& zend_tolower_ascii(charset_hint[2]) == 'f' && charset_hint[3] == '-' && charset_hint[4] == '8'
365+
&& charset_hint[5] == '\0';
366+
}
367+
/* }}} */
368+
369+
/* {{{ entity_charset lookup_charset
370+
* Returns the charset identifier of a non-empty charset name, or UTF-8 if it is not supported. */
371+
static zend_never_inline enum entity_charset lookup_charset(const char *charset_hint, bool quiet)
372+
{
373+
const unsigned char first = zend_tolower_ascii(charset_hint[0]);
374+
375+
/* now walk the charset map and look for the codeset (UTF-8 is resolved by the caller) */
376+
for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) {
377+
const char *codeset = charset_map[i].codeset;
378+
379+
if (zend_tolower_ascii(codeset[0]) != first) {
380+
continue;
381+
}
382+
/* compare up to the NUL of codeset: a mismatch stops the loop at the NUL of charset_hint at the latest */
383+
for (size_t j = 1; zend_tolower_ascii(charset_hint[j]) == zend_tolower_ascii(codeset[j]); j++) {
384+
if (codeset[j] == '\0') {
385+
return charset_map[i].charset;
386+
}
387+
}
388+
}
389+
390+
if (!quiet) {
391+
php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
392+
charset_hint);
393+
}
394+
395+
return cs_utf_8;
396+
}
397+
/* }}} */
398+
359399
/* {{{ entity_charset determine_charset
360400
* Returns the charset identifier based on an explicitly provided charset,
361401
* the internal_encoding and default_charset ini settings, or UTF-8 by default. */
362402
static enum entity_charset determine_charset(const char *charset_hint, bool quiet)
363403
{
364404
if (!charset_hint || !*charset_hint) {
365405
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-
}
406+
if (!charset_hint) {
407+
return cs_utf_8;
376408
}
409+
}
377410

378-
if (!quiet) {
379-
php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
380-
charset_hint);
381-
}
411+
/* UTF-8 is the most used charset, and it is not in the charset map */
412+
if (charset_is_utf8(charset_hint)) {
413+
return cs_utf_8;
382414
}
383415

384-
return cs_utf_8;
416+
return lookup_charset(charset_hint, quiet);
385417
}
386418
/* }}} */
387419

‎ext/standard/html_tables.h‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ static const struct {
4040
{ "ISO8859-1", sizeof("ISO8859-1")-1, cs_8859_1 },
4141
{ "ISO-8859-15", sizeof("ISO-8859-15")-1, cs_8859_15 },
4242
{ "ISO8859-15", sizeof("ISO8859-15")-1, cs_8859_15 },
43-
{ "utf-8", sizeof("utf-8")-1, cs_utf_8 },
4443
{ "cp1252", sizeof("cp1252")-1, cs_cp1252 },
4544
{ "Windows-1252", sizeof("Windows-1252")-1, cs_cp1252 },
4645
{ "1252", sizeof("1252")-1, cs_cp1252 },

‎ext/standard/html_tables/html_table_gen.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ enum entity_charset charset;
6060
{ "ISO8859-1", sizeof("ISO8859-1")-1, cs_8859_1 },
6161
{ "ISO-8859-15", sizeof("ISO-8859-15")-1, cs_8859_15 },
6262
{ "ISO8859-15", sizeof("ISO8859-15")-1, cs_8859_15 },
63-
{ "utf-8", sizeof("utf-8")-1, cs_utf_8 },
6463
{ "cp1252", sizeof("cp1252")-1, cs_cp1252 },
6564
{ "Windows-1252", sizeof("Windows-1252")-1, cs_cp1252 },
6665
{ "1252", sizeof("1252")-1, cs_cp1252 },
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)