From 29cc05fa5d589c548b82780f2ed1817e3a42e392 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Sep 2026 09:23:28 +0200 Subject: [PATCH] ext/standard: Return the input of htmlspecialchars() when nothing needs encoding htmlspecialchars() allocated a buffer of twice the input and decoded it character by character, even when the result was the input itself. The input is now scanned first for the characters that need encoding and for invalid multi-byte sequences, with SSE2 on 16-byte blocks. When there is none, the input string is returned, as html_entity_decode() and htmlspecialchars_decode() already do since 68dc7549988. Otherwise, encoding starts after the part that was scanned. The @refcount 1 annotations of htmlspecialchars() and htmlentities() are removed since they can now return their argument. --- NEWS | 2 + UPGRADING | 2 + Zend/Optimizer/zend_func_infos.h | 2 - ext/standard/basic_functions.stub.php | 2 - ext/standard/basic_functions_arginfo.h | 2 +- ext/standard/basic_functions_decl.h | 8 +- ext/standard/html.c | 325 +++++++++++++----- .../strings/htmlspecialchars_unchanged.phpt | 52 +++ 8 files changed, 300 insertions(+), 95 deletions(-) create mode 100644 ext/standard/tests/strings/htmlspecialchars_unchanged.phpt diff --git a/NEWS b/NEWS index a7f7caff7cee..d9c8f997f075 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 htmlspecialchars() when nothing needs encoding. + (Nicolas Grekas) - URI: . Fix casing of enum cases in UriHostType and UrlHostType to match the RFC diff --git a/UPGRADING b/UPGRADING index 46cf4cafab12..b648e47aa36f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -105,6 +105,8 @@ PHP 8.7 UPGRADE NOTES . Improved performance of str_rot13(). . Improved performance of pack() for large strings using the a, A and Z formats. + . htmlspecialchars() returns its input without copying it when nothing needs + encoding. - MBString: . Improved performance of mb_strlen for UTF-8 strings. diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 0add5a32f911..9e2c1b35aee4 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -463,8 +463,6 @@ static const func_info_t func_infos[] = { F1("inet_pton", MAY_BE_STRING|MAY_BE_FALSE), F1("metaphone", MAY_BE_STRING), F1("headers_list", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING), - F1("htmlspecialchars", MAY_BE_STRING), - F1("htmlentities", MAY_BE_STRING), F1("get_html_translation_table", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING), F1("bin2hex", MAY_BE_STRING), F1("hex2bin", MAY_BE_STRING|MAY_BE_FALSE), diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index e55993eeb23f..7292122abae4 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2273,14 +2273,12 @@ function headers_list(): array {} /* {{{ html.c */ -/** @refcount 1 */ function htmlspecialchars(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, ?string $encoding = null, bool $double_encode = true): string {} function htmlspecialchars_decode(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401): string {} function html_entity_decode(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, ?string $encoding = null): string {} -/** @refcount 1 */ function htmlentities(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, ?string $encoding = null, bool $double_encode = true): string {} /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index a4bab4cbbb9c..21a14c0bfedf 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: d09685377b0f033729c141c268c91cadaa587f7d + * Stub hash: 1e7c1c8937b439ecab1343a0e78a6092bcbbb0fc * Has decl header: yes */ #include "zend_attributes.h" diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index 5f1ea26850c8..e1f012c26370 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: d09685377b0f033729c141c268c91cadaa587f7d */ + * Stub hash: 1e7c1c8937b439ecab1343a0e78a6092bcbbb0fc */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_d09685377b0f033729c141c268c91cadaa587f7d_H -#define ZEND_BASIC_FUNCTIONS_DECL_d09685377b0f033729c141c268c91cadaa587f7d_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_1e7c1c8937b439ecab1343a0e78a6092bcbbb0fc_H +#define ZEND_BASIC_FUNCTIONS_DECL_1e7c1c8937b439ecab1343a0e78a6092bcbbb0fc_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_d09685377b0f033729c141c268c91cadaa587f7d_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_1e7c1c8937b439ecab1343a0e78a6092bcbbb0fc_H */ diff --git a/ext/standard/html.c b/ext/standard/html.c index 1e761c971eda..1ded9b000fd0 100644 --- a/ext/standard/html.c +++ b/ext/standard/html.c @@ -43,6 +43,8 @@ #include #include +#include "zend_bitset.h" +#include "zend_simd.h" #include "html_tables.h" /* Macro for disabling flag of translation of non-basic entities where this isn't supported. @@ -83,6 +85,88 @@ static char *get_default_charset(void) { } /* }}} */ +/* UTF-8 case of get_next_char(), for a cursor before the end of str and a status set to SUCCESS */ +static zend_always_inline unsigned int get_next_char_utf8( + const unsigned char *str, + size_t str_len, + size_t *cursor, + zend_result *status) +{ + size_t pos = *cursor; + unsigned int this_char = 0; + + /* We'll follow strategy 2. from section 3.6.1 of UTR #36: + * "In a reported illegal byte sequence, do not include any + * non-initial byte that encodes a valid character or is a leading + * byte for a valid sequence." */ + unsigned char c; + c = str[pos]; + if (c < 0x80) { + this_char = c; + pos++; + } else if (c < 0xc2) { + MB_FAILURE(pos, 1); + } else if (c < 0xe0) { + if (!CHECK_LEN(pos, 2)) + MB_FAILURE(pos, 1); + + if (!utf8_trail(str[pos + 1])) { + MB_FAILURE(pos, utf8_lead(str[pos + 1]) ? 1 : 2); + } + this_char = ((c & 0x1f) << 6) | (str[pos + 1] & 0x3f); + if (this_char < 0x80) { /* non-shortest form */ + MB_FAILURE(pos, 2); + } + pos += 2; + } else if (c < 0xf0) { + size_t avail = str_len - pos; + + if (avail < 3 || + !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2])) { + if (avail < 2 || utf8_lead(str[pos + 1])) + MB_FAILURE(pos, 1); + else if (avail < 3 || utf8_lead(str[pos + 2])) + MB_FAILURE(pos, 2); + else + MB_FAILURE(pos, 3); + } + + this_char = ((c & 0x0f) << 12) | ((str[pos + 1] & 0x3f) << 6) | (str[pos + 2] & 0x3f); + if (this_char < 0x800) { /* non-shortest form */ + MB_FAILURE(pos, 3); + } else if (this_char >= 0xd800 && this_char <= 0xdfff) { /* surrogate */ + MB_FAILURE(pos, 3); + } + pos += 3; + } else if (c < 0xf5) { + size_t avail = str_len - pos; + + if (avail < 4 || + !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2]) || + !utf8_trail(str[pos + 3])) { + if (avail < 2 || utf8_lead(str[pos + 1])) + MB_FAILURE(pos, 1); + else if (avail < 3 || utf8_lead(str[pos + 2])) + MB_FAILURE(pos, 2); + else if (avail < 4 || utf8_lead(str[pos + 3])) + MB_FAILURE(pos, 3); + else + MB_FAILURE(pos, 4); + } + + this_char = ((c & 0x07) << 18) | ((str[pos + 1] & 0x3f) << 12) | ((str[pos + 2] & 0x3f) << 6) | (str[pos + 3] & 0x3f); + if (this_char < 0x10000 || this_char > 0x10FFFF) { /* non-shortest form or outside range */ + MB_FAILURE(pos, 4); + } + pos += 4; + } else { + MB_FAILURE(pos, 1); + } + + *cursor = pos; + return this_char; +} + /* {{{ get_next_char */ static inline unsigned int get_next_char( enum entity_charset charset, @@ -102,76 +186,7 @@ static inline unsigned int get_next_char( switch (charset) { case cs_utf_8: - { - /* We'll follow strategy 2. from section 3.6.1 of UTR #36: - * "In a reported illegal byte sequence, do not include any - * non-initial byte that encodes a valid character or is a leading - * byte for a valid sequence." */ - unsigned char c; - c = str[pos]; - if (c < 0x80) { - this_char = c; - pos++; - } else if (c < 0xc2) { - MB_FAILURE(pos, 1); - } else if (c < 0xe0) { - if (!CHECK_LEN(pos, 2)) - MB_FAILURE(pos, 1); - - if (!utf8_trail(str[pos + 1])) { - MB_FAILURE(pos, utf8_lead(str[pos + 1]) ? 1 : 2); - } - this_char = ((c & 0x1f) << 6) | (str[pos + 1] & 0x3f); - if (this_char < 0x80) { /* non-shortest form */ - MB_FAILURE(pos, 2); - } - pos += 2; - } else if (c < 0xf0) { - size_t avail = str_len - pos; - - if (avail < 3 || - !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2])) { - if (avail < 2 || utf8_lead(str[pos + 1])) - MB_FAILURE(pos, 1); - else if (avail < 3 || utf8_lead(str[pos + 2])) - MB_FAILURE(pos, 2); - else - MB_FAILURE(pos, 3); - } - - this_char = ((c & 0x0f) << 12) | ((str[pos + 1] & 0x3f) << 6) | (str[pos + 2] & 0x3f); - if (this_char < 0x800) { /* non-shortest form */ - MB_FAILURE(pos, 3); - } else if (this_char >= 0xd800 && this_char <= 0xdfff) { /* surrogate */ - MB_FAILURE(pos, 3); - } - pos += 3; - } else if (c < 0xf5) { - size_t avail = str_len - pos; - - if (avail < 4 || - !utf8_trail(str[pos + 1]) || !utf8_trail(str[pos + 2]) || - !utf8_trail(str[pos + 3])) { - if (avail < 2 || utf8_lead(str[pos + 1])) - MB_FAILURE(pos, 1); - else if (avail < 3 || utf8_lead(str[pos + 2])) - MB_FAILURE(pos, 2); - else if (avail < 4 || utf8_lead(str[pos + 3])) - MB_FAILURE(pos, 3); - else - MB_FAILURE(pos, 4); - } - - this_char = ((c & 0x07) << 18) | ((str[pos + 1] & 0x3f) << 12) | ((str[pos + 2] & 0x3f) << 6) | (str[pos + 3] & 0x3f); - if (this_char < 0x10000 || this_char > 0x10FFFF) { /* non-shortest form or outside range */ - MB_FAILURE(pos, 4); - } - pos += 4; - } else { - MB_FAILURE(pos, 1); - } - } - break; + return get_next_char_utf8(str, str_len, cursor, status); case cs_big5: /* reference http://demo.icu-project.org/icu-bin/convexp?conv=big5 */ @@ -1125,12 +1140,127 @@ static inline void find_entity_for_char_basic( } /* }}} */ -/* {{{ php_escape_html_entities */ -PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t oldlen, int all, int flags, const char *hint_charset, bool double_encode, bool quiet) +/* Character classes used by html_verbatim_prefix_len(). The quote classes are the flags + * that make the quotes encoded. */ +#define HTML_CC_SQUOTE (1<<0) /* ' */ +#define HTML_CC_DQUOTE (1<<1) /* " */ +#define HTML_CC_BASIC (1<<2) /* &, < and >, always encoded */ +#define HTML_CC_NON_ASCII (1<<3) /* bytes 0x80-0xFF */ + +static_assert(ENT_HTML_QUOTE_SINGLE == HTML_CC_SQUOTE && ENT_HTML_QUOTE_DOUBLE == HTML_CC_DQUOTE, "the quote classes must match the quote flags"); + +#define N HTML_CC_NON_ASCII +static const unsigned char html_char_class[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, HTML_CC_DQUOTE, 0, 0, 0, HTML_CC_BASIC, HTML_CC_SQUOTE, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, HTML_CC_BASIC, 0, HTML_CC_BASIC, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, + N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, +}; +#undef N + +/* Moves *pos after the UTF-8 character at *pos, or returns false if it is invalid */ +static zend_always_inline bool html_skip_utf8_char(const unsigned char *str, size_t str_len, size_t *pos) +{ + size_t cursor = *pos; + zend_result status = SUCCESS; + + get_next_char_utf8(str, str_len, &cursor, &status); + if (status == FAILURE) { + return false; + } + *pos = cursor; + return true; +} + +#ifdef XSSE2 +/* Returns the bit mask of the bytes of in that html_verbatim_prefix_len() stops at. '<' and '>' + * differ only by bit 1, and '&' and '\'' only by bit 0, which apos_mask clears when '\'' is + * encoded. quot is '"', or '&' when '"' is not encoded, and high is 0x80, or 0 in single-byte + * charsets. */ +static zend_always_inline int html_stop_mask(__m128i in, __m128i apos_mask, __m128i quot, __m128i high) +{ + __m128i m = _mm_cmpeq_epi8(_mm_and_si128(in, _mm_set1_epi8(~2)), _mm_set1_epi8('<')); + m = _mm_or_si128(m, _mm_cmpeq_epi8(_mm_and_si128(in, apos_mask), _mm_set1_epi8('&'))); + m = _mm_or_si128(m, _mm_or_si128(_mm_cmpeq_epi8(in, quot), _mm_and_si128(in, high))); + return _mm_movemask_epi8(m); +} +#endif + +/* Returns the length of the longest prefix of old that escape_html_entities_from() copies + * unchanged when only the basic entities are encoded and disallowed characters are not + * substituted: the prefix contains no character to encode and no invalid multi-byte + * sequence, and ends on a character boundary. */ +static zend_always_inline size_t html_verbatim_prefix_len(const unsigned char *old, size_t oldlen, int flags, enum entity_charset charset) { - size_t cursor, maxlen, len; + /* In all supported charsets, bytes below 0x80 are single-byte characters. Bytes above + * 0x7F are characters of their own in single-byte charsets. In UTF-8 they are validated + * below, and the prefix stops at the first of them in the other multi-byte charsets. */ + const unsigned char stop = HTML_CC_BASIC + | (flags & (ENT_HTML_QUOTE_SINGLE | ENT_HTML_QUOTE_DOUBLE)) + | (CHARSET_SINGLE_BYTE(charset) ? 0 : HTML_CC_NON_ASCII); + size_t pos = 0; + +#ifdef XSSE2 + if (oldlen >= sizeof(__m128i)) { + /* Scan blocks of 16 bytes, the last one ending at the end of old */ + const __m128i apos_mask = (flags & ENT_HTML_QUOTE_SINGLE) ? _mm_set1_epi8(~1) : _mm_set1_epi8(~0); + const __m128i quot = (flags & ENT_HTML_QUOTE_DOUBLE) ? _mm_set1_epi8('"') : _mm_set1_epi8('&'); + const __m128i high = (stop & HTML_CC_NON_ASCII) ? _mm_set1_epi8((char) 0x80) : _mm_setzero_si128(); + const size_t last = oldlen - sizeof(__m128i); + + while (1) { + int mask; + + if (pos < last) { + mask = html_stop_mask(_mm_loadu_si128((const __m128i *) (old + pos)), apos_mask, quot, high); + if (!mask) { + pos += sizeof(__m128i); + continue; + } + } else { + /* the last block, whose bytes before pos are already scanned */ + mask = html_stop_mask(_mm_loadu_si128((const __m128i *) (old + last)), apos_mask, quot, high) >> (pos - last); + if (!mask) { + return oldlen; + } + } + pos += zend_ulong_ntz(mask); + if (old[pos] < 0x80 || charset != cs_utf_8 || !html_skip_utf8_char(old, oldlen, &pos)) { + return pos; + } + } + } +#endif + + while (pos < oldlen && !(html_char_class[old[pos]] & stop)) { + pos++; + } + while (pos < oldlen && old[pos] >= 0x80 && charset == cs_utf_8 && html_skip_utf8_char(old, oldlen, &pos)) { + while (pos < oldlen && !(html_char_class[old[pos]] & stop)) { + pos++; + } + } + + return pos; +} + +/* Encodes old, whose first cursor bytes are known to be copied unchanged */ +static zend_string *escape_html_entities_from(const unsigned char *old, size_t oldlen, size_t cursor, int all, int flags, enum entity_charset charset, bool double_encode) +{ + size_t maxlen, len; zend_string *replaced; - enum entity_charset charset = determine_charset(hint_charset, quiet); int doctype = flags & ENT_HTML_DOC_TYPE_MASK; entity_table_opt entity_table; const enc_to_uni *to_uni_table = NULL; @@ -1139,14 +1269,6 @@ PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t const unsigned char *replacement = NULL; size_t replacement_len = 0; - if (all) { /* replace with all named entities */ - if (!quiet && CHARSET_PARTIAL_SUPPORT(charset)) { - php_error_docref(NULL, E_NOTICE, "Only basic entities " - "substitution is supported for multi-byte encodings other than UTF-8; " - "functionality is equivalent to htmlspecialchars"); - } - LIMIT_ALL(all, doctype, charset); - } entity_table = determine_entity_table(all, doctype); if (all && !CHARSET_UNICODE_COMPAT(charset)) { to_uni_table = enc_to_uni_index[charset]; @@ -1176,8 +1298,8 @@ PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t } replaced = zend_string_alloc(maxlen, 0); - len = 0; - cursor = 0; + memcpy(ZSTR_VAL(replaced), old, cursor); + len = cursor; while (cursor < oldlen) { const unsigned char *mbsequence = NULL; size_t mbseqlen = 0, @@ -1336,6 +1458,37 @@ PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t return replaced; } + +/* {{{ php_escape_html_entities */ +static zend_always_inline zend_string *escape_html_entities(const unsigned char *old, size_t oldlen, zend_string *old_str, int all, int flags, const char *hint_charset, bool double_encode, bool quiet) +{ + enum entity_charset charset = determine_charset(hint_charset, quiet); + size_t verbatim_len = 0; + + if (all) { /* replace with all named entities */ + if (!quiet && CHARSET_PARTIAL_SUPPORT(charset)) { + php_error_docref(NULL, E_NOTICE, "Only basic entities " + "substitution is supported for multi-byte encodings other than UTF-8; " + "functionality is equivalent to htmlspecialchars"); + } + LIMIT_ALL(all, flags & ENT_HTML_DOC_TYPE_MASK, charset); + } + + if (!all && !(flags & ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS)) { + verbatim_len = html_verbatim_prefix_len(old, oldlen, flags, charset); + if (verbatim_len == oldlen) { + /* nothing to encode */ + return old_str ? zend_string_copy(old_str) : zend_string_init((const char *) old, oldlen, 0); + } + } + + return escape_html_entities_from(old, oldlen, verbatim_len, all, flags, charset, double_encode); +} + +PHPAPI zend_string *php_escape_html_entities_ex(const unsigned char *old, size_t oldlen, int all, int flags, const char *hint_charset, bool double_encode, bool quiet) +{ + return escape_html_entities(old, oldlen, NULL, all, flags, hint_charset, double_encode, quiet); +} /* }}} */ /* {{{ php_html_entities */ @@ -1357,8 +1510,8 @@ static void php_html_entities(INTERNAL_FUNCTION_PARAMETERS, int all) if (ZSTR_LEN(str) == 0) { RETURN_EMPTY_STRING(); } - replaced = php_escape_html_entities_ex( - (unsigned char*)ZSTR_VAL(str), ZSTR_LEN(str), all, (int) flags, + replaced = escape_html_entities( + (unsigned char*)ZSTR_VAL(str), ZSTR_LEN(str), str, all, (int) flags, hint_charset ? ZSTR_VAL(hint_charset) : NULL, double_encode, /* quiet */ 0); RETVAL_STR(replaced); } diff --git a/ext/standard/tests/strings/htmlspecialchars_unchanged.phpt b/ext/standard/tests/strings/htmlspecialchars_unchanged.phpt new file mode 100644 index 000000000000..383235dc31a1 --- /dev/null +++ b/ext/standard/tests/strings/htmlspecialchars_unchanged.phpt @@ -0,0 +1,52 @@ +--TEST-- +htmlspecialchars() returns the input string when there is nothing to encode +--FILE-- +'", ENT_QUOTES), ' ', htmlspecialchars("$p\"'", ENT_NOQUOTES), ' ', htmlspecialchars("$p\"'", ENT_COMPAT), "\n"; +} + +// invalid UTF-8 after valid multi-byte characters +$p = str_repeat("\u{E9}\u{20AC}\u{1F600}", 3); +var_dump(htmlspecialchars("$p\xED\xA0\x80<", ENT_QUOTES)); +var_dump(htmlspecialchars("$p\xED\xA0\x80<", ENT_QUOTES | ENT_IGNORE)); +var_dump(htmlspecialchars("$p\xF4\x90\x80\x80<", ENT_QUOTES | ENT_SUBSTITUTE)); + +// bytes above 0x7F in other charsets +var_dump(bin2hex(htmlspecialchars(str_repeat("\xE9", 20) . '<', ENT_QUOTES, 'ISO-8859-1'))); +var_dump(htmlspecialchars(str_repeat('a', 20) . "\x81<", ENT_QUOTES | ENT_SUBSTITUTE, 'Shift_JIS')); + +// double_encode = false +var_dump(htmlspecialchars(str_repeat('a', 20) . '&&foo '', ENT_QUOTES, 'UTF-8', false)); +?> +--EXPECT-- +string(30) "xbcabcabcabcabcabcabcabcabcabc" refcount(3) +string(5) "café" interned +string(28) ""it's" "it's" "it's" "it's" " refcount(3) +< >' "' "' +a< a>' a"' a"' +aaaaaaaaaaaaaaa< aaaaaaaaaaaaaaa>' aaaaaaaaaaaaaaa"' aaaaaaaaaaaaaaa"' +aaaaaaaaaaaaaaaa< aaaaaaaaaaaaaaaa>' aaaaaaaaaaaaaaaa"' aaaaaaaaaaaaaaaa"' +aaaaaaaaaaaaaaaaa< aaaaaaaaaaaaaaaaa>' aaaaaaaaaaaaaaaaa"' aaaaaaaaaaaaaaaaa"' +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa< aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa< aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa< aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +string(0) "" +string(31) "é€😀é€😀é€😀<" +string(34) "é€😀é€😀é€😀�<" +string(48) "e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9e9266c743b" +string(32) "aaaaaaaaaaaaaaaaaaaa�<" +string(39) "aaaaaaaaaaaaaaaaaaaa&&foo '"