-
Notifications
You must be signed in to change notification settings - Fork 7.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimization for htmlspecialchars function #18126
base: master
Are you sure you want to change the base?
Conversation
A dedicated php_htmlspecialchars function instead of the “universal” php_escape_html_entities_ex. We work with ASCII-compatible encodings, we can employ byte-by-byte scanning and a lookup table to identify special characters. For c < 0x80, the lookup table is used; for potentially multi-byte characters, we continue to rely on get_next_char. This approach provides a noticeable performance improvement for ASCII strings and some improvement for multi-byte strings due to more optimized logic.
The new htmlspecialchars function respects the maximum entity size, defined as LONGEST_ENTITY_LENGTH. There is no strict limit on the length of a numeric entity in the HTML and XML specifications, but in practice the maximum possible is , which takes up 10 characters. Any numeric entities larger than this size are effectively invalid and will not be processed by browsers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't really gone through the logic yet, but there are already things that don't really make sense.
/* {{{ php_html_entities */ | ||
static void php_htmlspecialchars(INTERNAL_FUNCTION_PARAMETERS) | ||
{ | ||
zend_string *str, *hint_charset = NULL; | ||
zend_long flags = ENT_QUOTES|ENT_SUBSTITUTE; | ||
zend_string *replaced; | ||
bool double_encode = 1; | ||
|
||
ZEND_PARSE_PARAMETERS_START(1, 4) | ||
Z_PARAM_STR(str) | ||
Z_PARAM_OPTIONAL | ||
Z_PARAM_LONG(flags) | ||
Z_PARAM_STR_OR_NULL(hint_charset) | ||
Z_PARAM_BOOL(double_encode); | ||
ZEND_PARSE_PARAMETERS_END(); | ||
|
||
replaced = php_htmlspecialchars_ex( | ||
str, (int) flags, | ||
hint_charset ? ZSTR_VAL(hint_charset) : NULL, double_encode, /* quiet */ 0); | ||
RETVAL_STR(replaced); | ||
} | ||
/* }}} */ | ||
|
||
/* {{{ Convert special characters to HTML entities */ | ||
PHP_FUNCTION(htmlspecialchars) | ||
{ | ||
php_html_entities(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); | ||
php_htmlspecialchars(INTERNAL_FUNCTION_PARAM_PASSTHRU); | ||
} | ||
/* }}} */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you using a pass through function when there is only ever one use case now? Especially as you are adding a C function call overhead, which is weird for an optimization PR.
You should also "inline" the other usage of the php_html_entities
pass through function.
echo htmlspecialchars('"""""""""""""""""""""""""""""""""""""""""""""', | ||
echo htmlspecialchars('"""""""""""""""""""""""""""""""""""""""""""""�', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added support for the LONGEST_ENTITY_LENGTH constant to define the maximum length of an entity. While it originally applies to named entities, I think it also makes sense to use it to limit the length of numeric entities.
There’s no strict limit on numeric entity length in the HTML or XML specs, but in practice the longest valid one is 
, which is 10 characters long — so there’s no point in scanning the string beyond that when looking for a semicolon.
Any numeric entities longer than that are effectively invalid and won’t be processed by browsers anyway.
/* Lookup table for php_htmlspecialchars */ | ||
typedef struct { | ||
char* entity[256]; | ||
ushort entity_len[256]; | ||
} htmlspecialchars_lut; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this closer to the declaration of the function.
@@ -752,6 +758,60 @@ static zend_result resolve_named_entity_html(const char *start, size_t length, c | |||
} | |||
/* }}} */ | |||
|
|||
/* {{{ is_codepoint_allowed */ | |||
static inline zend_bool is_codepoint_allowed( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static inline zend_bool is_codepoint_allowed( | |
static bool is_codepoint_allowed( |
return unicode_cp_is_allowed(cp, doctype); | ||
} | ||
|
||
return 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return 1; | |
return true; |
size_t replacement_len = 0; | ||
if (flags & (ENT_HTML_SUBSTITUTE_ERRORS | ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS)) { | ||
if (charset == cs_utf_8) { | ||
replacement = (const unsigned char*)"\xEF\xBF\xBD"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the cast?
if (flags & (ENT_HTML_SUBSTITUTE_ERRORS | ENT_HTML_SUBSTITUTE_DISALLOWED_CHARS)) { | ||
if (charset == cs_utf_8) { | ||
replacement = (const unsigned char*)"\xEF\xBF\xBD"; | ||
replacement_len = sizeof("\xEF\xBF\xBD") - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replacement_len = sizeof("\xEF\xBF\xBD") - 1; | |
replacement_len = strlen("\xEF\xBF\xBD"); |
replacement = (const unsigned char*)"�"; | ||
replacement_len = sizeof("�") - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto prior remarks
/* Lookup table for php_htmlspecialchars */ | ||
typedef struct { | ||
char* entity[256]; | ||
ushort entity_len[256]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ushort
is not standard.
ushort entity_len[256]; | |
uint8_t entity_len[256]; |
/* {{{ php_htmlspecialchars */ | ||
PHPAPI zend_string* php_htmlspecialchars_ex( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this a PHPAPI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review! I’ll go through all the comments and make the necessary changes. I don’t have much experience contributing to public PHP projects yet, so I might not be fully aware of all the conventions and best practices.
I was working off the existing code and reused some parts as-is. I plan to refactor php_html_entities later. The main goal here was to switch from a hashtable to a LUT for special character replacement, and to restructure the logic accordingly.
Optimization for
htmlspecialchars
function.A dedicated
php_htmlspecialchars
function instead of the “universal”php_escape_html_entities_ex
.We work with ASCII-compatible encodings, we can employ byte-by-byte scanning and a lookup table to identify special characters. For
c < 0x80
, the lookup table is used; for potentially multi-byte characters, we continue to rely onget_next_char
.This approach provides a noticeable performance improvement for ASCII strings and some improvement for multi-byte strings due to more optimized logic.
Important!
We have changed the entity validation logic so that the maximum length of an entity name can no longer exceed
LONGEST_ENTITY_LENGTH
.Benchmarks 4k char strings:
htmlspecialchars
withUTF-8
encodinghtmlspecialchars
with lang-specific encoding. Without encoding hinthtmlspecialchars
with lang-specific encoding. With encoding hintWe may need more benchmarks.
This is not the final optimization, as
get_next_char
remains suboptimal for this function due to extra character-detection steps that aren’t required under the default flags.