From 1ffcb2ade9ecc3f059d948d39ecc194ff0106187 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Sep 2026 10:11:35 +0200 Subject: [PATCH] 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. The codesets of the charset map are now lowercase. The walk lowercases the hint once, then compares the length and the first byte before calling memcmp(), so other charsets resolve faster too. The "utf-8" entry of the charset map is removed, since the walk can no longer reach it. --- NEWS | 2 + UPGRADING | 3 + ext/standard/html.c | 62 ++++++++++++++----- ext/standard/html_tables.h | 41 ++++++------ ext/standard/html_tables/html_table_gen.php | 41 ++++++------ .../tests/strings/html_charset_lookup.phpt | 19 ++++++ .../tests/strings/html_charset_utf8.phpt | 32 ++++++++++ 7 files changed, 147 insertions(+), 53 deletions(-) create mode 100644 ext/standard/tests/strings/html_charset_lookup.phpt create mode 100644 ext/standard/tests/strings/html_charset_utf8.phpt diff --git a/NEWS b/NEWS index a7f7caff7cee..93421d4bb4a3 100644 --- a/NEWS +++ b/NEWS @@ -125,6 +125,8 @@ PHP NEWS in a php://filter URL. (Sjoerd Langkemper) . Improved performance of array_chunk() when not preserving keys. (mehmetcansahin) + . Improved performance of resolving the charset of HTML functions, + especially UTF-8. (Nicolas Grekas) - URI: . Fix casing of enum cases in UriHostType and UrlHostType to match the RFC diff --git a/UPGRADING b/UPGRADING index 46cf4cafab12..c9da085ae7b6 100644 --- a/UPGRADING +++ b/UPGRADING @@ -105,6 +105,9 @@ PHP 8.7 UPGRADE NOTES . Improved performance of str_rot13(). . Improved performance of pack() for large strings using the a, A and Z formats. + . Improved performance of htmlspecialchars(), htmlentities(), + html_entity_decode() and get_html_translation_table(), especially with + UTF-8. - MBString: . Improved performance of mb_strlen for UTF-8 strings. diff --git a/ext/standard/html.c b/ext/standard/html.c index 1e761c971eda..1381536dd3fb 100644 --- a/ext/standard/html.c +++ b/ext/standard/html.c @@ -356,6 +356,46 @@ PHPAPI unsigned int php_next_utf8_char( } /* }}} */ +/* Returns whether a charset name is "utf-8", in any case */ +static zend_always_inline bool charset_is_utf8(const char *charset_hint) +{ + return zend_tolower_ascii(charset_hint[0]) == 'u' && zend_tolower_ascii(charset_hint[1]) == 't' + && zend_tolower_ascii(charset_hint[2]) == 'f' && charset_hint[3] == '-' && charset_hint[4] == '8' + && charset_hint[5] == '\0'; +} + +/* Returns the charset identifier of a non-empty charset name, or UTF-8 if it is not supported. */ +static zend_never_inline enum entity_charset lookup_charset(const char *charset_hint, bool quiet) +{ + /* the codesets of the charset map are lowercase, so lowercase the hint once */ + char lowercase_hint[LONGEST_CODESET_LENGTH]; + size_t len; + + for (len = 0; charset_hint[len]; len++) { + if (len == LONGEST_CODESET_LENGTH) { + /* longer than any codeset */ + goto not_supported; + } + lowercase_hint[len] = zend_tolower_ascii(charset_hint[len]); + } + + /* now walk the charset map and look for the codeset (UTF-8 is resolved by the caller) */ + for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) { + if (charset_map[i].codeset_len == len && charset_map[i].codeset[0] == lowercase_hint[0] + && memcmp(lowercase_hint, charset_map[i].codeset, len) == 0) { + return charset_map[i].charset; + } + } + +not_supported: + if (!quiet) { + php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8", + charset_hint); + } + + return cs_utf_8; +} + /* {{{ entity_charset determine_charset * Returns the charset identifier based on an explicitly provided charset, * the internal_encoding and default_charset ini settings, or UTF-8 by default. */ @@ -363,25 +403,17 @@ static enum entity_charset determine_charset(const char *charset_hint, bool quie { if (!charset_hint || !*charset_hint) { charset_hint = get_default_charset(); - } - - if (charset_hint && *charset_hint) { - size_t len = strlen(charset_hint); - /* now walk the charset map and look for the codeset */ - for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) { - if (len == charset_map[i].codeset_len && - zend_binary_strcasecmp(charset_hint, len, charset_map[i].codeset, len) == 0) { - return charset_map[i].charset; - } + if (!charset_hint) { + return cs_utf_8; } + } - if (!quiet) { - php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8", - charset_hint); - } + /* UTF-8 is the most used charset, and it is not in the charset map */ + if (charset_is_utf8(charset_hint)) { + return cs_utf_8; } - return cs_utf_8; + return lookup_charset(charset_hint, quiet); } /* }}} */ diff --git a/ext/standard/html_tables.h b/ext/standard/html_tables.h index 16f30283fdd4..b01368e7eafb 100644 --- a/ext/standard/html_tables.h +++ b/ext/standard/html_tables.h @@ -31,46 +31,49 @@ enum entity_charset { cs_utf_8, cs_8859_1, cs_cp1252, cs_8859_15, cs_cp1251, #define CHARSET_SINGLE_BYTE(cs) ((cs) > cs_utf_8 && (cs) < cs_big5) #define CHARSET_PARTIAL_SUPPORT(cs) ((cs) >= cs_big5) +/* codesets must be lowercase, since lookup_charset() compares them with a lowercase copy of the hint */ static const struct { const char *codeset; uint32_t codeset_len; enum entity_charset charset; } charset_map[] = { - { "ISO-8859-1", sizeof("ISO-8859-1")-1, cs_8859_1 }, - { "ISO8859-1", sizeof("ISO8859-1")-1, cs_8859_1 }, - { "ISO-8859-15", sizeof("ISO-8859-15")-1, cs_8859_15 }, - { "ISO8859-15", sizeof("ISO8859-15")-1, cs_8859_15 }, - { "utf-8", sizeof("utf-8")-1, cs_utf_8 }, + { "iso-8859-1", sizeof("iso-8859-1")-1, cs_8859_1 }, + { "iso8859-1", sizeof("iso8859-1")-1, cs_8859_1 }, + { "iso-8859-15", sizeof("iso-8859-15")-1, cs_8859_15 }, + { "iso8859-15", sizeof("iso8859-15")-1, cs_8859_15 }, { "cp1252", sizeof("cp1252")-1, cs_cp1252 }, - { "Windows-1252", sizeof("Windows-1252")-1, cs_cp1252 }, + { "windows-1252", sizeof("windows-1252")-1, cs_cp1252 }, { "1252", sizeof("1252")-1, cs_cp1252 }, - { "BIG5", sizeof("BIG5")-1, cs_big5 }, + { "big5", sizeof("big5")-1, cs_big5 }, { "950", sizeof("950")-1, cs_big5 }, - { "GB2312", sizeof("GB2312")-1, cs_gb2312 }, + { "gb2312", sizeof("gb2312")-1, cs_gb2312 }, { "936", sizeof("936")-1, cs_gb2312 }, - { "BIG5-HKSCS", sizeof("BIG5-HKSCS")-1, cs_big5hkscs }, - { "Shift_JIS", sizeof("Shift_JIS")-1, cs_sjis }, - { "SJIS", sizeof("SJIS")-1, cs_sjis }, + { "big5-hkscs", sizeof("big5-hkscs")-1, cs_big5hkscs }, + { "shift_jis", sizeof("shift_jis")-1, cs_sjis }, + { "sjis", sizeof("sjis")-1, cs_sjis }, { "932", sizeof("932")-1, cs_sjis }, - { "SJIS-win", sizeof("SJIS-win")-1, cs_sjis }, - { "CP932", sizeof("CP932")-1, cs_sjis }, - { "EUCJP", sizeof("EUCJP")-1, cs_eucjp }, - { "EUC-JP", sizeof("EUC-JP")-1, cs_eucjp }, - { "eucJP-win", sizeof("eucJP-win")-1, cs_eucjp }, - { "KOI8-R", sizeof("KOI8-R")-1, cs_koi8r }, + { "sjis-win", sizeof("sjis-win")-1, cs_sjis }, + { "cp932", sizeof("cp932")-1, cs_sjis }, + { "eucjp", sizeof("eucjp")-1, cs_eucjp }, + { "euc-jp", sizeof("euc-jp")-1, cs_eucjp }, + { "eucjp-win", sizeof("eucjp-win")-1, cs_eucjp }, + { "koi8-r", sizeof("koi8-r")-1, cs_koi8r }, { "koi8-ru", sizeof("koi8-ru")-1, cs_koi8r }, { "koi8r", sizeof("koi8r")-1, cs_koi8r }, { "cp1251", sizeof("cp1251")-1, cs_cp1251 }, - { "Windows-1251", sizeof("Windows-1251")-1, cs_cp1251 }, + { "windows-1251", sizeof("windows-1251")-1, cs_cp1251 }, { "win-1251", sizeof("win-1251")-1, cs_cp1251 }, { "iso8859-5", sizeof("iso8859-5")-1, cs_8859_5 }, { "iso-8859-5", sizeof("iso-8859-5")-1, cs_8859_5 }, { "cp866", sizeof("cp866")-1, cs_cp866 }, { "866", sizeof("866")-1, cs_cp866 }, { "ibm866", sizeof("ibm866")-1, cs_cp866 }, - { "MacRoman", sizeof("MacRoman")-1, cs_macroman } + { "macroman", sizeof("macroman")-1, cs_macroman } }; +/* length of the longest codeset */ +#define LONGEST_CODESET_LENGTH 12 + /* longest entity name length excluding & and ; */ #define LONGEST_ENTITY_LENGTH 31 diff --git a/ext/standard/html_tables/html_table_gen.php b/ext/standard/html_tables/html_table_gen.php index a3b2d8807d70..6204493edeab 100755 --- a/ext/standard/html_tables/html_table_gen.php +++ b/ext/standard/html_tables/html_table_gen.php @@ -51,46 +51,49 @@ enum entity_charset { cs_utf_8, cs_8859_1, cs_cp1252, cs_8859_15, cs_cp1251, #define CHARSET_SINGLE_BYTE(cs) ((cs) > cs_utf_8 && (cs) < cs_big5) #define CHARSET_PARTIAL_SUPPORT(cs) ((cs) >= cs_big5) +/* codesets must be lowercase, since lookup_charset() compares them with a lowercase copy of the hint */ static const struct { const char *codeset; uint32_t codeset_len; enum entity_charset charset; } charset_map[] = { - { "ISO-8859-1", sizeof("ISO-8859-1")-1, cs_8859_1 }, - { "ISO8859-1", sizeof("ISO8859-1")-1, cs_8859_1 }, - { "ISO-8859-15", sizeof("ISO-8859-15")-1, cs_8859_15 }, - { "ISO8859-15", sizeof("ISO8859-15")-1, cs_8859_15 }, - { "utf-8", sizeof("utf-8")-1, cs_utf_8 }, + { "iso-8859-1", sizeof("iso-8859-1")-1, cs_8859_1 }, + { "iso8859-1", sizeof("iso8859-1")-1, cs_8859_1 }, + { "iso-8859-15", sizeof("iso-8859-15")-1, cs_8859_15 }, + { "iso8859-15", sizeof("iso8859-15")-1, cs_8859_15 }, { "cp1252", sizeof("cp1252")-1, cs_cp1252 }, - { "Windows-1252", sizeof("Windows-1252")-1, cs_cp1252 }, + { "windows-1252", sizeof("windows-1252")-1, cs_cp1252 }, { "1252", sizeof("1252")-1, cs_cp1252 }, - { "BIG5", sizeof("BIG5")-1, cs_big5 }, + { "big5", sizeof("big5")-1, cs_big5 }, { "950", sizeof("950")-1, cs_big5 }, - { "GB2312", sizeof("GB2312")-1, cs_gb2312 }, + { "gb2312", sizeof("gb2312")-1, cs_gb2312 }, { "936", sizeof("936")-1, cs_gb2312 }, - { "BIG5-HKSCS", sizeof("BIG5-HKSCS")-1, cs_big5hkscs }, - { "Shift_JIS", sizeof("Shift_JIS")-1, cs_sjis }, - { "SJIS", sizeof("SJIS")-1, cs_sjis }, + { "big5-hkscs", sizeof("big5-hkscs")-1, cs_big5hkscs }, + { "shift_jis", sizeof("shift_jis")-1, cs_sjis }, + { "sjis", sizeof("sjis")-1, cs_sjis }, { "932", sizeof("932")-1, cs_sjis }, - { "SJIS-win", sizeof("SJIS-win")-1, cs_sjis }, - { "CP932", sizeof("CP932")-1, cs_sjis }, - { "EUCJP", sizeof("EUCJP")-1, cs_eucjp }, - { "EUC-JP", sizeof("EUC-JP")-1, cs_eucjp }, - { "eucJP-win", sizeof("eucJP-win")-1, cs_eucjp }, - { "KOI8-R", sizeof("KOI8-R")-1, cs_koi8r }, + { "sjis-win", sizeof("sjis-win")-1, cs_sjis }, + { "cp932", sizeof("cp932")-1, cs_sjis }, + { "eucjp", sizeof("eucjp")-1, cs_eucjp }, + { "euc-jp", sizeof("euc-jp")-1, cs_eucjp }, + { "eucjp-win", sizeof("eucjp-win")-1, cs_eucjp }, + { "koi8-r", sizeof("koi8-r")-1, cs_koi8r }, { "koi8-ru", sizeof("koi8-ru")-1, cs_koi8r }, { "koi8r", sizeof("koi8r")-1, cs_koi8r }, { "cp1251", sizeof("cp1251")-1, cs_cp1251 }, - { "Windows-1251", sizeof("Windows-1251")-1, cs_cp1251 }, + { "windows-1251", sizeof("windows-1251")-1, cs_cp1251 }, { "win-1251", sizeof("win-1251")-1, cs_cp1251 }, { "iso8859-5", sizeof("iso8859-5")-1, cs_8859_5 }, { "iso-8859-5", sizeof("iso-8859-5")-1, cs_8859_5 }, { "cp866", sizeof("cp866")-1, cs_cp866 }, { "866", sizeof("866")-1, cs_cp866 }, { "ibm866", sizeof("ibm866")-1, cs_cp866 }, - { "MacRoman", sizeof("MacRoman")-1, cs_macroman } + { "macroman", sizeof("macroman")-1, cs_macroman } }; +/* length of the longest codeset */ +#define LONGEST_CODESET_LENGTH 12 + /* longest entity name length excluding & and ; */ #define LONGEST_ENTITY_LENGTH 31 diff --git a/ext/standard/tests/strings/html_charset_lookup.phpt b/ext/standard/tests/strings/html_charset_lookup.phpt new file mode 100644 index 000000000000..3de7ff7a3056 --- /dev/null +++ b/ext/standard/tests/strings/html_charset_lookup.phpt @@ -0,0 +1,19 @@ +--TEST-- +Charset names are case-insensitive, and names longer than the longest one are not supported +--FILE-- + +--EXPECTF-- +'WINDOWS-1252': e9 +'wInDoWs-1252': e9 +'MACROMAN': 8e + +Warning: html_entity_decode(): Charset "Windows-12520" is not supported, assuming UTF-8 in %s on line %d +'Windows-12520': c3a9 + +Warning: html_entity_decode(): Charset "MacRoman-but-longer" is not supported, assuming UTF-8 in %s on line %d +'MacRoman-but-longer': c3a9 diff --git a/ext/standard/tests/strings/html_charset_utf8.phpt b/ext/standard/tests/strings/html_charset_utf8.phpt new file mode 100644 index 000000000000..f67f6ff577d4 --- /dev/null +++ b/ext/standard/tests/strings/html_charset_utf8.phpt @@ -0,0 +1,32 @@ +--TEST-- +Charset names that are UTF-8 or close to it +--FILE-- + +--EXPECTF-- +'UTF-8': é< +'utf-8': é< +'uTf-8': é< + +Warning: htmlspecialchars(): Charset "UTF8" is not supported, assuming UTF-8 in %s on line %d +'UTF8': é< + +Warning: htmlspecialchars(): Charset "UTF-" is not supported, assuming UTF-8 in %s on line %d +'UTF-': é< + +Warning: htmlspecialchars(): Charset "UTF-8 " is not supported, assuming UTF-8 in %s on line %d +'UTF-8 ': é< + +Warning: htmlspecialchars(): Charset "UTF-88" is not supported, assuming UTF-8 in %s on line %d +'UTF-88': é< + +Warning: htmlspecialchars(): Charset "UTF_8" is not supported, assuming UTF-8 in %s on line %d +'UTF_8': é< +é<