Commit 16a62e36773 for php

commit 16a62e367736024be544219b77886af154350d09
Author: Weilin Du <weilindu@php.net>
Date:   Fri Oct 2 01:14:51 2026 +0800

    ext/intl: Fix grapheme_strstr() and grapheme_strpos() missing canonically equivalent matches (#24029)

diff --git a/ext/intl/grapheme/grapheme_string.c b/ext/intl/grapheme/grapheme_string.c
index 6b418e65cff..6a9233b6b6a 100644
--- a/ext/intl/grapheme/grapheme_string.c
+++ b/ext/intl/grapheme/grapheme_string.c
@@ -117,7 +117,9 @@ PHP_FUNCTION(grapheme_strpos)
 		if (found) {
 			RETURN_LONG(found - haystack);
 		}
-		RETURN_FALSE;
+		if (grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0) {
+			RETURN_FALSE;
+		}
 	}

 	/* do utf16 part of the strpos */
@@ -550,7 +552,9 @@ static void strstr_common_handler(INTERNAL_FUNCTION_PARAMETERS, int f_ignore_cas
 		Z_PARAM_BOOL(part)
 	ZEND_PARSE_PARAMETERS_END();

-	if ( !f_ignore_case ) {
+	if ( !f_ignore_case
+			&& grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0
+			&& grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0 ) {

 		/* ASCII optimization: quick check to see if the string might be there */
 		found = php_memnstr(haystack, needle, needle_len, haystack + haystack_len);
@@ -560,15 +564,12 @@ static void strstr_common_handler(INTERNAL_FUNCTION_PARAMETERS, int f_ignore_cas
 			RETURN_FALSE;
 		}

-		/* if it is there, and if the haystack is ascii, we are all done */
-		if ( grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0 ) {
-			size_t found_offset = found - haystack;
+		size_t found_offset = found - haystack;

-			if (part) {
-				RETURN_STRINGL(haystack, found_offset);
-			} else {
-				RETURN_STRINGL(found, haystack_len - found_offset);
-			}
+		if (part) {
+			RETURN_STRINGL(haystack, found_offset);
+		} else {
+			RETURN_STRINGL(found, haystack_len - found_offset);
 		}

 	}
diff --git a/ext/intl/grapheme/grapheme_util.c b/ext/intl/grapheme/grapheme_util.c
index 66c3876a5bd..dbe80d96276 100644
--- a/ext/intl/grapheme/grapheme_util.c
+++ b/ext/intl/grapheme/grapheme_util.c
@@ -106,6 +106,9 @@ int32_t grapheme_strpos_utf16(char *haystack, size_t haystack_len, char *needle,
 	if(puchar_pos) {
 		*puchar_pos = -1;
 	}
+	if (haystack_len == 0 && needle_len > 0) {
+		return -1;
+	}
 	/* convert the strings to UTF-16. */

 	status = U_ZERO_ERROR;
diff --git a/ext/intl/tests/grapheme_search_empty_haystack.phpt b/ext/intl/tests/grapheme_search_empty_haystack.phpt
new file mode 100644
index 00000000000..b8b36295fae
--- /dev/null
+++ b/ext/intl/tests/grapheme_search_empty_haystack.phpt
@@ -0,0 +1,55 @@
+--TEST--
+Grapheme searches handle an empty haystack without ICU errors
+--EXTENSIONS--
+intl
+--INI--
+intl.error_level=2
+--FILE--
+<?php
+foreach ([0, 1] as $useExceptions) {
+    ini_set('intl.use_exceptions', (string) $useExceptions);
+    foreach (['grapheme_strpos', 'grapheme_stripos', 'grapheme_strrpos',
+              'grapheme_strripos', 'grapheme_strstr', 'grapheme_stristr'] as $function) {
+        $results = [];
+        foreach (['a', "\u{00E9}", "e\u{0301}", "\r\n", ''] as $needle) {
+            if ($function === 'grapheme_strstr' || $function === 'grapheme_stristr') {
+                $results[] = [$function('', $needle), $function('', $needle, true)];
+            } else {
+                $results[] = $function('', $needle);
+            }
+        }
+        echo $function, ': ', json_encode($results), ', error: ', intl_get_error_code(), "\n";
+    }
+}
+
+foreach (['grapheme_strpos', 'grapheme_stripos', 'grapheme_strrpos', 'grapheme_strripos'] as $function) {
+    foreach ([-1, 1] as $offset) {
+        try {
+            $function('', "\u{00E9}", $offset);
+        } catch (ValueError $e) {
+            echo $function, '(', $offset, '): ', $e::class, "\n";
+        }
+    }
+}
+?>
+--EXPECT--
+grapheme_strpos: [false,false,false,false,0], error: 0
+grapheme_stripos: [false,false,false,false,0], error: 0
+grapheme_strrpos: [false,false,false,false,0], error: 0
+grapheme_strripos: [false,false,false,false,0], error: 0
+grapheme_strstr: [[false,false],[false,false],[false,false],[false,false],["",""]], error: 0
+grapheme_stristr: [[false,false],[false,false],[false,false],[false,false],["",""]], error: 0
+grapheme_strpos: [false,false,false,false,0], error: 0
+grapheme_stripos: [false,false,false,false,0], error: 0
+grapheme_strrpos: [false,false,false,false,0], error: 0
+grapheme_strripos: [false,false,false,false,0], error: 0
+grapheme_strstr: [[false,false],[false,false],[false,false],[false,false],["",""]], error: 0
+grapheme_stristr: [[false,false],[false,false],[false,false],[false,false],["",""]], error: 0
+grapheme_strpos(-1): ValueError
+grapheme_strpos(1): ValueError
+grapheme_stripos(-1): ValueError
+grapheme_stripos(1): ValueError
+grapheme_strrpos(-1): ValueError
+grapheme_strrpos(1): ValueError
+grapheme_strripos(-1): ValueError
+grapheme_strripos(1): ValueError
diff --git a/ext/intl/tests/grapheme_strpos_ascii_haystack.phpt b/ext/intl/tests/grapheme_strpos_ascii_haystack.phpt
new file mode 100644
index 00000000000..f54c369e2db
--- /dev/null
+++ b/ext/intl/tests/grapheme_strpos_ascii_haystack.phpt
@@ -0,0 +1,56 @@
+--TEST--
+grapheme_strpos() uses Unicode matching for non-ASCII needles in ASCII haystacks
+--EXTENSIONS--
+intl
+--INI--
+intl.use_exceptions=1
+intl.error_level=2
+--FILE--
+<?php
+foreach ([
+    ['K', "\u{212A}", 0],
+    ['aKbK', "\u{212A}", 0],
+    ['aKbK', "\u{212A}", 1],
+    ['aKbK', "\u{212A}", 2],
+    ['aKbK', "\u{212A}", 3],
+    ['aKbK', "\u{212A}", 4],
+    ['aKbK', "\u{212A}", -4],
+    ['aKbK', "\u{212A}", -2],
+    ['abc', "b\u{00AD}", 0],
+    ['abc', "\u{00E9}", 0],
+    ['abc', 'b', 1],
+    ['abc', 'b', 2],
+    ['', "\u{00E9}", 0],
+    ['', "e\u{0301}", 0],
+    ['', "\r\n", 0],
+    ['', '', 0],
+    ['abc', '', 3],
+] as [$haystack, $needle, $offset]) {
+    var_dump(grapheme_strpos($haystack, $needle, $offset));
+}
+
+try {
+    grapheme_strpos('', "\u{212A}", 1);
+} catch (ValueError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+int(0)
+int(1)
+int(1)
+int(3)
+int(3)
+bool(false)
+int(1)
+int(3)
+int(1)
+bool(false)
+int(1)
+bool(false)
+bool(false)
+bool(false)
+bool(false)
+int(0)
+int(3)
+grapheme_strpos(): Argument #3 ($offset) must be contained in argument #1 ($haystack)
diff --git a/ext/intl/tests/grapheme_strstr_canonical_match.phpt b/ext/intl/tests/grapheme_strstr_canonical_match.phpt
new file mode 100644
index 00000000000..96d8f6334b1
--- /dev/null
+++ b/ext/intl/tests/grapheme_strstr_canonical_match.phpt
@@ -0,0 +1,27 @@
+--TEST--
+grapheme_strstr() finds canonically equivalent strings with different byte representations
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+foreach ([
+    ["e\u{0301}x", "\u{00E9}"],
+    ["\u{00E9}x", "e\u{0301}"],
+    ["pe\u{0301}x", "\u{00E9}"],
+    ["pe\u{0301}x\u{00E9}", "\u{00E9}"],
+    ['abc', 'b'],
+    ['abc', 'z'],
+] as [$haystack, $needle]) {
+    echo json_encode([
+        grapheme_strstr($haystack, $needle),
+        grapheme_strstr($haystack, $needle, true),
+    ]), "\n";
+}
+?>
+--EXPECT--
+["e\u0301x",""]
+["\u00e9x",""]
+["e\u0301x","p"]
+["e\u0301x\u00e9","p"]
+["bc","a"]
+[false,false]