Commit 219289d8e79 for php
commit 219289d8e79e870b4bf816901117e2ce481a433e
Author: Artem Ukrainskiy <art.ukrainskiy@gmail.com>
Date: Mon Oct 26 15:25:18 2026 +0300
ext/standard: Stop at the first non-name byte in html_entity_decode() (#24142)
Entity names are letters and digits. Scan them and stop at the first other
byte instead of searching for a ';' up to 32 bytes ahead and hashing whatever
is in between. Also skip memchr() when the next byte is already '&'.
Output is unchanged. html_entity_decode() on 4 KB inputs:
only '&' 3.5x faster
only entities 1.1x faster
jQuery source 1.1-1.4x faster
jQuery minified 1.4-1.7x faster
HTML pages, Markdown same
"¶m=value" URLs in text up to 1.07x slower
diff --git a/ext/standard/html.c b/ext/standard/html.c
index 4631b980a0b..41ea6aef492 100644
--- a/ext/standard/html.c
+++ b/ext/standard/html.c
@@ -845,6 +845,12 @@ static inline size_t write_octet_sequence(unsigned char *buf, enum entity_charse
}
}
+/* [0-9A-Za-z], independent of the locale */
+static zend_always_inline bool is_entity_name_byte(unsigned char c)
+{
+ return (unsigned char)((c | 0x20) - 'a') < 26 || (unsigned char)(c - '0') < 10;
+}
+
/* {{{ traverse_for_entities
* Auxiliary function to php_unescape_html_entities().
* - The argument "all" determines if all numeric entities are decode or only those
@@ -867,7 +873,8 @@ static void traverse_for_entities(
const int doctype = flags & ENT_HTML_DOC_TYPE_MASK;
while (current_ptr < input_end) {
- const char *ampersand_ptr = memchr(current_ptr, '&', input_end - current_ptr);
+ /* Runs of '&' would call memchr() for every byte */
+ const char *ampersand_ptr = *current_ptr == '&' ? current_ptr : memchr(current_ptr, '&', input_end - current_ptr);
if (!ampersand_ptr) {
const size_t tail_len = input_end - current_ptr;
if (tail_len > 0) {
@@ -921,10 +928,13 @@ static void traverse_for_entities(
} else {
/* Processing named entity */
const char *name_start = current_ptr + 1;
- /* Search for ';' */
- const size_t max_search_len = MIN(LONGEST_ENTITY_LENGTH + 1, input_end - name_start);
- const char *semi_colon_ptr = memchr(name_start, ';', max_search_len);
- if (!semi_colon_ptr) {
+ const char *semi_colon_ptr = name_start;
+ const char *const name_limit = name_start + MIN(LONGEST_ENTITY_LENGTH, input_end - name_start - 1);
+ /* A name is letters and digits only, so the first other byte must be the ';' */
+ while (semi_colon_ptr < name_limit && is_entity_name_byte(*semi_colon_ptr)) {
+ semi_colon_ptr++;
+ }
+ if (*semi_colon_ptr != ';') {
goto invalid_incomplete_entity;
} else {
const size_t name_len = semi_colon_ptr - name_start;