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
      "&param=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;