From 676e1c7715563723a20bd92b642c3843401bed51 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 1 Oct 2026 00:57:48 +0800 Subject: [PATCH 1/4] ext/intl: Fix grapheme_strstr() missing canonically equivalent matches --- ext/intl/grapheme/grapheme_string.c | 17 ++++++------ .../grapheme_strstr_canonical_match.phpt | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 ext/intl/tests/grapheme_strstr_canonical_match.phpt diff --git a/ext/intl/grapheme/grapheme_string.c b/ext/intl/grapheme/grapheme_string.c index 6b418e65cff4..e02038bff605 100644 --- a/ext/intl/grapheme/grapheme_string.c +++ b/ext/intl/grapheme/grapheme_string.c @@ -550,7 +550,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 +562,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/tests/grapheme_strstr_canonical_match.phpt b/ext/intl/tests/grapheme_strstr_canonical_match.phpt new file mode 100644 index 000000000000..96d8f6334b1a --- /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-- + +--EXPECT-- +["e\u0301x",""] +["\u00e9x",""] +["e\u0301x","p"] +["e\u0301x\u00e9","p"] +["bc","a"] +[false,false] From b4e5cdbe8b276b1fd4f42be3e7592719c51f271e Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 1 Oct 2026 01:39:05 +0800 Subject: [PATCH 2/4] Fix edge case when we have an empty haystack --- ext/intl/grapheme/grapheme_string.c | 4 +++ .../tests/grapheme_strstr_empty_haystack.phpt | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 ext/intl/tests/grapheme_strstr_empty_haystack.phpt diff --git a/ext/intl/grapheme/grapheme_string.c b/ext/intl/grapheme/grapheme_string.c index e02038bff605..ff423efd5c5b 100644 --- a/ext/intl/grapheme/grapheme_string.c +++ b/ext/intl/grapheme/grapheme_string.c @@ -550,6 +550,10 @@ static void strstr_common_handler(INTERNAL_FUNCTION_PARAMETERS, int f_ignore_cas Z_PARAM_BOOL(part) ZEND_PARSE_PARAMETERS_END(); + if (!f_ignore_case && haystack_len == 0 && needle_len > 0) { + RETURN_FALSE; + } + if ( !f_ignore_case && grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0 && grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0 ) { diff --git a/ext/intl/tests/grapheme_strstr_empty_haystack.phpt b/ext/intl/tests/grapheme_strstr_empty_haystack.phpt new file mode 100644 index 000000000000..247199d625b7 --- /dev/null +++ b/ext/intl/tests/grapheme_strstr_empty_haystack.phpt @@ -0,0 +1,30 @@ +--TEST-- +grapheme_strstr() handles an empty haystack without ICU errors +--EXTENSIONS-- +intl +--INI-- +intl.error_level=2 +--FILE-- + +--EXPECT-- +[false,false,0] +[false,false,0] +[false,false,0] +[false,false,0] +["","",0] +[false,false,0] +[false,false,0] +[false,false,0] +[false,false,0] +["","",0] From df9cecf9433da8948cfedf9ce9854265aeee6104 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 1 Oct 2026 02:28:41 +0800 Subject: [PATCH 3/4] Also fix grapheme_strpos --- ext/intl/grapheme/grapheme_string.c | 4 +- .../tests/grapheme_strpos_ascii_haystack.phpt | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 ext/intl/tests/grapheme_strpos_ascii_haystack.phpt diff --git a/ext/intl/grapheme/grapheme_string.c b/ext/intl/grapheme/grapheme_string.c index ff423efd5c5b..8089e57cd0ed 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 (haystack_len == 0 || grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0) { + RETURN_FALSE; + } } /* do utf16 part of the strpos */ 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 000000000000..f54c369e2dbf --- /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-- +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) From 3c3eb561847c0550858c59c0e04107fd1b672790 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 1 Oct 2026 14:26:18 +0800 Subject: [PATCH 4/4] feedback --- ext/intl/grapheme/grapheme_string.c | 6 +- ext/intl/grapheme/grapheme_util.c | 3 + .../tests/grapheme_search_empty_haystack.phpt | 55 +++++++++++++++++++ .../tests/grapheme_strstr_empty_haystack.phpt | 30 ---------- 4 files changed, 59 insertions(+), 35 deletions(-) create mode 100644 ext/intl/tests/grapheme_search_empty_haystack.phpt delete mode 100644 ext/intl/tests/grapheme_strstr_empty_haystack.phpt diff --git a/ext/intl/grapheme/grapheme_string.c b/ext/intl/grapheme/grapheme_string.c index 8089e57cd0ed..6a9233b6b6a6 100644 --- a/ext/intl/grapheme/grapheme_string.c +++ b/ext/intl/grapheme/grapheme_string.c @@ -117,7 +117,7 @@ PHP_FUNCTION(grapheme_strpos) if (found) { RETURN_LONG(found - haystack); } - if (haystack_len == 0 || grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0) { + if (grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0) { RETURN_FALSE; } } @@ -552,10 +552,6 @@ static void strstr_common_handler(INTERNAL_FUNCTION_PARAMETERS, int f_ignore_cas Z_PARAM_BOOL(part) ZEND_PARSE_PARAMETERS_END(); - if (!f_ignore_case && haystack_len == 0 && needle_len > 0) { - RETURN_FALSE; - } - if ( !f_ignore_case && grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0 && grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0 ) { diff --git a/ext/intl/grapheme/grapheme_util.c b/ext/intl/grapheme/grapheme_util.c index f0c79785483f..f4b21904e6bc 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 000000000000..b8b36295fae6 --- /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-- + +--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_strstr_empty_haystack.phpt b/ext/intl/tests/grapheme_strstr_empty_haystack.phpt deleted file mode 100644 index 247199d625b7..000000000000 --- a/ext/intl/tests/grapheme_strstr_empty_haystack.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -grapheme_strstr() handles an empty haystack without ICU errors ---EXTENSIONS-- -intl ---INI-- -intl.error_level=2 ---FILE-- - ---EXPECT-- -[false,false,0] -[false,false,0] -[false,false,0] -[false,false,0] -["","",0] -[false,false,0] -[false,false,0] -[false,false,0] -[false,false,0] -["","",0]