Skip to content

Commit 1ffcb2a

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. 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.
1 parent c65321c commit 1ffcb2a

7 files changed

Lines changed: 147 additions & 53 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ PHP NEWS
125125
in a php://filter URL. (Sjoerd Langkemper)
126126
. Improved performance of array_chunk() when not preserving keys.
127127
(mehmetcansahin)
128+
. Improved performance of resolving the charset of HTML functions,
129+
especially UTF-8. (Nicolas Grekas)
128130

129131
- URI:
130132
. 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
@@ -105,6 +105,9 @@ PHP 8.7 UPGRADE NOTES
105105
. Improved performance of str_rot13().
106106
. Improved performance of pack() for large strings using the a, A and Z
107107
formats.
108+
. Improved performance of htmlspecialchars(), htmlentities(),
109+
html_entity_decode() and get_html_translation_table(), especially with
110+
UTF-8.
108111

109112
- MBString:
110113
. 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+
/* Returns whether a charset name is "utf-8", in any case */
360+
static zend_always_inline bool charset_is_utf8(const char *charset_hint)
361+
{
362+
return zend_tolower_ascii(charset_hint[0]) == 'u' && zend_tolower_ascii(charset_hint[1]) == 't'
363+
&& zend_tolower_ascii(charset_hint[2]) == 'f' && charset_hint[3] == '-' && charset_hint[4] == '8'
364+
&& charset_hint[5] == '\0';
365+
}
366+
367+
/* Returns the charset identifier of a non-empty charset name, or UTF-8 if it is not supported. */
368+
static zend_never_inline enum entity_charset lookup_charset(const char *charset_hint, bool quiet)
369+
{
370+
/* the codesets of the charset map are lowercase, so lowercase the hint once */
371+
char lowercase_hint[LONGEST_CODESET_LENGTH];
372+
size_t len;
373+
374+
for (len = 0; charset_hint[len]; len++) {
375+
if (len == LONGEST_CODESET_LENGTH) {
376+
/* longer than any codeset */
377+
goto not_supported;
378+
}
379+
lowercase_hint[len] = zend_tolower_ascii(charset_hint[len]);
380+
}
381+
382+
/* now walk the charset map and look for the codeset (UTF-8 is resolved by the caller) */
383+
for (size_t i = 0; i < sizeof(charset_map)/sizeof(charset_map[0]); i++) {
384+
if (charset_map[i].codeset_len == len && charset_map[i].codeset[0] == lowercase_hint[0]
385+
&& memcmp(lowercase_hint, charset_map[i].codeset, len) == 0) {
386+
return charset_map[i].charset;
387+
}
388+
}
389+
390+
not_supported:
391+
if (!quiet) {
392+
php_error_docref(NULL, E_WARNING, "Charset \"%s\" is not supported, assuming UTF-8",
393+
charset_hint);
394+
}
395+
396+
return cs_utf_8;
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: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,46 +31,49 @@ enum entity_charset { cs_utf_8, cs_8859_1, cs_cp1252, cs_8859_15, cs_cp1251,
3131
#define CHARSET_SINGLE_BYTE(cs) ((cs) > cs_utf_8 && (cs) < cs_big5)
3232
#define CHARSET_PARTIAL_SUPPORT(cs) ((cs) >= cs_big5)
3333

34+
/* codesets must be lowercase, since lookup_charset() compares them with a lowercase copy of the hint */
3435
static const struct {
3536
const char *codeset;
3637
uint32_t codeset_len;
3738
enum entity_charset charset;
3839
} charset_map[] = {
39-
{ "ISO-8859-1", sizeof("ISO-8859-1")-1, cs_8859_1 },
40-
{ "ISO8859-1", sizeof("ISO8859-1")-1, cs_8859_1 },
41-
{ "ISO-8859-15", sizeof("ISO-8859-15")-1, cs_8859_15 },
42-
{ "ISO8859-15", sizeof("ISO8859-15")-1, cs_8859_15 },
43-
{ "utf-8", sizeof("utf-8")-1, cs_utf_8 },
40+
{ "iso-8859-1", sizeof("iso-8859-1")-1, cs_8859_1 },
41+
{ "iso8859-1", sizeof("iso8859-1")-1, cs_8859_1 },
42+
{ "iso-8859-15", sizeof("iso-8859-15")-1, cs_8859_15 },
43+
{ "iso8859-15", sizeof("iso8859-15")-1, cs_8859_15 },
4444
{ "cp1252", sizeof("cp1252")-1, cs_cp1252 },
45-
{ "Windows-1252", sizeof("Windows-1252")-1, cs_cp1252 },
45+
{ "windows-1252", sizeof("windows-1252")-1, cs_cp1252 },
4646
{ "1252", sizeof("1252")-1, cs_cp1252 },
47-
{ "BIG5", sizeof("BIG5")-1, cs_big5 },
47+
{ "big5", sizeof("big5")-1, cs_big5 },
4848
{ "950", sizeof("950")-1, cs_big5 },
49-
{ "GB2312", sizeof("GB2312")-1, cs_gb2312 },
49+
{ "gb2312", sizeof("gb2312")-1, cs_gb2312 },
5050
{ "936", sizeof("936")-1, cs_gb2312 },
51-
{ "BIG5-HKSCS", sizeof("BIG5-HKSCS")-1, cs_big5hkscs },
52-
{ "Shift_JIS", sizeof("Shift_JIS")-1, cs_sjis },
53-
{ "SJIS", sizeof("SJIS")-1, cs_sjis },
51+
{ "big5-hkscs", sizeof("big5-hkscs")-1, cs_big5hkscs },
52+
{ "shift_jis", sizeof("shift_jis")-1, cs_sjis },
53+
{ "sjis", sizeof("sjis")-1, cs_sjis },
5454
{ "932", sizeof("932")-1, cs_sjis },
55-
{ "SJIS-win", sizeof("SJIS-win")-1, cs_sjis },
56-
{ "CP932", sizeof("CP932")-1, cs_sjis },
57-
{ "EUCJP", sizeof("EUCJP")-1, cs_eucjp },
58-
{ "EUC-JP", sizeof("EUC-JP")-1, cs_eucjp },
59-
{ "eucJP-win", sizeof("eucJP-win")-1, cs_eucjp },
60-
{ "KOI8-R", sizeof("KOI8-R")-1, cs_koi8r },
55+
{ "sjis-win", sizeof("sjis-win")-1, cs_sjis },
56+
{ "cp932", sizeof("cp932")-1, cs_sjis },
57+
{ "eucjp", sizeof("eucjp")-1, cs_eucjp },
58+
{ "euc-jp", sizeof("euc-jp")-1, cs_eucjp },
59+
{ "eucjp-win", sizeof("eucjp-win")-1, cs_eucjp },
60+
{ "koi8-r", sizeof("koi8-r")-1, cs_koi8r },
6161
{ "koi8-ru", sizeof("koi8-ru")-1, cs_koi8r },
6262
{ "koi8r", sizeof("koi8r")-1, cs_koi8r },
6363
{ "cp1251", sizeof("cp1251")-1, cs_cp1251 },
64-
{ "Windows-1251", sizeof("Windows-1251")-1, cs_cp1251 },
64+
{ "windows-1251", sizeof("windows-1251")-1, cs_cp1251 },
6565
{ "win-1251", sizeof("win-1251")-1, cs_cp1251 },
6666
{ "iso8859-5", sizeof("iso8859-5")-1, cs_8859_5 },
6767
{ "iso-8859-5", sizeof("iso-8859-5")-1, cs_8859_5 },
6868
{ "cp866", sizeof("cp866")-1, cs_cp866 },
6969
{ "866", sizeof("866")-1, cs_cp866 },
7070
{ "ibm866", sizeof("ibm866")-1, cs_cp866 },
71-
{ "MacRoman", sizeof("MacRoman")-1, cs_macroman }
71+
{ "macroman", sizeof("macroman")-1, cs_macroman }
7272
};
7373

74+
/* length of the longest codeset */
75+
#define LONGEST_CODESET_LENGTH 12
76+
7477
/* longest entity name length excluding & and ; */
7578
#define LONGEST_ENTITY_LENGTH 31
7679

‎ext/standard/html_tables/html_table_gen.php‎

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,46 +51,49 @@ enum entity_charset { cs_utf_8, cs_8859_1, cs_cp1252, cs_8859_15, cs_cp1251,
5151
#define CHARSET_SINGLE_BYTE(cs) ((cs) > cs_utf_8 && (cs) < cs_big5)
5252
#define CHARSET_PARTIAL_SUPPORT(cs) ((cs) >= cs_big5)
5353
54+
/* codesets must be lowercase, since lookup_charset() compares them with a lowercase copy of the hint */
5455
static const struct {
5556
const char *codeset;
5657
uint32_t codeset_len;
5758
enum entity_charset charset;
5859
} charset_map[] = {
59-
{ "ISO-8859-1", sizeof("ISO-8859-1")-1, cs_8859_1 },
60-
{ "ISO8859-1", sizeof("ISO8859-1")-1, cs_8859_1 },
61-
{ "ISO-8859-15", sizeof("ISO-8859-15")-1, cs_8859_15 },
62-
{ "ISO8859-15", sizeof("ISO8859-15")-1, cs_8859_15 },
63-
{ "utf-8", sizeof("utf-8")-1, cs_utf_8 },
60+
{ "iso-8859-1", sizeof("iso-8859-1")-1, cs_8859_1 },
61+
{ "iso8859-1", sizeof("iso8859-1")-1, cs_8859_1 },
62+
{ "iso-8859-15", sizeof("iso-8859-15")-1, cs_8859_15 },
63+
{ "iso8859-15", sizeof("iso8859-15")-1, cs_8859_15 },
6464
{ "cp1252", sizeof("cp1252")-1, cs_cp1252 },
65-
{ "Windows-1252", sizeof("Windows-1252")-1, cs_cp1252 },
65+
{ "windows-1252", sizeof("windows-1252")-1, cs_cp1252 },
6666
{ "1252", sizeof("1252")-1, cs_cp1252 },
67-
{ "BIG5", sizeof("BIG5")-1, cs_big5 },
67+
{ "big5", sizeof("big5")-1, cs_big5 },
6868
{ "950", sizeof("950")-1, cs_big5 },
69-
{ "GB2312", sizeof("GB2312")-1, cs_gb2312 },
69+
{ "gb2312", sizeof("gb2312")-1, cs_gb2312 },
7070
{ "936", sizeof("936")-1, cs_gb2312 },
71-
{ "BIG5-HKSCS", sizeof("BIG5-HKSCS")-1, cs_big5hkscs },
72-
{ "Shift_JIS", sizeof("Shift_JIS")-1, cs_sjis },
73-
{ "SJIS", sizeof("SJIS")-1, cs_sjis },
71+
{ "big5-hkscs", sizeof("big5-hkscs")-1, cs_big5hkscs },
72+
{ "shift_jis", sizeof("shift_jis")-1, cs_sjis },
73+
{ "sjis", sizeof("sjis")-1, cs_sjis },
7474
{ "932", sizeof("932")-1, cs_sjis },
75-
{ "SJIS-win", sizeof("SJIS-win")-1, cs_sjis },
76-
{ "CP932", sizeof("CP932")-1, cs_sjis },
77-
{ "EUCJP", sizeof("EUCJP")-1, cs_eucjp },
78-
{ "EUC-JP", sizeof("EUC-JP")-1, cs_eucjp },
79-
{ "eucJP-win", sizeof("eucJP-win")-1, cs_eucjp },
80-
{ "KOI8-R", sizeof("KOI8-R")-1, cs_koi8r },
75+
{ "sjis-win", sizeof("sjis-win")-1, cs_sjis },
76+
{ "cp932", sizeof("cp932")-1, cs_sjis },
77+
{ "eucjp", sizeof("eucjp")-1, cs_eucjp },
78+
{ "euc-jp", sizeof("euc-jp")-1, cs_eucjp },
79+
{ "eucjp-win", sizeof("eucjp-win")-1, cs_eucjp },
80+
{ "koi8-r", sizeof("koi8-r")-1, cs_koi8r },
8181
{ "koi8-ru", sizeof("koi8-ru")-1, cs_koi8r },
8282
{ "koi8r", sizeof("koi8r")-1, cs_koi8r },
8383
{ "cp1251", sizeof("cp1251")-1, cs_cp1251 },
84-
{ "Windows-1251", sizeof("Windows-1251")-1, cs_cp1251 },
84+
{ "windows-1251", sizeof("windows-1251")-1, cs_cp1251 },
8585
{ "win-1251", sizeof("win-1251")-1, cs_cp1251 },
8686
{ "iso8859-5", sizeof("iso8859-5")-1, cs_8859_5 },
8787
{ "iso-8859-5", sizeof("iso-8859-5")-1, cs_8859_5 },
8888
{ "cp866", sizeof("cp866")-1, cs_cp866 },
8989
{ "866", sizeof("866")-1, cs_cp866 },
9090
{ "ibm866", sizeof("ibm866")-1, cs_cp866 },
91-
{ "MacRoman", sizeof("MacRoman")-1, cs_macroman }
91+
{ "macroman", sizeof("macroman")-1, cs_macroman }
9292
};
9393
94+
/* length of the longest codeset */
95+
#define LONGEST_CODESET_LENGTH 12
96+
9497
/* longest entity name length excluding & and ; */
9598
#define LONGEST_ENTITY_LENGTH 31
9699
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
Charset names are case-insensitive, and names longer than the longest one are not supported
3+
--FILE--
4+
<?php
5+
foreach (['WINDOWS-1252', 'wInDoWs-1252', 'MACROMAN', 'Windows-12520', 'MacRoman-but-longer'] as $charset) {
6+
$decoded = html_entity_decode('&eacute;', ENT_QUOTES, $charset);
7+
echo var_export($charset, true), ': ', bin2hex($decoded), "\n";
8+
}
9+
?>
10+
--EXPECTF--
11+
'WINDOWS-1252': e9
12+
'wInDoWs-1252': e9
13+
'MACROMAN': 8e
14+
15+
Warning: html_entity_decode(): Charset "Windows-12520" is not supported, assuming UTF-8 in %s on line %d
16+
'Windows-12520': c3a9
17+
18+
Warning: html_entity_decode(): Charset "MacRoman-but-longer" is not supported, assuming UTF-8 in %s on line %d
19+
'MacRoman-but-longer': c3a9
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)