diff --git a/NEWS b/NEWS index 93421d4bb4a3..9258c398f1e8 100644 --- a/NEWS +++ b/NEWS @@ -127,6 +127,8 @@ PHP NEWS (mehmetcansahin) . Improved performance of resolving the charset of HTML functions, especially UTF-8. (Nicolas Grekas) + . 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 5257f89ed872..ac335b4e5aad 100644 --- a/UPGRADING +++ b/UPGRADING @@ -115,6 +115,8 @@ PHP 8.7 UPGRADE NOTES . Improved performance of htmlspecialchars(), htmlentities(), html_entity_decode() and get_html_translation_table(), especially with UTF-8. + . 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 1381536dd3fb..075d629f10b6 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 */ @@ -1157,12 +1172,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; @@ -1171,14 +1301,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]; @@ -1208,8 +1330,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, @@ -1368,6 +1490,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 */ @@ -1389,8 +1542,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 '"