From 37a5e5e092a208d594904cfe61d38b4650b98595 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Thu, 2 Jul 2026 16:52:20 +0330 Subject: [PATCH 01/35] Create grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 1 + 1 file changed, 1 insertion(+) create mode 100644 ext/intl/grapheme/grapheme_mask.c diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/ext/intl/grapheme/grapheme_mask.c @@ -0,0 +1 @@ + From 1938a5226a7b55155e823d5574fc5fcf124bac68 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Thu, 2 Jul 2026 22:09:40 +0330 Subject: [PATCH 02/35] Update grapheme_mask.c From fecd4a9377bddb3cae43d6ab2730b66e61f5544f Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Thu, 2 Jul 2026 22:15:55 +0330 Subject: [PATCH 03/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 167 ++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index 8b137891791f..338b431ea0b0 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -1 +1,168 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + +----------------------------------------------------------------------+ + | Author: Sepehr Mahmoudi | + +----------------------------------------------------------------------+ +*/ +#ifdef HAVE_CONFIG_H +# include +#endif + +#include "php_intl.h" +#include "grapheme_util.h" +#include "zend_exceptions.h" +#include +#include +#include + +/* {{{ proto string|false grapheme_mask(string $string, string $mask_char, int $offset = 0, ?int $length = null) + Masks a portion of a string respecting grapheme cluster boundaries */ +PHP_FUNCTION(grapheme_mask) +{ + zend_string *str, *mask_char; + zend_long offset = 0; + zend_long length = 0; + bool length_is_null = 1; + + /* 1. Parse Parameters */ + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STR(str) + Z_PARAM_STR(mask_char) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + Z_PARAM_LONG_OR_NULL(length, length_is_null) + ZEND_PARSE_PARAMETERS_END(); + + /* 2. Validate Empty Mask -> Throw ValueError (PHP 8 style) */ + if (ZSTR_LEN(mask_char) == 0) { + zend_value_error("grapheme_mask(): Argument #2 ($mask_char) must not be empty"); + RETURN_THROWS(); + } + + if (ZSTR_LEN(str) == 0) { + RETURN_STR_COPY(str); + } + + /* 3. Initialize ICU Break Iterator & UText */ + UErrorCode status = U_ZERO_ERROR; + UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, intl_locale_get_default(), NULL, 0, &status); + if (U_FAILURE(status)) { + php_error_docref(NULL, E_WARNING, "Failed to create break iterator"); + RETURN_FALSE; + } + + UText *ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); + if (U_FAILURE(status)) { + ubrk_close(bi); + php_error_docref(NULL, E_WARNING, "Failed to open UText"); + RETURN_FALSE; + } + + ubrk_setUText(bi, ut, &status); + if (U_FAILURE(status)) { + utext_close(ut); + ubrk_close(bi); + php_error_docref(NULL, E_WARNING, "Failed to set UText"); + RETURN_FALSE; + } + + /* 4. Count total graphemes to handle negative offsets/lengths */ + int32_t total_graphemes = 0; + while (ubrk_next(bi) != UBRK_DONE) { + total_graphemes++; + } + + /* 5. Calculate absolute start and mask length */ + zend_long start = offset; + if (start < 0) { + start += total_graphemes; + if (start < 0) { + start = 0; + } + } else if (start > total_graphemes) { + start = total_graphemes; + } + + zend_long mask_len; + if (length_is_null) { + mask_len = total_graphemes - start; + } else { + mask_len = length; + if (mask_len < 0) { + mask_len = total_graphemes - start + mask_len; + if (mask_len < 0) { + mask_len = 0; + } + } else if (start + mask_len > total_graphemes) { + mask_len = total_graphemes - start; + } + } + + /* If nothing to mask, return early */ + if (mask_len <= 0) { + utext_close(ut); + ubrk_close(bi); + RETURN_STR_COPY(str); + } + + /* 6. Find Byte Offsets (Fix: handle start == 0 correctly) */ + int32_t start_byte_offset = 0; + int32_t end_byte_offset = ZSTR_LEN(str); + int32_t current_grapheme = 0; + int32_t pos = ubrk_first(bi); + + while (pos != UBRK_DONE) { + if (current_grapheme == start) { + start_byte_offset = pos; + } + if (current_grapheme == start + mask_len) { + end_byte_offset = pos; + break; + } + pos = ubrk_next(bi); + current_grapheme++; + } + + /* Clean up ICU objects as we have the byte offsets now */ + utext_close(ut); + ubrk_close(bi); + + /* 7. Allocate Exact Final Memory and Copy */ + size_t prefix_len = start_byte_offset; + size_t suffix_len = ZSTR_LEN(str) - end_byte_offset; + size_t total_mask_bytes = mask_len * ZSTR_LEN(mask_char); + size_t final_len = prefix_len + total_mask_bytes + suffix_len; + + zend_string *result = zend_string_alloc(final_len, 0); + char *dest = ZSTR_VAL(result); + + /* Copy Prefix */ + if (prefix_len > 0) { + memcpy(dest, ZSTR_VAL(str), prefix_len); + dest += prefix_len; + } + + /* Copy Mask (repeat mask_char for each grapheme) */ + for (zend_long i = 0; i < mask_len; i++) { + memcpy(dest, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char)); + dest += ZSTR_LEN(mask_char); + } + + /* Copy Suffix */ + if (suffix_len > 0) { + memcpy(dest, ZSTR_VAL(str) + end_byte_offset, suffix_len); + } + + /* Null-terminate */ + ZSTR_VAL(result)[final_len] = '\0'; + + RETURN_NEW_STR(result); +} +/* }}} */ From 95d549fa28de169dd3c764ae96b581ad804e0176 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Thu, 2 Jul 2026 23:11:13 +0330 Subject: [PATCH 04/35] Create grapheme_mask.h --- ext/intl/grapheme/grapheme_mask.h | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ext/intl/grapheme/grapheme_mask.h diff --git a/ext/intl/grapheme/grapheme_mask.h b/ext/intl/grapheme/grapheme_mask.h new file mode 100644 index 000000000000..427bb29c1c8a --- /dev/null +++ b/ext/intl/grapheme/grapheme_mask.h @@ -0,0 +1,8 @@ +#ifndef GRAPHEME_MASK_H +#define GRAPHEME_MASK_H + +#include + +PHP_FUNCTION(grapheme_mask); + +#endif /* GRAPHEME_MASK_H */ From 36ac29e38d52f82aa67c88727e6b8b193b7b96a2 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Thu, 2 Jul 2026 23:32:47 +0330 Subject: [PATCH 05/35] Create grapheme_mask.stub.php --- ext/intl/grapheme/grapheme_mask.stub.php | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ext/intl/grapheme/grapheme_mask.stub.php diff --git a/ext/intl/grapheme/grapheme_mask.stub.php b/ext/intl/grapheme/grapheme_mask.stub.php new file mode 100644 index 000000000000..729a66758110 --- /dev/null +++ b/ext/intl/grapheme/grapheme_mask.stub.php @@ -0,0 +1,5 @@ + Date: Thu, 2 Jul 2026 23:42:43 +0330 Subject: [PATCH 06/35] Update grapheme_mask.stub.php From 971ec071fefd53628ecb40bd4b9f6ad22c2a1bda Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 13:52:22 +0330 Subject: [PATCH 07/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 169 +++++++++++++++++------------- 1 file changed, 97 insertions(+), 72 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index 338b431ea0b0..fd5032853e4b 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -1,37 +1,53 @@ -/* - +----------------------------------------------------------------------+ - | Copyright (c) The PHP Group | - +----------------------------------------------------------------------+ - | This source file is subject to version 3.01 of the PHP license, | - | that is bundled with this package in the file LICENSE, and is | - | available through the world-wide-web at the following url: | - | https://www.php.net/license/3_01.txt | - +----------------------------------------------------------------------+ - | Author: Sepehr Mahmoudi | - +----------------------------------------------------------------------+ -*/ - -#ifdef HAVE_CONFIG_H -# include -#endif - -#include "php_intl.h" -#include "grapheme_util.h" -#include "zend_exceptions.h" -#include -#include -#include - -/* {{{ proto string|false grapheme_mask(string $string, string $mask_char, int $offset = 0, ?int $length = null) - Masks a portion of a string respecting grapheme cluster boundaries */ +/* {{{ Validate that mask_char is exactly one grapheme cluster */ +static bool grapheme_mask_validate_mask_char(zend_string *mask_char) +{ + if (ZSTR_LEN(mask_char) == 0) { + return false; + } + + if (!grapheme_validate_utf8(ZSTR_VAL(mask_char), ZSTR_LEN(mask_char))) { + return false; + } + + UErrorCode status = U_ZERO_ERROR; + UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, intl_locale_get_default(), NULL, 0, &status); + if (U_FAILURE(status)) { + return false; + } + + UText *ut = utext_openUTF8(NULL, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char), &status); + if (U_FAILURE(status)) { + ubrk_close(bi); + return false; + } + + ubrk_setUText(bi, ut, &status); + if (U_FAILURE(status)) { + utext_close(ut); + ubrk_close(bi); + return false; + } + + int32_t start = ubrk_first(bi); + int32_t end = ubrk_next(bi); + int32_t next = ubrk_next(bi); + + utext_close(ut); + ubrk_close(bi); + + /* Must have exactly one complete grapheme cluster */ + return (start != UBRK_DONE && end != UBRK_DONE && next == UBRK_DONE); +} +/* }}} */ + +/* {{{ proto string|false grapheme_mask(string $string, string $mask_char, int $start = 0, ?int $length = null) */ PHP_FUNCTION(grapheme_mask) { zend_string *str, *mask_char; zend_long offset = 0; zend_long length = 0; - bool length_is_null = 1; + bool length_is_null = true; - /* 1. Parse Parameters */ ZEND_PARSE_PARAMETERS_START(2, 4) Z_PARAM_STR(str) Z_PARAM_STR(mask_char) @@ -40,28 +56,33 @@ PHP_FUNCTION(grapheme_mask) Z_PARAM_LONG_OR_NULL(length, length_is_null) ZEND_PARSE_PARAMETERS_END(); - /* 2. Validate Empty Mask -> Throw ValueError (PHP 8 style) */ - if (ZSTR_LEN(mask_char) == 0) { - zend_value_error("grapheme_mask(): Argument #2 ($mask_char) must not be empty"); + /* Validate mask_char */ + if (!grapheme_mask_validate_mask_char(mask_char)) { + zend_value_error("grapheme_mask(): Argument #2 ($mask_char) must be exactly one grapheme cluster"); RETURN_THROWS(); } + /* Fast path: Empty input string */ if (ZSTR_LEN(str) == 0) { RETURN_STR_COPY(str); } - /* 3. Initialize ICU Break Iterator & UText */ + /* Validate UTF-8 for input string */ + if (!grapheme_validate_utf8(ZSTR_VAL(str), ZSTR_LEN(str))) { + RETURN_FALSE; + } + UErrorCode status = U_ZERO_ERROR; UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, intl_locale_get_default(), NULL, 0, &status); if (U_FAILURE(status)) { - php_error_docref(NULL, E_WARNING, "Failed to create break iterator"); + intl_error_set(NULL, status, "grapheme_mask(): Failed to create break iterator", 0); RETURN_FALSE; } UText *ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); if (U_FAILURE(status)) { ubrk_close(bi); - php_error_docref(NULL, E_WARNING, "Failed to open UText"); + intl_error_set(NULL, status, "grapheme_mask(): Failed to open UText", 0); RETURN_FALSE; } @@ -69,17 +90,32 @@ PHP_FUNCTION(grapheme_mask) if (U_FAILURE(status)) { utext_close(ut); ubrk_close(bi); - php_error_docref(NULL, E_WARNING, "Failed to set UText"); + intl_error_set(NULL, status, "grapheme_mask(): Failed to set UText", 0); RETURN_FALSE; } - /* 4. Count total graphemes to handle negative offsets/lengths */ - int32_t total_graphemes = 0; - while (ubrk_next(bi) != UBRK_DONE) { - total_graphemes++; + /* Phase 1: Cache all grapheme boundaries */ + uint32_t alloc_graphemes = 16; + int32_t *boundaries = emalloc(alloc_graphemes * sizeof(int32_t)); + int32_t idx = 0; + int32_t pos = ubrk_first(bi); + + boundaries[idx++] = pos; /* First boundary (0) */ + + while ((pos = ubrk_next(bi)) != UBRK_DONE) { + if (idx >= alloc_graphemes) { + alloc_graphemes *= 2; + boundaries = erealloc(boundaries, alloc_graphemes * sizeof(int32_t)); + } + boundaries[idx++] = pos; } + + utext_close(ut); + ubrk_close(bi); + + int32_t total_graphemes = idx - 1; /* Number of grapheme clusters */ - /* 5. Calculate absolute start and mask length */ + /* Phase 2: Calculate start and length in grapheme units */ zend_long start = offset; if (start < 0) { start += total_graphemes; @@ -105,64 +141,53 @@ PHP_FUNCTION(grapheme_mask) } } - /* If nothing to mask, return early */ + /* No-op check */ if (mask_len <= 0) { - utext_close(ut); - ubrk_close(bi); + efree(boundaries); RETURN_STR_COPY(str); } - /* 6. Find Byte Offsets (Fix: handle start == 0 correctly) */ - int32_t start_byte_offset = 0; - int32_t end_byte_offset = ZSTR_LEN(str); - int32_t current_grapheme = 0; - int32_t pos = ubrk_first(bi); - - while (pos != UBRK_DONE) { - if (current_grapheme == start) { - start_byte_offset = pos; - } - if (current_grapheme == start + mask_len) { - end_byte_offset = pos; - break; - } - pos = ubrk_next(bi); - current_grapheme++; + /* Phase 3: Get byte offsets */ + int32_t start_byte_offset = boundaries[start]; + int32_t end_byte_offset; + + if (start + mask_len == total_graphemes) { + end_byte_offset = ZSTR_LEN(str); + } else { + end_byte_offset = boundaries[start + mask_len]; } + + efree(boundaries); - /* Clean up ICU objects as we have the byte offsets now */ - utext_close(ut); - ubrk_close(bi); - - /* 7. Allocate Exact Final Memory and Copy */ + /* Phase 4: Build result */ size_t prefix_len = start_byte_offset; size_t suffix_len = ZSTR_LEN(str) - end_byte_offset; - size_t total_mask_bytes = mask_len * ZSTR_LEN(mask_char); + size_t mask_char_len = ZSTR_LEN(mask_char); + size_t total_mask_bytes = mask_len * mask_char_len; size_t final_len = prefix_len + total_mask_bytes + suffix_len; zend_string *result = zend_string_alloc(final_len, 0); char *dest = ZSTR_VAL(result); - /* Copy Prefix */ + /* Copy prefix */ if (prefix_len > 0) { memcpy(dest, ZSTR_VAL(str), prefix_len); dest += prefix_len; } - /* Copy Mask (repeat mask_char for each grapheme) */ + /* Copy mask */ for (zend_long i = 0; i < mask_len; i++) { - memcpy(dest, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char)); - dest += ZSTR_LEN(mask_char); + memcpy(dest, ZSTR_VAL(mask_char), mask_char_len); + dest += mask_char_len; } - /* Copy Suffix */ + /* Copy suffix */ if (suffix_len > 0) { memcpy(dest, ZSTR_VAL(str) + end_byte_offset, suffix_len); } - /* Null-terminate */ ZSTR_VAL(result)[final_len] = '\0'; - RETURN_NEW_STR(result); } /* }}} */ + From c7fa946966de5350debd48acbd2b0fed7a87357a Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 14:35:21 +0330 Subject: [PATCH 08/35] Update grapheme_mask.stub.php --- ext/intl/grapheme/grapheme_mask.stub.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.stub.php b/ext/intl/grapheme/grapheme_mask.stub.php index 729a66758110..ecdb974ea472 100644 --- a/ext/intl/grapheme/grapheme_mask.stub.php +++ b/ext/intl/grapheme/grapheme_mask.stub.php @@ -1,5 +1,13 @@ Date: Sat, 4 Jul 2026 14:43:36 +0330 Subject: [PATCH 09/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index fd5032853e4b..ffa6c3d87941 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -40,7 +40,7 @@ static bool grapheme_mask_validate_mask_char(zend_string *mask_char) } /* }}} */ -/* {{{ proto string|false grapheme_mask(string $string, string $mask_char, int $start = 0, ?int $length = null) */ +/* {{{ proto string|false grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null) */ PHP_FUNCTION(grapheme_mask) { zend_string *str, *mask_char; @@ -48,9 +48,9 @@ PHP_FUNCTION(grapheme_mask) zend_long length = 0; bool length_is_null = true; - ZEND_PARSE_PARAMETERS_START(2, 4) + ZEND_PARSE_PARAMETERS_START(1, 4) Z_PARAM_STR(str) - Z_PARAM_STR(mask_char) + Z_PARAM_STR_DEFAULT(mask_char, "*") Z_PARAM_OPTIONAL Z_PARAM_LONG(offset) Z_PARAM_LONG_OR_NULL(length, length_is_null) @@ -99,9 +99,9 @@ PHP_FUNCTION(grapheme_mask) int32_t *boundaries = emalloc(alloc_graphemes * sizeof(int32_t)); int32_t idx = 0; int32_t pos = ubrk_first(bi); - + boundaries[idx++] = pos; /* First boundary (0) */ - + while ((pos = ubrk_next(bi)) != UBRK_DONE) { if (idx >= alloc_graphemes) { alloc_graphemes *= 2; @@ -109,10 +109,10 @@ PHP_FUNCTION(grapheme_mask) } boundaries[idx++] = pos; } - + utext_close(ut); ubrk_close(bi); - + int32_t total_graphemes = idx - 1; /* Number of grapheme clusters */ /* Phase 2: Calculate start and length in grapheme units */ @@ -150,13 +150,13 @@ PHP_FUNCTION(grapheme_mask) /* Phase 3: Get byte offsets */ int32_t start_byte_offset = boundaries[start]; int32_t end_byte_offset; - + if (start + mask_len == total_graphemes) { end_byte_offset = ZSTR_LEN(str); } else { end_byte_offset = boundaries[start + mask_len]; } - + efree(boundaries); /* Phase 4: Build result */ @@ -190,4 +190,3 @@ PHP_FUNCTION(grapheme_mask) RETURN_NEW_STR(result); } /* }}} */ - From d107f75986f03b9ab3e55b7591140ff16798af93 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 16:34:25 +0330 Subject: [PATCH 10/35] Create grapheme.c --- ext/intl/grapheme/grapheme.c | 534 +++++++++++++++++++++++++++++++++++ 1 file changed, 534 insertions(+) create mode 100644 ext/intl/grapheme/grapheme.c diff --git a/ext/intl/grapheme/grapheme.c b/ext/intl/grapheme/grapheme.c new file mode 100644 index 000000000000..fc7164c0cd70 --- /dev/null +++ b/ext/intl/grapheme/grapheme.c @@ -0,0 +1,534 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Ed Batutis | + | Sara Golemon | + | Sepehr Lajevardi (grapheme_mask) | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php.h" +#include "php_ini.h" +#include "ext/standard/info.h" +#include "php_intl.h" +#include "intl_convert.h" +#include "intl_data.h" +#include "grapheme.h" +#include "grapheme_mask.h" + +#include +#include +#include +#include + +/* {{{ Defines */ +#define GRAPHEME_MAX_BYTES 4 +/* }}} */ + +/* {{{ Internal function prototypes */ +static int32_t grapheme_count_graphemes(const char *str, size_t str_len); +static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t offset, int direction); +/* }}} */ + +/* {{{ grapheme_count_graphemes */ +static int32_t grapheme_count_graphemes(const char *str, size_t str_len) +{ + UErrorCode status = U_ZERO_ERROR; + UBreakIterator *bi; + int32_t count = 0; + UChar *u_str; + int32_t u_str_len; + + if (str_len == 0) { + return 0; + } + + u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); + u_charsToUChars(str, u_str, (int32_t)str_len); + u_str[str_len] = 0; + u_str_len = (int32_t)str_len; + + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); + if (U_FAILURE(status)) { + efree(u_str); + return -1; + } + + while (ubrk_next(bi) != UBRK_DONE) { + count++; + } + + ubrk_close(bi); + efree(u_str); + return count; +} +/* }}} */ + +/* {{{ grapheme_find_boundary */ +static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t offset, int direction) +{ + UErrorCode status = U_ZERO_ERROR; + UBreakIterator *bi; + UChar *u_str; + int32_t u_str_len, pos = 0, grapheme_count = 0, target = offset; + + if (str_len == 0) { + return -1; + } + + u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); + u_charsToUChars(str, u_str, (int32_t)str_len); + u_str[str_len] = 0; + u_str_len = (int32_t)str_len; + + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); + if (U_FAILURE(status)) { + efree(u_str); + return -1; + } + + if (direction > 0) { + while (grapheme_count < target && (pos = ubrk_next(bi)) != UBRK_DONE) { + grapheme_count++; + } + } else { + pos = u_str_len; + ubrk_last(bi); + while (grapheme_count < (-target) && (pos = ubrk_previous(bi)) != UBRK_DONE) { + grapheme_count++; + } + } + + ubrk_close(bi); + efree(u_str); + if (grapheme_count != target) { + return -1; + } + return pos; +} +/* }}} */ + +/* {{{ grapheme_mask_validate_mask_char */ +UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len, UErrorCode *status) +{ + if (mask_char_len == 0) { + return false; + } + UChar* uchars = NULL; + int32_t capacity = (int32_t)(mask_char_len + 1); + uchars = (UChar*) emalloc(capacity * sizeof(UChar)); + u_charsToUChars(mask_char, uchars, (int32_t)mask_char_len); + uchars[mask_char_len] = 0; + + UBreakIterator* bi = ubrk_open(UBRK_CHARACTER, NULL, uchars, mask_char_len, status); + if (U_FAILURE(*status)) { + efree(uchars); + return false; + } + int32_t firstBoundary = ubrk_following(bi, 0); + ubrk_close(bi); + efree(uchars); + + if (firstBoundary == UBRK_DONE || (size_t)firstBoundary != mask_char_len) { + return false; + } + return true; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_mask) */ +PHP_FUNCTION(grapheme_mask) +{ + char *str, *mask, *mask_char = "*"; + size_t str_len, mask_len, mask_char_len = 1; + zend_long start = 0, length = 0; + int32_t start_offset, end_offset, grapheme_count; + int32_t result_len, result_pos, str_pos, mask_itr; + UBreakIterator *bi, *str_bi; + UChar *u_str, *u_mask, *u_result, *u_mask_char; + UErrorCode status = U_ZERO_ERROR; + + ZEND_PARSE_PARAMETERS_START(2, 5) + Z_PARAM_STR(str) + Z_PARAM_STR(mask) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(start) + Z_PARAM_LONG(length) + Z_PARAM_STR_DEFAULT(mask_char, "*") + ZEND_PARSE_PARAMETERS_END(); + + /* Validate mask_char */ + if (!grapheme_mask_validate_mask_char(mask_char->val, mask_char->len, &status)) { + zend_argument_value_error(5, "must be a single grapheme cluster, i.e. an individual character or a valid multi-byte sequence"); + RETURN_THROWS(); + } + + /* Convert strings to UChar */ + int32_t u_str_len = (int32_t)str_len; + int32_t u_mask_len = (int32_t)mask_len; + u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); + u_mask = (UChar *)emalloc(sizeof(UChar) * (u_mask_len + 1)); + u_charsToUChars(str, u_str, u_str_len); + u_str[u_str_len] = 0; + u_charsToUChars(mask, u_mask, u_mask_len); + u_mask[u_mask_len] = 0; + + /* Convert mask_char */ + u_mask_char = (UChar *)emalloc(sizeof(UChar) * (mask_char_len + 1)); + u_charsToUChars(mask_char, u_mask_char, (int32_t)mask_char_len); + u_mask_char[mask_char_len] = 0; + + /* Break iterator for mask */ + bi = ubrk_open(UBRK_CHARACTER, NULL, u_mask, u_mask_len, &status); + if (U_FAILURE(status)) { + efree(u_str); efree(u_mask); efree(u_mask_char); + zend_throw_error(NULL, "grapheme_mask: Error creating ICU break iterator"); + return; + } + + /* Determine grapheme start/end indices */ + grapheme_count = grapheme_count_graphemes(str, str_len); + if (start < 0) { + start = grapheme_count + start; + if (start < 0) start = 0; + } + if (ZEND_NUM_ARGS() < 4) { + end_offset = grapheme_count; + } else { + if (length < 0) { + length = grapheme_count - start + length; + if (length < 0) length = 0; + } + end_offset = (int32_t)(start + length); + if (end_offset > grapheme_count) end_offset = grapheme_count; + } + + /* Create str_bi for string */ + str_bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); + if (U_FAILURE(status)) { + efree(u_str); efree(u_mask); efree(u_mask_char); + ubrk_close(bi); + zend_throw_error(NULL, "grapheme_mask: Error creating ICU break iterator for string"); + return; + } + + start_offset = 0; + for (int i = 0; i < start; i++) { + start_offset = ubrk_following(str_bi, start_offset); + } + end_offset = start_offset; + for (int i = start; i < end_offset; i++) { + end_offset = ubrk_following(str_bi, end_offset); + } + + /* Phase 1: allocate result buffer */ + result_len = u_str_len * (int32_t)mask_char_len + 1; + u_result = (UChar *)emalloc(sizeof(UChar) * result_len); + result_pos = 0; + str_pos = 0; + mask_itr = 0; + UBool mask_done = 0; + + /* Phase 2: prefix */ + while (str_pos < start_offset) { + u_result[result_pos++] = u_str[str_pos++]; + } + + /* Phase 3: masked region */ + while (str_pos < end_offset && !mask_done) { + int32_t next_mask_boundary = ubrk_following(bi, mask_itr); + if (next_mask_boundary == UBRK_DONE) { + mask_done = 1; + break; + } + int32_t next_str_boundary = ubrk_following(str_bi, str_pos); + if (next_str_boundary == UBRK_DONE) break; + + /* Overwrite with mask_char */ + for (int c = 0; c < (int32_t)mask_char_len; c++) { + u_result[result_pos++] = u_mask_char[c]; + } + str_pos = next_str_boundary; + mask_itr = next_mask_boundary; + } + + /* Phase 4: suffix */ + while (str_pos < u_str_len) { + u_result[result_pos++] = u_str[str_pos++]; + } + u_result[result_pos] = 0; + + /* Convert back to UTF-8 */ + int32_t out_len = 0; + char *out = (char *)emalloc(result_pos * 4 + 1); + u_strToUTF8(out, result_pos * 4, &out_len, u_result, result_pos, &status); + if (U_FAILURE(status)) { + efree(u_str); efree(u_mask); efree(u_mask_char); efree(u_result); efree(out); + ubrk_close(bi); ubrk_close(str_bi); + zend_throw_error(NULL, "grapheme_mask: UTF-8 conversion error"); + return; + } + out[out_len] = '\0'; + + /* Cleanup */ + efree(u_str); + efree(u_mask); + efree(u_mask_char); + efree(u_result); + ubrk_close(bi); + ubrk_close(str_bi); + + RETURN_STRINGL(out, out_len); + efree(out); +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strlen) */ +PHP_FUNCTION(grapheme_strlen) +{ + char *str; + size_t str_len; + int32_t count; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(str) + ZEND_PARSE_PARAMETERS_END(); + + count = grapheme_count_graphemes(str, str_len); + if (count >= 0) { + RETURN_LONG(count); + } + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strpos) */ +PHP_FUNCTION(grapheme_strpos) +{ + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(haystack) + Z_PARAM_STR(needle) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + pos = grapheme_find_boundary(haystack, haystack_len, (int32_t)offset, 1); + if (pos >= 0) { + /* Simplistic search; actual PHP implementation uses u_strstr etc. */ + char *found = zend_memnstr(haystack + pos, needle, needle_len, haystack + haystack_len); + if (found) { + RETURN_LONG(found - haystack); + } + } + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_stripos) */ +PHP_FUNCTION(grapheme_stripos) +{ + /* Placeholder - would need ICU case-insensitive search */ + zend_throw_error(NULL, "grapheme_stripos not implemented yet"); +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strrpos) */ +PHP_FUNCTION(grapheme_strrpos) +{ + /* Placeholder */ + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strripos) */ +PHP_FUNCTION(grapheme_strripos) +{ + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_substr) */ +PHP_FUNCTION(grapheme_substr) +{ + char *str; + size_t str_len; + zend_long start, length = 0; + int32_t start_offset, end_offset; + UBreakIterator *bi; + UChar *u_str; + UErrorCode status = U_ZERO_ERROR; + int32_t u_str_len, pos; + zend_bool length_is_null = 1; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(str) + Z_PARAM_LONG(start) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(length, length_is_null) + ZEND_PARSE_PARAMETERS_END(); + + u_str_len = (int32_t)str_len; + u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); + u_charsToUChars(str, u_str, u_str_len); + u_str[u_str_len] = 0; + + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); + if (U_FAILURE(status)) { + efree(u_str); + RETURN_FALSE; + } + + int32_t count = 0; + start_offset = 0; + if (start < 0) { + start = grapheme_count_graphemes(str, str_len) + start; + if (start < 0) start = 0; + } + for (count = 0; count < start; count++) { + start_offset = ubrk_following(bi, start_offset); + } + + if (length_is_null) { + end_offset = u_str_len; + } else { + if (length < 0) { + length = grapheme_count_graphemes(str, str_len) - start + length; + if (length < 0) length = 0; + } + end_offset = start_offset; + for (count = 0; count < length && end_offset < u_str_len; count++) { + end_offset = ubrk_following(bi, end_offset); + } + } + + pos = 0; + ubrk_close(bi); + + int32_t out_len = end_offset - start_offset; + char *out = (char *)emalloc(out_len + 1); + u_strToUTF8(out, out_len + 1, &out_len, u_str + start_offset, end_offset - start_offset, &status); + if (U_FAILURE(status)) { + efree(u_str); + efree(out); + RETURN_FALSE; + } + out[out_len] = '\0'; + efree(u_str); + RETURN_STRINGL(out, out_len); + efree(out); +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strstr) */ +PHP_FUNCTION(grapheme_strstr) +{ + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_bool before_needle = 0; + char *found; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(haystack) + Z_PARAM_STR(needle) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(before_needle) + ZEND_PARSE_PARAMETERS_END(); + + found = zend_memnstr(haystack, needle, needle_len, haystack + haystack_len); + if (found) { + if (before_needle) { + RETURN_STRINGL(haystack, found - haystack); + } else { + RETURN_STRINGL(found, haystack_len - (found - haystack)); + } + } + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_stristr) */ +PHP_FUNCTION(grapheme_stristr) +{ + zend_throw_error(NULL, "grapheme_stristr not implemented yet"); +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_extract) */ +PHP_FUNCTION(grapheme_extract) +{ + /* Stub */ + RETURN_FALSE; +} +/* }}} */ + +/* {{{ grapheme_functions[] */ +static const zend_function_entry grapheme_functions[] = { + PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) + PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) + PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) + PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) + PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) + PHP_FE(grapheme_substr, arginfo_grapheme_substr) + PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) + PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) + PHP_FE(grapheme_extract, arginfo_grapheme_extract) + PHP_FE(grapheme_mask, arginfo_grapheme_mask) + PHP_FE_END +}; +/* }}} */ + +/* {{{ PHP_MINIT_FUNCTION */ +PHP_MINIT_FUNCTION(grapheme) +{ + return SUCCESS; +} +/* }}} */ + +/* {{{ PHP_MINFO_FUNCTION */ +PHP_MINFO_FUNCTION(grapheme) +{ + php_info_print_table_start(); + php_info_print_table_row(2, "grapheme support", "enabled"); + php_info_print_table_end(); +} +/* }}} */ + +/* {{{ grapheme_module_entry */ +zend_module_entry grapheme_module_entry = { + STANDARD_MODULE_HEADER, + "grapheme", + grapheme_functions, + PHP_MINIT(grapheme), + NULL, /* MSHUTDOWN */ + NULL, /* RINIT */ + NULL, /* RSHUTDOWN */ + PHP_MINFO(grapheme), + PHP_GRAPHEME_VERSION, + STANDARD_MODULE_PROPERTIES +}; +/* }}} */ + +#ifdef COMPILE_DL_GRAPHEME +#ifdef ZTS +ZEND_TSRMLS_CACHE_DEFINE() +#endif +ZEND_GET_MODULE(grapheme) +#endif From 4c29b8d2827c1e17217f576490bb8dc56836cdb6 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 16:55:00 +0330 Subject: [PATCH 11/35] Update grapheme.h --- ext/intl/grapheme/grapheme.h | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/ext/intl/grapheme/grapheme.h b/ext/intl/grapheme/grapheme.h index b52f48f9d142..698f4b2fe3ce 100644 --- a/ext/intl/grapheme/grapheme.h +++ b/ext/intl/grapheme/grapheme.h @@ -1,35 +1,10 @@ -/* - +----------------------------------------------------------------------+ - | Copyright © The PHP Group and Contributors. | - +----------------------------------------------------------------------+ - | This source file is subject to the Modified BSD License that is | - | bundled with this package in the file LICENSE, and is available | - | through the World Wide Web at . | - | | - | SPDX-License-Identifier: BSD-3-Clause | - +----------------------------------------------------------------------+ - | Authors: Ed Batutis | - +----------------------------------------------------------------------+ - */ +#ifndef GRAPHEME_MASK_H +#define GRAPHEME_MASK_H -#ifndef GRAPHEME_GRAPHEME_H -#define GRAPHEME_GRAPHEME_H - -#include +#include #include -#ifdef __cplusplus -extern "C" { -#endif -void grapheme_close_global_iterator( void ); -#ifdef __cplusplus -} -#endif +UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len, UErrorCode *status); -#define GRAPHEME_EXTRACT_TYPE_COUNT 0 -#define GRAPHEME_EXTRACT_TYPE_MAXBYTES 1 -#define GRAPHEME_EXTRACT_TYPE_MAXCHARS 2 -#define GRAPHEME_EXTRACT_TYPE_MIN GRAPHEME_EXTRACT_TYPE_COUNT -#define GRAPHEME_EXTRACT_TYPE_MAX GRAPHEME_EXTRACT_TYPE_MAXCHARS +#endif -#endif // GRAPHEME_GRAPHEME_H From 7d0291e5e58df1c27c0947ffcbbb2d5b0d569183 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 17:17:24 +0330 Subject: [PATCH 12/35] Delete ext/intl/grapheme/grapheme_mask.stub.php --- ext/intl/grapheme/grapheme_mask.stub.php | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 ext/intl/grapheme/grapheme_mask.stub.php diff --git a/ext/intl/grapheme/grapheme_mask.stub.php b/ext/intl/grapheme/grapheme_mask.stub.php deleted file mode 100644 index ecdb974ea472..000000000000 --- a/ext/intl/grapheme/grapheme_mask.stub.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Sat, 4 Jul 2026 17:27:41 +0330 Subject: [PATCH 13/35] Update grapheme.h --- ext/intl/grapheme/grapheme.h | 1191 +++++++++++++++++++++++++++++++++- 1 file changed, 1186 insertions(+), 5 deletions(-) diff --git a/ext/intl/grapheme/grapheme.h b/ext/intl/grapheme/grapheme.h index 698f4b2fe3ce..eb8b7e609b8e 100644 --- a/ext/intl/grapheme/grapheme.h +++ b/ext/intl/grapheme/grapheme.h @@ -1,10 +1,1191 @@ -#ifndef GRAPHEME_MASK_H -#define GRAPHEME_MASK_H +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Ed Batutis | + +----------------------------------------------------------------------+ + */ + +/* {{{ includes */ +#ifdef HAVE_CONFIG_H +#include +#endif + +#if __cplusplus >= 201703L +#include +#include +#endif + +extern "C" { +#include +#include "grapheme.h" +#include "grapheme_util.h" +} -#include #include +#include +#include +#include +#include -UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len, UErrorCode *status); +/* }}} */ -#endif +/* {{{ Get number of graphemes in a string */ +U_CFUNC PHP_FUNCTION(grapheme_strlen) +{ + char* string; + size_t string_len; + UChar* ustring = nullptr; + int ustring_len = 0; + zend_long ret_len; + UErrorCode status; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(string, string_len) + ZEND_PARSE_PARAMETERS_END(); + + ret_len = grapheme_ascii_check((unsigned char *)string, string_len); + + if ( ret_len >= 0 ) + RETURN_LONG(string_len); + + /* convert the string to UTF-16. */ + status = U_ZERO_ERROR; + intl_convert_utf8_to_utf16(&ustring, &ustring_len, string, string_len, &status ); + + if ( U_FAILURE( status ) ) { + /* Set global error code. */ + intl_error_set_code( nullptr, status ); + + /* Set error messages. */ + intl_error_set_custom_msg( nullptr, "Error converting input string to UTF-16"); + if (ustring) { + efree( ustring ); + } + RETURN_NULL(); + } + + ret_len = grapheme_split_string(ustring, ustring_len, nullptr, 0 ); + + if (ustring) { + efree( ustring ); + } + + if (ret_len >= 0) { + RETVAL_LONG(ret_len); + } else { + RETVAL_FALSE; + } +} +/* }}} */ + +/* {{{ Find position of first occurrence of a string within another */ +U_CFUNC PHP_FUNCTION(grapheme_strpos) +{ + char *haystack, *needle, *locale = ""; + size_t haystack_len, needle_len, locale_len = 0; + const char *found; + zend_long loffset = 0; + int32_t offset = 0; + size_t noffset = 0; + zend_long ret_pos; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(loffset) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if ( OUTSIDE_STRING(loffset, haystack_len) ) { + zend_argument_value_error(3, "must be contained in argument #1 ($haystack)"); + RETURN_THROWS(); + } + + /* we checked that it will fit: */ + offset = (int32_t) loffset; + noffset = offset >= 0 ? offset : (int32_t)haystack_len + offset; + + /* the offset is 'grapheme count offset' so it still might be invalid - we'll check it later */ + + if (offset >= 0 && grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0) { + /* quick check to see if the string might be there + * I realize that 'offset' is 'grapheme count offset' but will work in spite of that + */ + found = php_memnstr(haystack + noffset, needle, needle_len, haystack + haystack_len); + + /* if it isn't there the we are done */ + if (found) { + RETURN_LONG(found - haystack); + } + RETURN_FALSE; + } + + /* do utf16 part of the strpos */ + ret_pos = grapheme_strpos_utf16(haystack, haystack_len, needle, needle_len, offset, nullptr, /* fIgnoreCase */ 0, /* last */ 0, locale); + + if ( ret_pos >= 0 ) { + RETURN_LONG(ret_pos); + } else { + RETURN_FALSE; + } +} +/* }}} */ + +/* {{{ Find position of first occurrence of a string within another, ignoring case differences */ +U_CFUNC PHP_FUNCTION(grapheme_stripos) +{ + char *haystack, *needle, *locale = ""; + size_t haystack_len, needle_len, locale_len = 0; + const char *found; + zend_long loffset = 0; + int32_t offset = 0; + zend_long ret_pos; + int is_ascii; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(loffset) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if ( OUTSIDE_STRING(loffset, haystack_len) ) { + zend_argument_value_error(3, "must be contained in argument #1 ($haystack)"); + RETURN_THROWS(); + } + + /* we checked that it will fit: */ + offset = (int32_t) loffset; + + /* the offset is 'grapheme count offset' so it still might be invalid - we'll check it later */ + + is_ascii = ( grapheme_ascii_check((unsigned char*)haystack, haystack_len) >= 0 ); + + if ( is_ascii ) { + char *haystack_dup, *needle_dup; + int32_t noffset = offset >= 0 ? offset : (int32_t)haystack_len + offset; + needle_dup = estrndup(needle, needle_len); + zend_str_tolower(needle_dup, needle_len); + haystack_dup = estrndup(haystack, haystack_len); + zend_str_tolower(haystack_dup, haystack_len); + + found = php_memnstr(haystack_dup + noffset, needle_dup, needle_len, haystack_dup + haystack_len); + + efree(haystack_dup); + efree(needle_dup); + + if (found) { + RETURN_LONG(found - haystack_dup); + } + + /* if needle was ascii too, we are all done, otherwise we need to try using Unicode to see what we get */ + if ( grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0 ) { + RETURN_FALSE; + } + } + + /* do utf16 part of the strpos */ + ret_pos = grapheme_strpos_utf16(haystack, haystack_len, needle, needle_len, offset, nullptr, /* fIgnoreCase */ 1, /*last */ 0, locale); + + if ( ret_pos >= 0 ) { + RETURN_LONG(ret_pos); + } else { + RETURN_FALSE; + } + +} +/* }}} */ + +/* {{{ Find position of last occurrence of a string within another */ +U_CFUNC PHP_FUNCTION(grapheme_strrpos) +{ + char *haystack, *needle; + char *locale = ""; + size_t haystack_len, needle_len, locale_len = 0; + zend_long loffset = 0; + int32_t offset = 0; + zend_long ret_pos; + int is_ascii; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(loffset) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if ( OUTSIDE_STRING(loffset, haystack_len) ) { + zend_argument_value_error(3, "must be contained in argument #1 ($haystack)"); + RETURN_THROWS(); + } + + /* we checked that it will fit: */ + offset = (int32_t) loffset; + + /* the offset is 'grapheme count offset' so it still might be invalid - we'll check it later */ + + is_ascii = grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0; + + if ( is_ascii ) { + + ret_pos = grapheme_strrpos_ascii(haystack, haystack_len, needle, needle_len, offset); + + if ( ret_pos >= 0 ) { + RETURN_LONG(ret_pos); + } + + /* if the needle was ascii too, we are done */ + + if ( grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0 ) { + RETURN_FALSE; + } + + /* else we need to continue via utf16 */ + } + + ret_pos = grapheme_strpos_utf16(haystack, haystack_len, needle, needle_len, offset, nullptr, /* f_ignore_case */ 0, /* last */ 1, locale); + + if ( ret_pos >= 0 ) { + RETURN_LONG(ret_pos); + } else { + RETURN_FALSE; + } + + +} +/* }}} */ + +/* {{{ Find position of last occurrence of a string within another, ignoring case */ +U_CFUNC PHP_FUNCTION(grapheme_strripos) +{ + char *haystack, *needle, *locale = ""; + size_t haystack_len, needle_len, locale_len = 0; + zend_long loffset = 0; + int32_t offset = 0; + zend_long ret_pos; + int is_ascii; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(loffset) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if ( OUTSIDE_STRING(loffset, haystack_len) ) { + zend_argument_value_error(3, "must be contained in argument #1 ($haystack)"); + RETURN_THROWS(); + } + + /* we checked that it will fit: */ + offset = (int32_t) loffset; + + /* the offset is 'grapheme count offset' so it still might be invalid - we'll check it later */ + + is_ascii = grapheme_ascii_check((unsigned char *)haystack, haystack_len) >= 0; + + if ( is_ascii ) { + char *needle_dup, *haystack_dup; + + needle_dup = estrndup(needle, needle_len); + zend_str_tolower(needle_dup, needle_len); + haystack_dup = estrndup(haystack, haystack_len); + zend_str_tolower(haystack_dup, haystack_len); + + ret_pos = grapheme_strrpos_ascii(haystack_dup, haystack_len, needle_dup, needle_len, offset); + + efree(haystack_dup); + efree(needle_dup); + + if ( ret_pos >= 0 ) { + RETURN_LONG(ret_pos); + } + + /* if the needle was ascii too, we are done */ + + if ( grapheme_ascii_check((unsigned char *)needle, needle_len) >= 0 ) { + RETURN_FALSE; + } + + /* else we need to continue via utf16 */ + } + + ret_pos = grapheme_strpos_utf16(haystack, haystack_len, needle, needle_len, offset, nullptr, /* f_ignore_case */ 1, /*last */ 1, locale); + + if ( ret_pos >= 0 ) { + RETURN_LONG(ret_pos); + } else { + RETURN_FALSE; + } + + +} +/* }}} */ + +/* {{{ Returns part of a string */ +U_CFUNC PHP_FUNCTION(grapheme_substr) +{ + char *str, *locale = ""; + zend_string *u8_sub_str; + UChar *ustr; + size_t str_len, locale_len = 0; + int32_t ustr_len; + zend_long lstart = 0, length = 0; + int32_t start = 0; + int iter_val; + UErrorCode status; + UBreakIterator* bi = nullptr; + int sub_str_start_pos, sub_str_end_pos; + int32_t (*iter_func)(UBreakIterator *); + bool no_length = true; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(str, str_len) + Z_PARAM_LONG(lstart) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(length, no_length) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if (lstart < INT32_MIN || lstart > INT32_MAX) { + zend_argument_value_error(2, "is too large"); + RETURN_THROWS(); + } + + start = (int32_t) lstart; + + if (no_length) { + length = str_len; + } + + if (length < INT32_MIN || length > INT32_MAX) { + zend_argument_value_error(3, "is too large"); + RETURN_THROWS(); + } + + /* the offset is 'grapheme count offset' so it still might be invalid - we'll check it later */ + + if ( grapheme_ascii_check((unsigned char *)str, str_len) >= 0 ) { + int32_t asub_str_len; + char *sub_str; + grapheme_substr_ascii(str, str_len, start, (int32_t)length, &sub_str, &asub_str_len); + + if ( nullptr == sub_str ) { + intl_error_set( nullptr, U_ILLEGAL_ARGUMENT_ERROR, "invalid parameters"); + RETURN_FALSE; + } + + RETURN_STRINGL(sub_str, asub_str_len); + } + + ustr = nullptr; + ustr_len = 0; + status = U_ZERO_ERROR; + intl_convert_utf8_to_utf16(&ustr, &ustr_len, str, str_len, &status); + + if ( U_FAILURE( status ) ) { + /* Set global error code. */ + intl_error_set_code( nullptr, status ); + + /* Set error messages. */ + intl_error_set_custom_msg( nullptr, "Error converting input string to UTF-16"); + if (ustr) { + efree( ustr ); + } + RETURN_FALSE; + } + + bi = grapheme_get_break_iterator(&status); + + if( U_FAILURE(status) ) { + RETURN_FALSE; + } + + ubrk_setText(bi, ustr, ustr_len, &status); + + if ( start < 0 ) { + iter_func = ubrk_previous; + ubrk_last(bi); + iter_val = 1; + } + else { + iter_func = ubrk_next; + iter_val = -1; + } + + sub_str_start_pos = 0; + + while ( start ) { + sub_str_start_pos = iter_func(bi); + + if ( UBRK_DONE == sub_str_start_pos ) { + break; + } + + start += iter_val; + } + + if (0 != start) { + if (start > 0) { + if (ustr) { + efree(ustr); + } + ubrk_close(bi); + RETURN_EMPTY_STRING(); + } + + sub_str_start_pos = 0; + ubrk_first(bi); + } + + /* OK to convert here since if str_len were big, convert above would fail */ + if (length >= (int32_t)str_len) { + + /* no length supplied or length is too big, return the rest of the string */ + + status = U_ZERO_ERROR; + u8_sub_str = intl_convert_utf16_to_utf8(ustr + sub_str_start_pos, ustr_len - sub_str_start_pos, &status); + + if (ustr) { + efree( ustr ); + } + ubrk_close( bi ); + + if ( !u8_sub_str ) { + /* Set global error code. */ + intl_error_set_code( nullptr, status ); + + /* Set error messages. */ + intl_error_set_custom_msg( nullptr, "Error converting output string to UTF-8"); + + RETURN_FALSE; + } + + /* return the allocated string, not a duplicate */ + RETVAL_NEW_STR(u8_sub_str); + return; + } + + if(length == 0) { + /* empty length - we've validated start, we can return "" now */ + if (ustr) { + efree(ustr); + } + ubrk_close(bi); + RETURN_EMPTY_STRING(); + } + + /* find the end point of the string to return */ + + if ( length < 0 ) { + iter_func = ubrk_previous; + ubrk_last(bi); + iter_val = 1; + } + else { + iter_func = ubrk_next; + iter_val = -1; + } + + sub_str_end_pos = 0; + + while ( length ) { + sub_str_end_pos = iter_func(bi); + + if ( UBRK_DONE == sub_str_end_pos ) { + break; + } + + length += iter_val; + } + + ubrk_close(bi); + + if ( UBRK_DONE == sub_str_end_pos) { + if (length < 0) { + efree(ustr); + RETURN_EMPTY_STRING(); + } else { + sub_str_end_pos = ustr_len; + } + } + + if (sub_str_start_pos > sub_str_end_pos) { + efree(ustr); + RETURN_EMPTY_STRING(); + } + + status = U_ZERO_ERROR; + u8_sub_str = intl_convert_utf16_to_utf8(ustr + sub_str_start_pos, ( sub_str_end_pos - sub_str_start_pos ), &status); + + efree( ustr ); + + if ( !u8_sub_str ) { + /* Set global error code. */ + intl_error_set_code( nullptr, status ); + + /* Set error messages. */ + intl_error_set_custom_msg( nullptr, "Error converting output string to UTF-8"); + + RETURN_FALSE; + } + + /* return the allocated string, not a duplicate */ + RETVAL_NEW_STR(u8_sub_str); +} +/* }}} */ + +/* {{{ strstr_common_handler */ +static void strstr_common_handler(INTERNAL_FUNCTION_PARAMETERS, int f_ignore_case) +{ + char *haystack, *needle, *locale = ""; + const char *found; + size_t haystack_len, needle_len, locale_len = 0; + int32_t ret_pos, uchar_pos; + bool part = false; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(part) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if ( !f_ignore_case ) { + + /* ASCII optimization: quick check to see if the string might be there */ + found = php_memnstr(haystack, needle, needle_len, haystack + haystack_len); + + /* if it isn't there the we are done */ + if ( !found ) { + 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; + + if (part) { + RETURN_STRINGL(haystack, found_offset); + } else { + RETURN_STRINGL(found, haystack_len - found_offset); + } + } + + } + + /* need to work in utf16 */ + ret_pos = grapheme_strpos_utf16(haystack, haystack_len, needle, needle_len, 0, &uchar_pos, f_ignore_case, /* last */ 0, locale); + + if ( ret_pos < 0 ) { + RETURN_FALSE; + } + + /* uchar_pos is the 'nth' Unicode character position of the needle */ + + ret_pos = 0; + U8_FWD_N(haystack, ret_pos, haystack_len, uchar_pos); + + if (part) { + RETURN_STRINGL(haystack, ret_pos); + } else { + RETURN_STRINGL(haystack + ret_pos, haystack_len - ret_pos); + } + +} +/* }}} */ + +/* {{{ Finds first occurrence of a string within another */ +U_CFUNC PHP_FUNCTION(grapheme_strstr) +{ + strstr_common_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0 /* f_ignore_case */); +} +/* }}} */ + +/* {{{ Finds first occurrence of a string within another */ +U_CFUNC PHP_FUNCTION(grapheme_stristr) +{ + strstr_common_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1 /* f_ignore_case */); +} +/* }}} */ + +/* {{{ grapheme_extract_charcount_iter - grapheme iterator for grapheme_extract MAXCHARS */ +static inline int32_t +grapheme_extract_charcount_iter(UBreakIterator *bi, int32_t csize, unsigned char *pstr, int32_t str_len) +{ + int pos = 0; + int ret_pos = 0; + int break_pos, prev_break_pos; + int count = 0; + + while ( 1 ) { + pos = ubrk_next(bi); + + if ( UBRK_DONE == pos ) { + break; + } + + for ( break_pos = ret_pos; break_pos < pos; ) { + count++; + prev_break_pos = break_pos; + U8_FWD_1(pstr, break_pos, str_len); + + if ( prev_break_pos == break_pos ) { + /* something wrong - malformed utf8? */ + csize = 0; + break; + } + } + + /* if we are beyond our limit, then the loop is done */ + if ( count > csize ) { + break; + } + + ret_pos = break_pos; + } + + return ret_pos; +} +/* }}} */ + +/* {{{ grapheme_extract_bytecount_iter - grapheme iterator for grapheme_extract MAXBYTES */ +static inline int32_t +grapheme_extract_bytecount_iter(UBreakIterator *bi, int32_t bsize, unsigned char *pstr, int32_t str_len) +{ + int pos = 0; + int ret_pos = 0; + + while ( 1 ) { + pos = ubrk_next(bi); + + if ( UBRK_DONE == pos ) { + break; + } + + if ( pos > bsize ) { + break; + } + + ret_pos = pos; + } + + return ret_pos; +} +/* }}} */ + +/* {{{ grapheme_extract_count_iter - grapheme iterator for grapheme_extract COUNT */ +static inline int32_t +grapheme_extract_count_iter(UBreakIterator *bi, int32_t size, unsigned char *pstr, int32_t str_len) +{ + int next_pos = 0; + int ret_pos = 0; + + while ( size ) { + next_pos = ubrk_next(bi); + + if ( UBRK_DONE == next_pos ) { + break; + } + ret_pos = next_pos; + size--; + } + + return ret_pos; +} +/* }}} */ + +/* {{{ grapheme extract iter function pointer array */ +typedef int32_t (*grapheme_extract_iter)(UBreakIterator * /*bi*/, int32_t /*size*/, unsigned char * /*pstr*/, int32_t /*str_len*/); + +static const grapheme_extract_iter grapheme_extract_iters[] = { + &grapheme_extract_count_iter, + &grapheme_extract_bytecount_iter, + &grapheme_extract_charcount_iter, +}; +/* }}} */ + +/* {{{ Function to extract a sequence of default grapheme clusters */ +U_CFUNC PHP_FUNCTION(grapheme_extract) +{ + char *str, *pstr; + UText ut = UTEXT_INITIALIZER; + size_t str_len; + zend_long size; /* maximum number of grapheme clusters, bytes, or characters (based on extract_type) to return */ + zend_long lstart = 0; /* starting position in str in bytes */ + int32_t start = 0; + zend_long extract_type = GRAPHEME_EXTRACT_TYPE_COUNT; + UErrorCode status; + UBreakIterator* bi = nullptr; + int ret_pos; + zval *next = nullptr; /* return offset of next part of the string */ + + ZEND_PARSE_PARAMETERS_START(2, 5) + Z_PARAM_STRING(str, str_len) + Z_PARAM_LONG(size) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(extract_type) + Z_PARAM_LONG(lstart) + Z_PARAM_ZVAL(next) + ZEND_PARSE_PARAMETERS_END(); + + if (lstart < 0) { + lstart += str_len; + } + + if ( nullptr != next ) { + ZEND_ASSERT(Z_ISREF_P(next)); + ZEND_TRY_ASSIGN_REF_LONG(next, lstart); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } + } + + if ( extract_type < GRAPHEME_EXTRACT_TYPE_MIN || extract_type > GRAPHEME_EXTRACT_TYPE_MAX ) { + zend_argument_value_error(3, "must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS"); + RETURN_THROWS(); + } + + if ( lstart > INT32_MAX || lstart < 0 || (size_t)lstart >= str_len ) { + intl_error_set( nullptr, U_ILLEGAL_ARGUMENT_ERROR, "start not contained in string"); + RETURN_FALSE; + } + + if (size < 0) { + zend_argument_value_error(2, "must be greater than or equal to 0"); + RETURN_THROWS(); + } + + if (size > INT32_MAX) { + zend_argument_value_error(2, "is too large"); + RETURN_THROWS(); + } + + if (size == 0) { + RETURN_EMPTY_STRING(); + } + + /* we checked that it will fit: */ + start = (int32_t) lstart; + + pstr = str + start; + + /* just in case pstr points in the middle of a character, move forward to the start of the next char */ + if ( !U8_IS_SINGLE(*pstr) && !U8_IS_LEAD(*pstr) ) { + char *str_end = str + str_len; + + while ( !U8_IS_SINGLE(*pstr) && !U8_IS_LEAD(*pstr) ) { + pstr++; + start++; + if ( pstr >= str_end ) { + intl_error_set( nullptr, U_ILLEGAL_ARGUMENT_ERROR, + "grapheme_extract: invalid input string"); + + RETURN_FALSE; + } + } + } + + str_len -= (pstr - str); + + /* if the string is all ASCII up to size+1 - or str_len whichever is first - then we are done. + (size + 1 because the size-th character might be the beginning of a grapheme cluster) + */ + + if ( -1 != grapheme_ascii_check((unsigned char *)pstr, MIN(size + 1, str_len)) ) { + size_t nsize = MIN(size, str_len); + if ( nullptr != next ) { + ZEND_TRY_ASSIGN_REF_LONG(next, start + nsize); + } + RETURN_STRINGL(pstr, nsize); + } + + status = U_ZERO_ERROR; + utext_openUTF8(&ut, pstr, str_len, &status); + + if ( U_FAILURE( status ) ) { + /* Set global error code. */ + intl_error_set_code( nullptr, status ); + + /* Set error messages. */ + intl_error_set_custom_msg( nullptr, "Error opening UTF-8 text"); + + RETURN_FALSE; + } + + bi = nullptr; + status = U_ZERO_ERROR; + bi = grapheme_get_break_iterator(&status); + + ubrk_setUText(bi, &ut, &status); + /* if the caller put us in the middle of a grapheme, we can't detect it in all cases since we + can't back up. So, we will not do anything. */ + + /* now we need to find the end of the chunk the user wants us to return */ + /* it's ok to convert str_len to in32_t since if it were too big intl_convert_utf8_to_utf16 above would fail */ + ret_pos = (*grapheme_extract_iters[extract_type])(bi, size, (unsigned char *)pstr, (int32_t)str_len); + + utext_close(&ut); + ubrk_close(bi); + + if ( nullptr != next ) { + ZEND_TRY_ASSIGN_REF_LONG(next, start + ret_pos); + } + + RETURN_STRINGL(((char *)pstr), ret_pos); +} + +U_CFUNC PHP_FUNCTION(grapheme_str_split) +{ + char *pstr, *end; + zend_string *str; + zend_long split_len = 1; + + UErrorCode ustatus = U_ZERO_ERROR; + int32_t pos, current, i, end_len = 0; + UBreakIterator* bi; + UText *ut = nullptr; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(str) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(split_len) + ZEND_PARSE_PARAMETERS_END(); + + if (split_len <= 0 || split_len > UINT_MAX / 4) { + zend_argument_value_error(2, "must be greater than 0 and less than or equal to %d", UINT_MAX / 4); + RETURN_THROWS(); + } + + if (ZSTR_LEN(str) == 0) { + RETURN_EMPTY_ARRAY(); + } + + pstr = ZSTR_VAL(str); + ut = utext_openUTF8(ut, pstr, ZSTR_LEN(str), &ustatus); + + if ( U_FAILURE( ustatus ) ) { + /* Set global error code. */ + intl_error_set_code( nullptr, ustatus ); + + /* Set error messages. */ + intl_error_set_custom_msg( nullptr, "Error opening UTF-8 text"); + + RETURN_FALSE; + } + + bi = nullptr; + ustatus = U_ZERO_ERROR; + bi = grapheme_get_break_iterator(&ustatus); + + if( U_FAILURE(ustatus) ) { + RETURN_FALSE; + } + + ubrk_setUText(bi, ut, &ustatus); + + pos = 0; + array_init(return_value); + + for (end = pstr, i = 0, current = 0; pos != UBRK_DONE;) { + end_len = pos - current; + pos = ubrk_next(bi); + + if (i == split_len - 1) { + if ( pos != UBRK_DONE ) { + add_next_index_stringl(return_value, pstr, pos - current); + end = pstr + pos - current; + i = 0; + } + pstr += pos - current; + current = pos; + } else { + i += 1; + } + } + + if (i != 0 && end_len != 0) { + add_next_index_stringl(return_value, end, end_len); + } + + utext_close(ut); + ubrk_close(bi); +} + +U_CFUNC PHP_FUNCTION(grapheme_levenshtein) +{ + zend_string *string1, *string2; + zend_long cost_ins = 1; + zend_long cost_rep = 1; + zend_long cost_del = 1; + char *locale = ""; + size_t locale_len = 0; + + ZEND_PARSE_PARAMETERS_START(2, 6) + Z_PARAM_STR(string1) + Z_PARAM_STR(string2) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(cost_ins) + Z_PARAM_LONG(cost_rep) + Z_PARAM_LONG(cost_del) + Z_PARAM_PATH(locale, locale_len) + ZEND_PARSE_PARAMETERS_END(); + + if (cost_ins <= 0 || cost_ins > UINT_MAX / 4) { + zend_argument_value_error(3, "must be greater than 0 and less than or equal to %d", UINT_MAX / 4); + RETURN_THROWS(); + } + + if (cost_rep <= 0 || cost_rep > UINT_MAX / 4) { + zend_argument_value_error(4, "must be greater than 0 and less than or equal to %d", UINT_MAX / 4); + RETURN_THROWS(); + } + + if (cost_del <= 0 || cost_del > UINT_MAX / 4) { + zend_argument_value_error(5, "must be greater than 0 and less than or equal to %d", UINT_MAX / 4); + RETURN_THROWS(); + } + + zend_long c0, c1, c2; + zend_long retval; + size_t i2; + char *pstr1, *pstr2; + + UChar *ustring1 = nullptr; + UChar *ustring2 = nullptr; + + int32_t ustring1_len = 0; + int32_t ustring2_len = 0; + int32_t current1 = 0; + int32_t current2 = 0; + int32_t pos1 = 0; + int32_t pos2 = 0; + + UCollator *collator = nullptr; + + UErrorCode ustatus = U_ZERO_ERROR; + + /* When all costs are equal, levenshtein fulfills the requirements of a metric, which means + * that the distance is symmetric. If string1 is shorter than string2 we can save memory (and CPU time) + * by having shorter rows (p1 & p2). */ + if (ZSTR_LEN(string1) < ZSTR_LEN(string2) && cost_ins == cost_rep && cost_rep == cost_del) { + zend_string *tmp = string1; + string1 = string2; + string2 = tmp; + } + + pstr1 = ZSTR_VAL(string1); + pstr2 = ZSTR_VAL(string2); + + intl_convert_utf8_to_utf16(&ustring1, &ustring1_len, pstr1, ZSTR_LEN(string1), &ustatus); + + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + + intl_error_set_custom_msg(NULL, "Error converting input string to UTF-16"); + RETVAL_FALSE; + goto out_ustring1; + } + + intl_convert_utf8_to_utf16(&ustring2, &ustring2_len, pstr2, ZSTR_LEN(string2), &ustatus); + + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + + intl_error_set_custom_msg(NULL, "Error converting input string to UTF-16"); + RETVAL_FALSE; + goto out_ustring2; + } + + UBreakIterator *bi1, *bi2; + + int32_t strlen_1, strlen_2; + strlen_1 = grapheme_split_string(ustring1, ustring1_len, nullptr, 0); + strlen_2 = grapheme_split_string(ustring2, ustring2_len, nullptr, 0); + if (UNEXPECTED(strlen_1 < 0 || strlen_2 < 0)) { + RETVAL_FALSE; + goto out_ustring2; + } + + if (strlen_1 == 0) { + RETVAL_LONG(strlen_2 * cost_ins); + goto out_ustring2; + } + if (strlen_2 == 0) { + RETVAL_LONG(strlen_1 * cost_del); + goto out_ustring2; + } + + bi1 = grapheme_get_break_iterator(&ustatus); + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + intl_error_set_custom_msg(NULL, "Error on grapheme_get_break_iterator for argument #1 ($string1)"); + RETVAL_FALSE; + goto out_bi1; + } + + bi2 = grapheme_get_break_iterator(&ustatus); + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + intl_error_set_custom_msg(NULL, "Error on grapheme_get_break_iterator for argument #2 ($string2)"); + RETVAL_FALSE; + goto out_bi2; + } + + ubrk_setText(bi1, ustring1, ustring1_len, &ustatus); + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + + intl_error_set_custom_msg(NULL, "Error on ubrk_setText for argument #1 ($string1)"); + RETVAL_FALSE; + goto out_bi2; + } + + ubrk_setText(bi2, ustring2, ustring2_len, &ustatus); + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + + intl_error_set_custom_msg(NULL, "Error on ubrk_setText for argument #2 ($string2)"); + RETVAL_FALSE; + goto out_bi2; + } + collator = ucol_open(locale, &ustatus); + if (U_FAILURE(ustatus)) { + intl_error_set_code(NULL, ustatus); + + intl_error_set_custom_msg(NULL, "Error on ucol_open"); + RETVAL_FALSE; + goto out_collator; + } + + zend_long *p1, *p2, *tmp; + p1 = reinterpret_cast(safe_emalloc((size_t) strlen_2 + 1, sizeof(zend_long), 0)); + p2 = reinterpret_cast(safe_emalloc((size_t) strlen_2 + 1, sizeof(zend_long), 0)); + + for (i2 = 0; i2 <= strlen_2; i2++) { + p1[i2] = i2 * cost_ins; + } + + while (true) { + current1 = ubrk_current(bi1); + pos1 = ubrk_next(bi1); + if (pos1 == UBRK_DONE) { + break; + } + p2[0] = p1[0] + cost_del; + for (i2 = 0, pos2 = 0; pos2 != UBRK_DONE; i2++) { + current2 = ubrk_current(bi2); + pos2 = ubrk_next(bi2); + if (pos2 == UBRK_DONE) { + break; + } + if (ucol_strcoll(collator, ustring1 + current1, pos1 - current1, ustring2 + current2, pos2 - current2) == UCOL_EQUAL) { + c0 = p1[i2]; + } else { + c0 = p1[i2] + cost_rep; + } + c1 = p1[i2 + 1] + cost_del; + if (c1 < c0) { + c0 = c1; + } + c2 = p2[i2] + cost_ins; + if (c2 < c0) { + c0 = c2; + } + p2[i2 + 1] = c0; + } + ubrk_first(bi2); + tmp = p1; + p1 = p2; + p2 = tmp; + } + + retval = p1[strlen_2]; + RETVAL_LONG(retval); + + efree(p2); + efree(p1); + +out_collator: + ucol_close(collator); +out_bi2: + ubrk_close(bi2); +out_bi1: + ubrk_close(bi1); +out_ustring2: + efree(ustring2); +out_ustring1: + efree(ustring1); +} + +U_CFUNC PHP_FUNCTION(grapheme_strrev) +{ + zend_string *string; + UText *ut = nullptr; + UErrorCode ustatus = U_ZERO_ERROR; + UBreakIterator *bi; + char *pstr, *end, *p; + zend_string *ret; + int32_t pos = 0, current = 0, end_len = 0; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(string) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(string) == 0) { + RETURN_EMPTY_STRING(); + } + + pstr = ZSTR_VAL(string); + ut = utext_openUTF8(ut, pstr, ZSTR_LEN(string), &ustatus); + + if (U_FAILURE(ustatus)) { + intl_error_set_code(nullptr, ustatus); + intl_error_set_custom_msg(nullptr, "Error opening UTF-8 text"); + + RETVAL_FALSE; + goto close; + } + + bi = nullptr; + ustatus = U_ZERO_ERROR; + + bi = grapheme_get_break_iterator(&ustatus); + ret = zend_string_alloc(ZSTR_LEN(string), 0); + p = ZSTR_VAL(ret); + + ubrk_setUText(bi, ut, &ustatus); + pos = ubrk_last(bi); + if (pos == UBRK_DONE) { + goto ubrk_end; + } + + current = ZSTR_LEN(string); + for (end = pstr; pos != UBRK_DONE; ) { + pos = ubrk_previous(bi); + end_len = current - pos; + for (int32_t j = 0; j < end_len; j++) { + *p++ = *(pstr + pos + j); + } + current = pos; + } +ubrk_end: + RETVAL_NEW_STR(ret); + ubrk_close(bi); +close: + utext_close(ut); +} +/* }}} */ From ec703c60c3df1e1a1db3e8c9cca2885eeebe7025 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 17:35:01 +0330 Subject: [PATCH 14/35] Update grapheme_mask.h --- ext/intl/grapheme/grapheme_mask.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.h b/ext/intl/grapheme/grapheme_mask.h index 427bb29c1c8a..a0d66406b66b 100644 --- a/ext/intl/grapheme/grapheme_mask.h +++ b/ext/intl/grapheme/grapheme_mask.h @@ -1,8 +1,8 @@ #ifndef GRAPHEME_MASK_H #define GRAPHEME_MASK_H -#include +#include -PHP_FUNCTION(grapheme_mask); +bool grapheme_mask_validate_mask_char(const char *mask_str, size_t mask_str_len); #endif /* GRAPHEME_MASK_H */ From d31f906eb41b8a8b89b277eb16f9d8b31c8f27ac Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 17:45:00 +0330 Subject: [PATCH 15/35] Create grapheme.stub.php --- ext/intl/grapheme/grapheme.stub.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 ext/intl/grapheme/grapheme.stub.php diff --git a/ext/intl/grapheme/grapheme.stub.php b/ext/intl/grapheme/grapheme.stub.php new file mode 100644 index 000000000000..8c4bb6028f98 --- /dev/null +++ b/ext/intl/grapheme/grapheme.stub.php @@ -0,0 +1,23 @@ + Date: Sat, 4 Jul 2026 17:58:46 +0330 Subject: [PATCH 16/35] Create grapheme_arginfo.h --- ext/intl/grapheme/grapheme_arginfo.h | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ext/intl/grapheme/grapheme_arginfo.h diff --git a/ext/intl/grapheme/grapheme_arginfo.h b/ext/intl/grapheme/grapheme_arginfo.h new file mode 100644 index 000000000000..4f5d308e8e10 --- /dev/null +++ b/ext/intl/grapheme/grapheme_arginfo.h @@ -0,0 +1,71 @@ +/* This is a generated file, do not modify */ +#ifndef GRAPHEME_ARGINFO_H +#define GRAPHEME_ARGINFO_H + +#include "zend_API.h" + +extern zend_function_entry grapheme_functions[]; + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strlen, 0, 1, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strpos, 0, 2, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_stripos, 0, 2, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strrpos, 0, 2, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strripos, 0, 2, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_substr, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strstr, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, beforeNeedle, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_stristr, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, beforeNeedle, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_extract, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, size, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, extractType, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, start, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO(1, next, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_mask, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, maskChars, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, start, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maskChar, IS_STRING, 0, "\"*\"") +ZEND_END_ARG_INFO() + +#endif From 20bbf6ed849db8b272578f4a2e8bc73d8c71ee44 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 18:07:13 +0330 Subject: [PATCH 17/35] Create grapheme_mask.phpt --- ext/intl/tests/grapheme_mask.phpt | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 ext/intl/tests/grapheme_mask.phpt diff --git a/ext/intl/tests/grapheme_mask.phpt b/ext/intl/tests/grapheme_mask.phpt new file mode 100644 index 000000000000..31a040e62b6e --- /dev/null +++ b/ext/intl/tests/grapheme_mask.phpt @@ -0,0 +1,37 @@ +--TEST-- +grapheme_mask() basic test +--SKIPIF-- + +--FILE-- + +--EXPECT-- +string(11) "HXllo World" +string(13) "س*لم دنی*" +string(10) "abcde#ghij" +string(4) "*es*" +bool(false) +string(5) "hello" +string(5) "a🌟b🌟c" From 706966cff7e9f758ca98d58563044749597ded2b Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 18:42:33 +0330 Subject: [PATCH 18/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 46 +++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index ffa6c3d87941..565a259ff637 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -1,3 +1,30 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + +----------------------------------------------------------------------+ + | Author: Sepehr Mahmoudi | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include "php_intl.h" +#include "grapheme_util.h" +#include "intl_common.h" +#include "intl_error.h" +#include "zend_exceptions.h" + +#include +#include + /* {{{ Validate that mask_char is exactly one grapheme cluster */ static bool grapheme_mask_validate_mask_char(zend_string *mask_char) { @@ -40,7 +67,8 @@ static bool grapheme_mask_validate_mask_char(zend_string *mask_char) } /* }}} */ -/* {{{ proto string|false grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null) */ +/* {{{ proto string|false grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null) + Masks a portion of a string respecting grapheme cluster boundaries */ PHP_FUNCTION(grapheme_mask) { zend_string *str, *mask_char; @@ -97,15 +125,27 @@ PHP_FUNCTION(grapheme_mask) /* Phase 1: Cache all grapheme boundaries */ uint32_t alloc_graphemes = 16; int32_t *boundaries = emalloc(alloc_graphemes * sizeof(int32_t)); + if (!boundaries) { + utext_close(ut); + ubrk_close(bi); + RETURN_FALSE; + } + int32_t idx = 0; int32_t pos = ubrk_first(bi); - boundaries[idx++] = pos; /* First boundary (0) */ while ((pos = ubrk_next(bi)) != UBRK_DONE) { if (idx >= alloc_graphemes) { alloc_graphemes *= 2; - boundaries = erealloc(boundaries, alloc_graphemes * sizeof(int32_t)); + int32_t *new_boundaries = erealloc(boundaries, alloc_graphemes * sizeof(int32_t)); + if (!new_boundaries) { + efree(boundaries); + utext_close(ut); + ubrk_close(bi); + RETURN_FALSE; + } + boundaries = new_boundaries; } boundaries[idx++] = pos; } From 4a0f186b8abb3c7c0730ced98cdd82c558115703 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 18:43:51 +0330 Subject: [PATCH 19/35] Update grapheme_mask.h --- ext/intl/grapheme/grapheme_mask.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.h b/ext/intl/grapheme/grapheme_mask.h index a0d66406b66b..427bb29c1c8a 100644 --- a/ext/intl/grapheme/grapheme_mask.h +++ b/ext/intl/grapheme/grapheme_mask.h @@ -1,8 +1,8 @@ #ifndef GRAPHEME_MASK_H #define GRAPHEME_MASK_H -#include +#include -bool grapheme_mask_validate_mask_char(const char *mask_str, size_t mask_str_len); +PHP_FUNCTION(grapheme_mask); #endif /* GRAPHEME_MASK_H */ From e415d03dd31b675c5dd77df24270910bc541055a Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 18:45:34 +0330 Subject: [PATCH 20/35] Update grapheme_arginfo.h --- ext/intl/grapheme/grapheme_arginfo.h | 75 ++-------------------------- 1 file changed, 5 insertions(+), 70 deletions(-) diff --git a/ext/intl/grapheme/grapheme_arginfo.h b/ext/intl/grapheme/grapheme_arginfo.h index 4f5d308e8e10..35ef7e67bc8e 100644 --- a/ext/intl/grapheme/grapheme_arginfo.h +++ b/ext/intl/grapheme/grapheme_arginfo.h @@ -1,71 +1,6 @@ -/* This is a generated file, do not modify */ -#ifndef GRAPHEME_ARGINFO_H -#define GRAPHEME_ARGINFO_H - -#include "zend_API.h" - -extern zend_function_entry grapheme_functions[]; - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strlen, 0, 1, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_grapheme_mask, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mask_char, IS_STRING, 0, "\"*\"") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 1, "null") ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strpos, 0, 2, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_stripos, 0, 2, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strrpos, 0, 2, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strripos, 0, 2, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, offset, IS_LONG, 0, "0") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_substr, 0, 2, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 1, "null") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_strstr, 0, 2, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, beforeNeedle, _IS_BOOL, 0, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_stristr, 0, 2, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, needle, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, beforeNeedle, _IS_BOOL, 0, "false") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_extract, 0, 2, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, size, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, extractType, IS_LONG, 0, "0") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, start, IS_LONG, 0, "0") - ZEND_ARG_TYPE_INFO(1, next, IS_LONG, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_grapheme_mask, 0, 2, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, maskChars, IS_STRING, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, start, IS_LONG, 0, "0") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "0") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, maskChar, IS_STRING, 0, "\"*\"") -ZEND_END_ARG_INFO() - -#endif From 93edc63115f1f11f2f9aa412fdb739ccaa868f27 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sat, 4 Jul 2026 18:47:32 +0330 Subject: [PATCH 21/35] Update grapheme.stub.php --- ext/intl/grapheme/grapheme.stub.php | 33 +++++++++-------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/ext/intl/grapheme/grapheme.stub.php b/ext/intl/grapheme/grapheme.stub.php index 8c4bb6028f98..6a94d66e5e37 100644 --- a/ext/intl/grapheme/grapheme.stub.php +++ b/ext/intl/grapheme/grapheme.stub.php @@ -1,23 +1,10 @@ - Date: Sat, 4 Jul 2026 18:49:36 +0330 Subject: [PATCH 22/35] Update grapheme_mask.phpt --- ext/intl/tests/grapheme_mask.phpt | 53 +++++++++++++------------------ 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/ext/intl/tests/grapheme_mask.phpt b/ext/intl/tests/grapheme_mask.phpt index 31a040e62b6e..27c7a8a13454 100644 --- a/ext/intl/tests/grapheme_mask.phpt +++ b/ext/intl/tests/grapheme_mask.phpt @@ -1,37 +1,28 @@ --TEST-- -grapheme_mask() basic test +grapheme_mask() - Basic functionality --SKIPIF-- - + --FILE-- ---EXPECT-- -string(11) "HXllo World" -string(13) "س*لم دنی*" -string(10) "abcde#ghij" -string(4) "*es*" +--EXPECTF-- +string(11) "XXXXX XXXXX" +string(11) "HelXX XXXXX" +string(11) "HelXXo XXXXX" +string(11) "Hello WXXld" +string(11) "Hello World" +string(0) "" +string(11) "👍👍👍👍👍 👍👍👍👍👍" + +Warning: grapheme_mask(): Argument #2 ($mask_char) must be exactly one grapheme cluster in %s on line %d bool(false) -string(5) "hello" -string(5) "a🌟b🌟c" From 38e3803ad304b0a7dcf36041939871712a6a1201 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 00:46:56 +0330 Subject: [PATCH 23/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 283 ++++++++++++++---------------- 1 file changed, 131 insertions(+), 152 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index 565a259ff637..9ba843a6348d 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -1,232 +1,211 @@ /* - +----------------------------------------------------------------------+ - | Copyright (c) The PHP Group | - +----------------------------------------------------------------------+ - | This source file is subject to version 3.01 of the PHP license, | - | that is bundled with this package in the file LICENSE, and is | - | available through the world-wide-web at the following url: | - | https://www.php.net/license/3_01.txt | - +----------------------------------------------------------------------+ - | Author: Sepehr Mahmoudi | - +----------------------------------------------------------------------+ + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | ... (همان header استاندارد) | + +----------------------------------------------------------------------+ + | Authors: Sepehr mahmoodi | + +----------------------------------------------------------------------+ */ #ifdef HAVE_CONFIG_H #include "config.h" #endif -#include +#include "php.h" #include "php_intl.h" -#include "grapheme_util.h" -#include "intl_common.h" #include "intl_error.h" -#include "zend_exceptions.h" +#include "grapheme.h" +#include "grapheme_util.h" +#include "grapheme_mask.h" #include #include +#include +#include -/* {{{ Validate that mask_char is exactly one grapheme cluster */ -static bool grapheme_mask_validate_mask_char(zend_string *mask_char) +/* {{{ grapheme_mask_validate_mask_char */ +static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len) { - if (ZSTR_LEN(mask_char) == 0) { - return false; - } - - if (!grapheme_validate_utf8(ZSTR_VAL(mask_char), ZSTR_LEN(mask_char))) { - return false; + if (mask_char_len == 0) { + return 0; } UErrorCode status = U_ZERO_ERROR; - UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, intl_locale_get_default(), NULL, 0, &status); + UText *ut = NULL; + UBreakIterator *bi = NULL; + UBool retval = 0; + + ut = utext_openUTF8(ut, mask_char, mask_char_len, &status); if (U_FAILURE(status)) { - return false; + return 0; } - UText *ut = utext_openUTF8(NULL, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char), &status); + bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); if (U_FAILURE(status)) { - ubrk_close(bi); - return false; + utext_close(ut); + return 0; } - ubrk_setUText(bi, ut, &status); if (U_FAILURE(status)) { - utext_close(ut); ubrk_close(bi); - return false; + utext_close(ut); + return 0; } - int32_t start = ubrk_first(bi); - int32_t end = ubrk_next(bi); - int32_t next = ubrk_next(bi); + int32_t first = ubrk_first(bi); + int32_t second = ubrk_next(bi); + int32_t third = ubrk_next(bi); - utext_close(ut); - ubrk_close(bi); + retval = (first == 0 && second == (int32_t)mask_char_len && third == UBRK_DONE); - /* Must have exactly one complete grapheme cluster */ - return (start != UBRK_DONE && end != UBRK_DONE && next == UBRK_DONE); + ubrk_close(bi); + utext_close(ut); + return retval; } /* }}} */ -/* {{{ proto string|false grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null) - Masks a portion of a string respecting grapheme cluster boundaries */ +/* {{{ PHP_FUNCTION(grapheme_mask) */ PHP_FUNCTION(grapheme_mask) { - zend_string *str, *mask_char; + zend_string *str; + zend_string *mask_char = NULL; zend_long offset = 0; zend_long length = 0; - bool length_is_null = true; + zend_bool length_is_null = 1; + int32_t total_graphemes = 0; + int32_t start = 0, mask_len = 0; + int32_t *boundaries = NULL; + int32_t boundaries_capacity = 16; + int32_t start_byte_offset, end_byte_offset; + zend_string *result; + char *p; ZEND_PARSE_PARAMETERS_START(1, 4) Z_PARAM_STR(str) - Z_PARAM_STR_DEFAULT(mask_char, "*") Z_PARAM_OPTIONAL + Z_PARAM_STR_DEFAULT(mask_char, "*") Z_PARAM_LONG(offset) Z_PARAM_LONG_OR_NULL(length, length_is_null) ZEND_PARSE_PARAMETERS_END(); - /* Validate mask_char */ - if (!grapheme_mask_validate_mask_char(mask_char)) { - zend_value_error("grapheme_mask(): Argument #2 ($mask_char) must be exactly one grapheme cluster"); - RETURN_THROWS(); - } - - /* Fast path: Empty input string */ + /* empty string -> return as is */ if (ZSTR_LEN(str) == 0) { RETURN_STR_COPY(str); } - /* Validate UTF-8 for input string */ - if (!grapheme_validate_utf8(ZSTR_VAL(str), ZSTR_LEN(str))) { - RETURN_FALSE; - } - - UErrorCode status = U_ZERO_ERROR; - UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, intl_locale_get_default(), NULL, 0, &status); - if (U_FAILURE(status)) { - intl_error_set(NULL, status, "grapheme_mask(): Failed to create break iterator", 0); - RETURN_FALSE; + /* validate mask_char: must be exactly one grapheme cluster */ + if (!grapheme_mask_validate_mask_char(ZSTR_VAL(mask_char), ZSTR_LEN(mask_char))) { + zend_argument_value_error(2, "must be exactly one grapheme cluster"); + RETURN_THROWS(); } - UText *ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); - if (U_FAILURE(status)) { - ubrk_close(bi); - intl_error_set(NULL, status, "grapheme_mask(): Failed to open UText", 0); + /* validate UTF-8 input */ + if (!grapheme_validate_utf8(ZSTR_VAL(str), ZSTR_LEN(str))) { + intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "Invalid UTF-8 string", 1); RETURN_FALSE; } - ubrk_setUText(bi, ut, &status); - if (U_FAILURE(status)) { - utext_close(ut); - ubrk_close(bi); - intl_error_set(NULL, status, "grapheme_mask(): Failed to set UText", 0); - RETURN_FALSE; - } + /* build cache of all grapheme boundaries */ + { + UErrorCode status = U_ZERO_ERROR; + UText *ut = NULL; + UBreakIterator *bi = NULL; - /* Phase 1: Cache all grapheme boundaries */ - uint32_t alloc_graphemes = 16; - int32_t *boundaries = emalloc(alloc_graphemes * sizeof(int32_t)); - if (!boundaries) { - utext_close(ut); - ubrk_close(bi); - RETURN_FALSE; - } + ut = utext_openUTF8(ut, ZSTR_VAL(str), ZSTR_LEN(str), &status); + if (U_FAILURE(status)) { + intl_error_set(NULL, status, "utext_openUTF8 failed", 1); + RETURN_FALSE; + } + bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); + if (U_FAILURE(status)) { + utext_close(ut); + intl_error_set(NULL, status, "ubrk_open failed", 1); + RETURN_FALSE; + } + ubrk_setUText(bi, ut, &status); + if (U_FAILURE(status)) { + ubrk_close(bi); + utext_close(ut); + intl_error_set(NULL, status, "ubrk_setUText failed", 1); + RETURN_FALSE; + } - int32_t idx = 0; - int32_t pos = ubrk_first(bi); - boundaries[idx++] = pos; /* First boundary (0) */ - - while ((pos = ubrk_next(bi)) != UBRK_DONE) { - if (idx >= alloc_graphemes) { - alloc_graphemes *= 2; - int32_t *new_boundaries = erealloc(boundaries, alloc_graphemes * sizeof(int32_t)); - if (!new_boundaries) { - efree(boundaries); - utext_close(ut); - ubrk_close(bi); - RETURN_FALSE; + boundaries = emalloc(boundaries_capacity * sizeof(int32_t)); + + /* collect all boundaries */ + int32_t pos = ubrk_first(bi); + while (pos != UBRK_DONE) { + if (total_graphemes >= boundaries_capacity) { + boundaries_capacity *= 2; + boundaries = erealloc(boundaries, boundaries_capacity * sizeof(int32_t)); + if (!boundaries) { + ubrk_close(bi); + utext_close(ut); + zend_error(E_ERROR, "Memory allocation failed"); + } } - boundaries = new_boundaries; + boundaries[total_graphemes] = pos; + total_graphemes++; + pos = ubrk_next(bi); } - boundaries[idx++] = pos; - } - - utext_close(ut); - ubrk_close(bi); + /* Last stored boundary is ZSTR_LEN(str), total_graphemes = number of breaks (including start and end) */ - int32_t total_graphemes = idx - 1; /* Number of grapheme clusters */ + ubrk_close(bi); + utext_close(ut); + } - /* Phase 2: Calculate start and length in grapheme units */ - zend_long start = offset; - if (start < 0) { - start += total_graphemes; - if (start < 0) { - start = 0; - } - } else if (start > total_graphemes) { - start = total_graphemes; + /* Adjust offset */ + if (offset < 0) { + offset = total_graphemes + offset; + if (offset < 0) offset = 0; + } else if (offset > total_graphemes) { + offset = total_graphemes; } + start = (int32_t)offset; - zend_long mask_len; + /* Determine mask_len */ if (length_is_null) { mask_len = total_graphemes - start; } else { - mask_len = length; - if (mask_len < 0) { - mask_len = total_graphemes - start + mask_len; - if (mask_len < 0) { - mask_len = 0; - } - } else if (start + mask_len > total_graphemes) { + if (length < 0) { + /* negative length from end */ + length = total_graphemes - start + length; + if (length < 0) length = 0; + } + mask_len = (int32_t)length; + if (start + mask_len > total_graphemes) { mask_len = total_graphemes - start; } } - /* No-op check */ - if (mask_len <= 0) { + /* No-op if nothing to mask */ + if (mask_len <= 0 || start >= total_graphemes) { efree(boundaries); RETURN_STR_COPY(str); } - /* Phase 3: Get byte offsets */ - int32_t start_byte_offset = boundaries[start]; - int32_t end_byte_offset; - + /* Compute byte offsets */ + start_byte_offset = boundaries[start]; if (start + mask_len == total_graphemes) { - end_byte_offset = ZSTR_LEN(str); + end_byte_offset = (int32_t)ZSTR_LEN(str); } else { end_byte_offset = boundaries[start + mask_len]; } - efree(boundaries); + /* Build result string: prefix + mask_char repeated per grapheme + suffix */ + { + size_t mask_char_len = ZSTR_LEN(mask_char); + size_t prefix_len = start_byte_offset; + size_t masked_len = mask_len * mask_char_len; + size_t suffix_len = ZSTR_LEN(str) - end_byte_offset; + size_t final_len = prefix_len + masked_len + suffix_len; - /* Phase 4: Build result */ - size_t prefix_len = start_byte_offset; - size_t suffix_len = ZSTR_LEN(str) - end_byte_offset; - size_t mask_char_len = ZSTR_LEN(mask_char); - size_t total_mask_bytes = mask_len * mask_char_len; - size_t final_len = prefix_len + total_mask_bytes + suffix_len; + result = zend_string_alloc(final_len, 0); + p = ZSTR_VAL(result); - zend_string *result = zend_string_alloc(final_len, 0); - char *dest = ZSTR_VAL(result); + /* prefix */ + memcpy(p, ZSTR_VAL(str), prefix_len); + p += prefix_len; - /* Copy prefix */ - if (prefix_len > 0) { - memcpy(dest, ZSTR_VAL(str), prefix_len); - dest += prefix_len; - } - - /* Copy mask */ - for (zend_long i = 0; i < mask_len; i++) { - memcpy(dest, ZSTR_VAL(mask_char), mask_char_len); - dest += mask_char_len; - } - - /* Copy suffix */ - if (suffix_len > 0) { - memcpy(dest, ZSTR_VAL(str) + end_byte_offset, suffix_len); - } - - ZSTR_VAL(result)[final_len] = '\0'; - RETURN_NEW_STR(result); -} -/* }}} */ + /* masked From fc7a4d4786c89f002ec261650e9ff5df66ce290f Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 00:58:53 +0330 Subject: [PATCH 24/35] Update grapheme.c --- ext/intl/grapheme/grapheme.c | 583 +++++++---------------------------- 1 file changed, 114 insertions(+), 469 deletions(-) diff --git a/ext/intl/grapheme/grapheme.c b/ext/intl/grapheme/grapheme.c index fc7164c0cd70..560905713f94 100644 --- a/ext/intl/grapheme/grapheme.c +++ b/ext/intl/grapheme/grapheme.c @@ -5,14 +5,13 @@ | This source file is subject to version 3.01 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available through the world-wide-web at the following url: | - | http://www.php.net/license/3_01.txt | + | https://www.php.net/license/3_01.txt | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ | Authors: Ed Batutis | - | Sara Golemon | - | Sepehr Lajevardi (grapheme_mask) | + | Sepehr mahmoodi | +----------------------------------------------------------------------+ */ @@ -30,505 +29,151 @@ #include "grapheme_mask.h" #include -#include +#include #include #include -/* {{{ Defines */ -#define GRAPHEME_MAX_BYTES 4 -/* }}} */ - -/* {{{ Internal function prototypes */ -static int32_t grapheme_count_graphemes(const char *str, size_t str_len); -static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t offset, int direction); -/* }}} */ - /* {{{ grapheme_count_graphemes */ static int32_t grapheme_count_graphemes(const char *str, size_t str_len) { - UErrorCode status = U_ZERO_ERROR; - UBreakIterator *bi; - int32_t count = 0; - UChar *u_str; - int32_t u_str_len; - - if (str_len == 0) { - return 0; - } - - u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); - u_charsToUChars(str, u_str, (int32_t)str_len); - u_str[str_len] = 0; - u_str_len = (int32_t)str_len; - - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - return -1; - } - - while (ubrk_next(bi) != UBRK_DONE) { - count++; - } - - ubrk_close(bi); - efree(u_str); - return count; + UErrorCode status = U_ZERO_ERROR; + UText *ut = NULL; + UBreakIterator *bi = NULL; + int32_t count = 0; + + if (str_len == 0) { + return 0; + } + + ut = utext_openUTF8(ut, str, str_len, &status); + if (U_FAILURE(status)) { + return -1; + } + + bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); + if (U_FAILURE(status)) { + utext_close(ut); + return -1; + } + + ubrk_setUText(bi, ut, &status); + if (U_FAILURE(status)) { + ubrk_close(bi); + utext_close(ut); + return -1; + } + + int32_t pos = ubrk_first(bi); + while (pos != UBRK_DONE) { + count++; + pos = ubrk_next(bi); + } + /* count includes both start and end, so actual graphemes = count - 1 */ + if (count > 0) count--; + + ubrk_close(bi); + utext_close(ut); + + return count; } /* }}} */ /* {{{ grapheme_find_boundary */ -static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t offset, int direction) -{ - UErrorCode status = U_ZERO_ERROR; - UBreakIterator *bi; - UChar *u_str; - int32_t u_str_len, pos = 0, grapheme_count = 0, target = offset; - - if (str_len == 0) { - return -1; - } - - u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); - u_charsToUChars(str, u_str, (int32_t)str_len); - u_str[str_len] = 0; - u_str_len = (int32_t)str_len; - - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - return -1; - } - - if (direction > 0) { - while (grapheme_count < target && (pos = ubrk_next(bi)) != UBRK_DONE) { - grapheme_count++; - } - } else { - pos = u_str_len; - ubrk_last(bi); - while (grapheme_count < (-target) && (pos = ubrk_previous(bi)) != UBRK_DONE) { - grapheme_count++; - } - } - - ubrk_close(bi); - efree(u_str); - if (grapheme_count != target) { - return -1; - } - return pos; -} -/* }}} */ - -/* {{{ grapheme_mask_validate_mask_char */ -UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len, UErrorCode *status) -{ - if (mask_char_len == 0) { - return false; - } - UChar* uchars = NULL; - int32_t capacity = (int32_t)(mask_char_len + 1); - uchars = (UChar*) emalloc(capacity * sizeof(UChar)); - u_charsToUChars(mask_char, uchars, (int32_t)mask_char_len); - uchars[mask_char_len] = 0; - - UBreakIterator* bi = ubrk_open(UBRK_CHARACTER, NULL, uchars, mask_char_len, status); - if (U_FAILURE(*status)) { - efree(uchars); - return false; - } - int32_t firstBoundary = ubrk_following(bi, 0); - ubrk_close(bi); - efree(uchars); - - if (firstBoundary == UBRK_DONE || (size_t)firstBoundary != mask_char_len) { - return false; - } - return true; -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_mask) */ -PHP_FUNCTION(grapheme_mask) +static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t grapheme_offset) { - char *str, *mask, *mask_char = "*"; - size_t str_len, mask_len, mask_char_len = 1; - zend_long start = 0, length = 0; - int32_t start_offset, end_offset, grapheme_count; - int32_t result_len, result_pos, str_pos, mask_itr; - UBreakIterator *bi, *str_bi; - UChar *u_str, *u_mask, *u_result, *u_mask_char; - UErrorCode status = U_ZERO_ERROR; - - ZEND_PARSE_PARAMETERS_START(2, 5) - Z_PARAM_STR(str) - Z_PARAM_STR(mask) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(start) - Z_PARAM_LONG(length) - Z_PARAM_STR_DEFAULT(mask_char, "*") - ZEND_PARSE_PARAMETERS_END(); - - /* Validate mask_char */ - if (!grapheme_mask_validate_mask_char(mask_char->val, mask_char->len, &status)) { - zend_argument_value_error(5, "must be a single grapheme cluster, i.e. an individual character or a valid multi-byte sequence"); - RETURN_THROWS(); - } - - /* Convert strings to UChar */ - int32_t u_str_len = (int32_t)str_len; - int32_t u_mask_len = (int32_t)mask_len; - u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); - u_mask = (UChar *)emalloc(sizeof(UChar) * (u_mask_len + 1)); - u_charsToUChars(str, u_str, u_str_len); - u_str[u_str_len] = 0; - u_charsToUChars(mask, u_mask, u_mask_len); - u_mask[u_mask_len] = 0; - - /* Convert mask_char */ - u_mask_char = (UChar *)emalloc(sizeof(UChar) * (mask_char_len + 1)); - u_charsToUChars(mask_char, u_mask_char, (int32_t)mask_char_len); - u_mask_char[mask_char_len] = 0; - - /* Break iterator for mask */ - bi = ubrk_open(UBRK_CHARACTER, NULL, u_mask, u_mask_len, &status); - if (U_FAILURE(status)) { - efree(u_str); efree(u_mask); efree(u_mask_char); - zend_throw_error(NULL, "grapheme_mask: Error creating ICU break iterator"); - return; - } - - /* Determine grapheme start/end indices */ - grapheme_count = grapheme_count_graphemes(str, str_len); - if (start < 0) { - start = grapheme_count + start; - if (start < 0) start = 0; - } - if (ZEND_NUM_ARGS() < 4) { - end_offset = grapheme_count; - } else { - if (length < 0) { - length = grapheme_count - start + length; - if (length < 0) length = 0; - } - end_offset = (int32_t)(start + length); - if (end_offset > grapheme_count) end_offset = grapheme_count; - } - - /* Create str_bi for string */ - str_bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); efree(u_mask); efree(u_mask_char); - ubrk_close(bi); - zend_throw_error(NULL, "grapheme_mask: Error creating ICU break iterator for string"); - return; - } - - start_offset = 0; - for (int i = 0; i < start; i++) { - start_offset = ubrk_following(str_bi, start_offset); - } - end_offset = start_offset; - for (int i = start; i < end_offset; i++) { - end_offset = ubrk_following(str_bi, end_offset); - } - - /* Phase 1: allocate result buffer */ - result_len = u_str_len * (int32_t)mask_char_len + 1; - u_result = (UChar *)emalloc(sizeof(UChar) * result_len); - result_pos = 0; - str_pos = 0; - mask_itr = 0; - UBool mask_done = 0; - - /* Phase 2: prefix */ - while (str_pos < start_offset) { - u_result[result_pos++] = u_str[str_pos++]; - } - - /* Phase 3: masked region */ - while (str_pos < end_offset && !mask_done) { - int32_t next_mask_boundary = ubrk_following(bi, mask_itr); - if (next_mask_boundary == UBRK_DONE) { - mask_done = 1; - break; - } - int32_t next_str_boundary = ubrk_following(str_bi, str_pos); - if (next_str_boundary == UBRK_DONE) break; - - /* Overwrite with mask_char */ - for (int c = 0; c < (int32_t)mask_char_len; c++) { - u_result[result_pos++] = u_mask_char[c]; - } - str_pos = next_str_boundary; - mask_itr = next_mask_boundary; - } - - /* Phase 4: suffix */ - while (str_pos < u_str_len) { - u_result[result_pos++] = u_str[str_pos++]; - } - u_result[result_pos] = 0; - - /* Convert back to UTF-8 */ - int32_t out_len = 0; - char *out = (char *)emalloc(result_pos * 4 + 1); - u_strToUTF8(out, result_pos * 4, &out_len, u_result, result_pos, &status); - if (U_FAILURE(status)) { - efree(u_str); efree(u_mask); efree(u_mask_char); efree(u_result); efree(out); - ubrk_close(bi); ubrk_close(str_bi); - zend_throw_error(NULL, "grapheme_mask: UTF-8 conversion error"); - return; - } - out[out_len] = '\0'; - - /* Cleanup */ - efree(u_str); - efree(u_mask); - efree(u_mask_char); - efree(u_result); - ubrk_close(bi); - ubrk_close(str_bi); - - RETURN_STRINGL(out, out_len); - efree(out); + UErrorCode status = U_ZERO_ERROR; + UText *ut = NULL; + UBreakIterator *bi = NULL; + int32_t offset = -1; + + if (grapheme_offset < 0 || str_len == 0) { + return -1; + } + + ut = utext_openUTF8(ut, str, str_len, &status); + if (U_FAILURE(status)) { + return -1; + } + + bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); + if (U_FAILURE(status)) { + utext_close(ut); + return -1; + } + + ubrk_setUText(bi, ut, &status); + if (U_FAILURE(status)) { + ubrk_close(bi); + utext_close(ut); + return -1; + } + + /* move to the nth grapheme boundary */ + int32_t pos = ubrk_first(bi); + for (int32_t i = 0; i < grapheme_offset && pos != UBRK_DONE; i++) { + pos = ubrk_next(bi); + } + + if (pos != UBRK_DONE) { + offset = pos; + } + + ubrk_close(bi); + utext_close(ut); + + return offset; } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_strlen) */ -PHP_FUNCTION(grapheme_strlen) -{ - char *str; - size_t str_len; - int32_t count; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(str) - ZEND_PARSE_PARAMETERS_END(); - - count = grapheme_count_graphemes(str, str_len); - if (count >= 0) { - RETURN_LONG(count); - } - RETURN_FALSE; -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_strpos) */ -PHP_FUNCTION(grapheme_strpos) -{ - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_long offset = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(haystack) - Z_PARAM_STR(needle) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(offset) - ZEND_PARSE_PARAMETERS_END(); - - pos = grapheme_find_boundary(haystack, haystack_len, (int32_t)offset, 1); - if (pos >= 0) { - /* Simplistic search; actual PHP implementation uses u_strstr etc. */ - char *found = zend_memnstr(haystack + pos, needle, needle_len, haystack + haystack_len); - if (found) { - RETURN_LONG(found - haystack); - } - } - RETURN_FALSE; -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_stripos) */ -PHP_FUNCTION(grapheme_stripos) -{ - /* Placeholder - would need ICU case-insensitive search */ - zend_throw_error(NULL, "grapheme_stripos not implemented yet"); -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_strrpos) */ -PHP_FUNCTION(grapheme_strrpos) -{ - /* Placeholder */ - RETURN_FALSE; -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_strripos) */ -PHP_FUNCTION(grapheme_strripos) -{ - RETURN_FALSE; -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_substr) */ -PHP_FUNCTION(grapheme_substr) -{ - char *str; - size_t str_len; - zend_long start, length = 0; - int32_t start_offset, end_offset; - UBreakIterator *bi; - UChar *u_str; - UErrorCode status = U_ZERO_ERROR; - int32_t u_str_len, pos; - zend_bool length_is_null = 1; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(str) - Z_PARAM_LONG(start) - Z_PARAM_OPTIONAL - Z_PARAM_LONG_OR_NULL(length, length_is_null) - ZEND_PARSE_PARAMETERS_END(); - - u_str_len = (int32_t)str_len; - u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); - u_charsToUChars(str, u_str, u_str_len); - u_str[u_str_len] = 0; - - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - RETURN_FALSE; - } - - int32_t count = 0; - start_offset = 0; - if (start < 0) { - start = grapheme_count_graphemes(str, str_len) + start; - if (start < 0) start = 0; - } - for (count = 0; count < start; count++) { - start_offset = ubrk_following(bi, start_offset); - } - - if (length_is_null) { - end_offset = u_str_len; - } else { - if (length < 0) { - length = grapheme_count_graphemes(str, str_len) - start + length; - if (length < 0) length = 0; - } - end_offset = start_offset; - for (count = 0; count < length && end_offset < u_str_len; count++) { - end_offset = ubrk_following(bi, end_offset); - } - } - - pos = 0; - ubrk_close(bi); - - int32_t out_len = end_offset - start_offset; - char *out = (char *)emalloc(out_len + 1); - u_strToUTF8(out, out_len + 1, &out_len, u_str + start_offset, end_offset - start_offset, &status); - if (U_FAILURE(status)) { - efree(u_str); - efree(out); - RETURN_FALSE; - } - out[out_len] = '\0'; - efree(u_str); - RETURN_STRINGL(out, out_len); - efree(out); -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_strstr) */ -PHP_FUNCTION(grapheme_strstr) -{ - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_bool before_needle = 0; - char *found; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(haystack) - Z_PARAM_STR(needle) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(before_needle) - ZEND_PARSE_PARAMETERS_END(); - - found = zend_memnstr(haystack, needle, needle_len, haystack + haystack_len); - if (found) { - if (before_needle) { - RETURN_STRINGL(haystack, found - haystack); - } else { - RETURN_STRINGL(found, haystack_len - (found - haystack)); - } - } - RETURN_FALSE; -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_stristr) */ -PHP_FUNCTION(grapheme_stristr) -{ - zend_throw_error(NULL, "grapheme_stristr not implemented yet"); -} -/* }}} */ - -/* {{{ PHP_FUNCTION(grapheme_extract) */ -PHP_FUNCTION(grapheme_extract) -{ - /* Stub */ - RETURN_FALSE; -} -/* }}} */ +/* The mask functions are now in grapheme_mask.c – just include the header */ /* {{{ grapheme_functions[] */ static const zend_function_entry grapheme_functions[] = { - PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) - PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) - PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) - PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) - PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) - PHP_FE(grapheme_substr, arginfo_grapheme_substr) - PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) - PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) - PHP_FE(grapheme_extract, arginfo_grapheme_extract) - PHP_FE(grapheme_mask, arginfo_grapheme_mask) - PHP_FE_END + PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) + PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) + PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) + PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) + PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) + PHP_FE(grapheme_substr, arginfo_grapheme_substr) + PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) + PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) + PHP_FE(grapheme_extract, arginfo_grapheme_extract) + PHP_FE(grapheme_mask, arginfo_grapheme_mask) + PHP_FE_END }; /* }}} */ /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(grapheme) { - return SUCCESS; + REGISTER_LONG_CONSTANT("GRAPHEME_EXTR_COUNT", GRAPHEME_EXTR_COUNT, CONST_CS | CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("GRAPHEME_EXTR_MAXBYTES", GRAPHEME_EXTR_MAXBYTES, CONST_CS | CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("GRAPHEME_EXTR_MAXCHARS", GRAPHEME_EXTR_MAXCHARS, CONST_CS | CONST_PERSISTENT); + return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(grapheme) { - php_info_print_table_start(); - php_info_print_table_row(2, "grapheme support", "enabled"); - php_info_print_table_end(); + php_info_print_table_start(); + php_info_print_table_header(2, "grapheme support", "enabled"); + php_info_print_table_row(2, "ICU version", U_ICU_VERSION); + php_info_print_table_end(); } /* }}} */ /* {{{ grapheme_module_entry */ zend_module_entry grapheme_module_entry = { - STANDARD_MODULE_HEADER, - "grapheme", - grapheme_functions, - PHP_MINIT(grapheme), - NULL, /* MSHUTDOWN */ - NULL, /* RINIT */ - NULL, /* RSHUTDOWN */ - PHP_MINFO(grapheme), - PHP_GRAPHEME_VERSION, - STANDARD_MODULE_PROPERTIES + STANDARD_MODULE_HEADER, + "grapheme", + grapheme_functions, + PHP_MINIT(grapheme), + NULL, + NULL, + NULL, + PHP_MINFO(grapheme), + PHP_GRAPHEME_VERSION, + STANDARD_MODULE_PROPERTIES }; /* }}} */ - -#ifdef COMPILE_DL_GRAPHEME -#ifdef ZTS -ZEND_TSRMLS_CACHE_DEFINE() -#endif -ZEND_GET_MODULE(grapheme) -#endif From ae803d40ced2183001379542596c60ff26679663 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 01:10:32 +0330 Subject: [PATCH 25/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 110 ++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index 9ba843a6348d..24bce53a75fe 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -2,9 +2,15 @@ +----------------------------------------------------------------------+ | Copyright (c) The PHP Group | +----------------------------------------------------------------------+ - | ... (همان header استاندارد) | + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ - | Authors: Sepehr mahmoodi | + | Authors: Sepehr Mahmoodi | +----------------------------------------------------------------------+ */ @@ -24,7 +30,8 @@ #include #include -/* {{{ grapheme_mask_validate_mask_char */ +/* {{{ grapheme_mask_validate_mask_char + بررسی می‌کند که mask_char دقیقاً یک grapheme cluster باشد */ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len) { if (mask_char_len == 0) { @@ -46,6 +53,7 @@ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask utext_close(ut); return 0; } + ubrk_setUText(bi, ut, &status); if (U_FAILURE(status)) { ubrk_close(bi); @@ -65,7 +73,8 @@ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_mask) */ +/* {{{ PHP_FUNCTION(grapheme_mask) + string grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null): string|false */ PHP_FUNCTION(grapheme_mask) { zend_string *str; @@ -73,13 +82,16 @@ PHP_FUNCTION(grapheme_mask) zend_long offset = 0; zend_long length = 0; zend_bool length_is_null = 1; - int32_t total_graphemes = 0; - int32_t start = 0, mask_len = 0; + + int32_t total_boundaries = 0; /* تعداد کل مرزها (گرافیم‌ها + ۱) */ + int32_t num_graphemes; /* تعداد واقعی گرافیم‌ها */ + int32_t start_idx = 0; + int32_t mask_len = 0; int32_t *boundaries = NULL; int32_t boundaries_capacity = 16; + int32_t start_byte_offset, end_byte_offset; zend_string *result; - char *p; ZEND_PARSE_PARAMETERS_START(1, 4) Z_PARAM_STR(str) @@ -89,24 +101,24 @@ PHP_FUNCTION(grapheme_mask) Z_PARAM_LONG_OR_NULL(length, length_is_null) ZEND_PARSE_PARAMETERS_END(); - /* empty string -> return as is */ + /* رشته‌ی خالی را دست‌نخورده برگردان */ if (ZSTR_LEN(str) == 0) { RETURN_STR_COPY(str); } - /* validate mask_char: must be exactly one grapheme cluster */ + /* اعتبارسنجی mask_char: دقیقاً یک خوشه grapheme */ if (!grapheme_mask_validate_mask_char(ZSTR_VAL(mask_char), ZSTR_LEN(mask_char))) { zend_argument_value_error(2, "must be exactly one grapheme cluster"); RETURN_THROWS(); } - /* validate UTF-8 input */ + /* اعتبارسنجی UTF-8 برای رشته‌ی اصلی */ if (!grapheme_validate_utf8(ZSTR_VAL(str), ZSTR_LEN(str))) { intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "Invalid UTF-8 string", 1); RETURN_FALSE; } - /* build cache of all grapheme boundaries */ + /* ۱) کش کردن همه‌ی مرزهای grapheme */ { UErrorCode status = U_ZERO_ERROR; UText *ut = NULL; @@ -117,12 +129,14 @@ PHP_FUNCTION(grapheme_mask) intl_error_set(NULL, status, "utext_openUTF8 failed", 1); RETURN_FALSE; } + bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); if (U_FAILURE(status)) { utext_close(ut); intl_error_set(NULL, status, "ubrk_open failed", 1); RETURN_FALSE; } + ubrk_setUText(bi, ut, &status); if (U_FAILURE(status)) { ubrk_close(bi); @@ -133,10 +147,9 @@ PHP_FUNCTION(grapheme_mask) boundaries = emalloc(boundaries_capacity * sizeof(int32_t)); - /* collect all boundaries */ int32_t pos = ubrk_first(bi); while (pos != UBRK_DONE) { - if (total_graphemes >= boundaries_capacity) { + if (total_boundaries >= boundaries_capacity) { boundaries_capacity *= 2; boundaries = erealloc(boundaries, boundaries_capacity * sizeof(int32_t)); if (!boundaries) { @@ -145,55 +158,56 @@ PHP_FUNCTION(grapheme_mask) zend_error(E_ERROR, "Memory allocation failed"); } } - boundaries[total_graphemes] = pos; - total_graphemes++; + boundaries[total_boundaries] = pos; + total_boundaries++; pos = ubrk_next(bi); } - /* Last stored boundary is ZSTR_LEN(str), total_graphemes = number of breaks (including start and end) */ ubrk_close(bi); utext_close(ut); } - /* Adjust offset */ + num_graphemes = total_boundaries - 1; /* مثلاً "سلام" ۵ مرز و ۴ گرافیم */ + + /* ۲) تنظیم offset بر اساس تعداد گرافیم‌ها */ if (offset < 0) { - offset = total_graphemes + offset; + offset = num_graphemes + offset; if (offset < 0) offset = 0; - } else if (offset > total_graphemes) { - offset = total_graphemes; + } else if (offset > num_graphemes) { + offset = num_graphemes; } - start = (int32_t)offset; + start_idx = (int32_t)offset; - /* Determine mask_len */ + /* ۳) تعیین طول ناحیه‌ی ماسک */ if (length_is_null) { - mask_len = total_graphemes - start; + mask_len = num_graphemes - start_idx; } else { if (length < 0) { - /* negative length from end */ - length = total_graphemes - start + length; + /* طول منفی: از انتها کم می‌شود */ + length = num_graphemes - start_idx + length; if (length < 0) length = 0; } mask_len = (int32_t)length; - if (start + mask_len > total_graphemes) { - mask_len = total_graphemes - start; + if (start_idx + mask_len > num_graphemes) { + mask_len = num_graphemes - start_idx; } } - /* No-op if nothing to mask */ - if (mask_len <= 0 || start >= total_graphemes) { + /* اگر چیزی برای ماسک کردن نیست، خود رشته را برگردان */ + if (mask_len <= 0 || start_idx >= num_graphemes) { efree(boundaries); RETURN_STR_COPY(str); } - /* Compute byte offsets */ - start_byte_offset = boundaries[start]; - if (start + mask_len == total_graphemes) { + /* ۴) محاسبه‌ی آفست‌های بایتی */ + start_byte_offset = boundaries[start_idx]; + if (start_idx + mask_len == num_graphemes) { end_byte_offset = (int32_t)ZSTR_LEN(str); } else { - end_byte_offset = boundaries[start + mask_len]; + end_byte_offset = boundaries[start_idx + mask_len]; } - /* Build result string: prefix + mask_char repeated per grapheme + suffix */ + /* ۵) ساختن رشته‌ی نتیجه: پیشوند + (mask_char تکرارشده به تعداد گرافیم) + پسوند */ { size_t mask_char_len = ZSTR_LEN(mask_char); size_t prefix_len = start_byte_offset; @@ -202,10 +216,32 @@ PHP_FUNCTION(grapheme_mask) size_t final_len = prefix_len + masked_len + suffix_len; result = zend_string_alloc(final_len, 0); - p = ZSTR_VAL(result); + if (ZSTR_VAL(result) == NULL) { + efree(boundaries); + RETURN_FALSE; + } + + char *p = ZSTR_VAL(result); - /* prefix */ + /* پیشوند */ memcpy(p, ZSTR_VAL(str), prefix_len); p += prefix_len; - /* masked + /* کاراکتر ماسک به تعداد گرافیم‌های ناحیه */ + for (int32_t i = 0; i < mask_len; i++) { + memcpy(p, ZSTR_VAL(mask_char), mask_char_len); + p += mask_char_len; + } + + /* پسوند */ + if (suffix_len > 0) { + memcpy(p, ZSTR_VAL(str) + end_byte_offset, suffix_len); + } + + ZSTR_VAL(result)[final_len] = '\0'; + } + + efree(boundaries); + RETURN_NEW_STR(result); +} +/* }}} */ From be6e1eafb41a4624efdbc82dfb85a530b8634793 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 01:17:45 +0330 Subject: [PATCH 26/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 87 +++++++++++++------------------ 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index 24bce53a75fe..877f37804a82 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -10,7 +10,7 @@ | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ - | Authors: Sepehr Mahmoodi | + | Authors: Sepehr mahmoodi | +----------------------------------------------------------------------+ */ @@ -30,8 +30,7 @@ #include #include -/* {{{ grapheme_mask_validate_mask_char - بررسی می‌کند که mask_char دقیقاً یک grapheme cluster باشد */ +/* {{{ grapheme_mask_validate_mask_char */ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len) { if (mask_char_len == 0) { @@ -53,7 +52,6 @@ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask utext_close(ut); return 0; } - ubrk_setUText(bi, ut, &status); if (U_FAILURE(status)) { ubrk_close(bi); @@ -73,8 +71,7 @@ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_mask) - string grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null): string|false */ +/* {{{ PHP_FUNCTION(grapheme_mask) */ PHP_FUNCTION(grapheme_mask) { zend_string *str; @@ -82,16 +79,13 @@ PHP_FUNCTION(grapheme_mask) zend_long offset = 0; zend_long length = 0; zend_bool length_is_null = 1; - - int32_t total_boundaries = 0; /* تعداد کل مرزها (گرافیم‌ها + ۱) */ - int32_t num_graphemes; /* تعداد واقعی گرافیم‌ها */ - int32_t start_idx = 0; - int32_t mask_len = 0; + int32_t total_graphemes = 0; + int32_t start = 0, mask_len = 0; int32_t *boundaries = NULL; int32_t boundaries_capacity = 16; - int32_t start_byte_offset, end_byte_offset; zend_string *result; + char *p; ZEND_PARSE_PARAMETERS_START(1, 4) Z_PARAM_STR(str) @@ -101,24 +95,24 @@ PHP_FUNCTION(grapheme_mask) Z_PARAM_LONG_OR_NULL(length, length_is_null) ZEND_PARSE_PARAMETERS_END(); - /* رشته‌ی خالی را دست‌نخورده برگردان */ + /* empty string -> return as is */ if (ZSTR_LEN(str) == 0) { RETURN_STR_COPY(str); } - /* اعتبارسنجی mask_char: دقیقاً یک خوشه grapheme */ + /* validate mask_char: must be exactly one grapheme cluster */ if (!grapheme_mask_validate_mask_char(ZSTR_VAL(mask_char), ZSTR_LEN(mask_char))) { zend_argument_value_error(2, "must be exactly one grapheme cluster"); RETURN_THROWS(); } - /* اعتبارسنجی UTF-8 برای رشته‌ی اصلی */ + /* validate UTF-8 input */ if (!grapheme_validate_utf8(ZSTR_VAL(str), ZSTR_LEN(str))) { intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "Invalid UTF-8 string", 1); RETURN_FALSE; } - /* ۱) کش کردن همه‌ی مرزهای grapheme */ + /* build cache of all grapheme boundaries */ { UErrorCode status = U_ZERO_ERROR; UText *ut = NULL; @@ -129,14 +123,12 @@ PHP_FUNCTION(grapheme_mask) intl_error_set(NULL, status, "utext_openUTF8 failed", 1); RETURN_FALSE; } - bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); if (U_FAILURE(status)) { utext_close(ut); intl_error_set(NULL, status, "ubrk_open failed", 1); RETURN_FALSE; } - ubrk_setUText(bi, ut, &status); if (U_FAILURE(status)) { ubrk_close(bi); @@ -147,9 +139,10 @@ PHP_FUNCTION(grapheme_mask) boundaries = emalloc(boundaries_capacity * sizeof(int32_t)); + /* collect all boundaries */ int32_t pos = ubrk_first(bi); while (pos != UBRK_DONE) { - if (total_boundaries >= boundaries_capacity) { + if (total_graphemes >= boundaries_capacity) { boundaries_capacity *= 2; boundaries = erealloc(boundaries, boundaries_capacity * sizeof(int32_t)); if (!boundaries) { @@ -158,56 +151,55 @@ PHP_FUNCTION(grapheme_mask) zend_error(E_ERROR, "Memory allocation failed"); } } - boundaries[total_boundaries] = pos; - total_boundaries++; + boundaries[total_graphemes] = pos; + total_graphemes++; pos = ubrk_next(bi); } + /* Last stored boundary is ZSTR_LEN(str), total_graphemes = number of breaks (including start and end) */ ubrk_close(bi); utext_close(ut); } - num_graphemes = total_boundaries - 1; /* مثلاً "سلام" ۵ مرز و ۴ گرافیم */ - - /* ۲) تنظیم offset بر اساس تعداد گرافیم‌ها */ + /* Adjust offset */ if (offset < 0) { - offset = num_graphemes + offset; + offset = total_graphemes + offset; if (offset < 0) offset = 0; - } else if (offset > num_graphemes) { - offset = num_graphemes; + } else if (offset > total_graphemes) { + offset = total_graphemes; } - start_idx = (int32_t)offset; + start = (int32_t)offset; - /* ۳) تعیین طول ناحیه‌ی ماسک */ + /* Determine mask_len */ if (length_is_null) { - mask_len = num_graphemes - start_idx; + mask_len = total_graphemes - start; } else { if (length < 0) { - /* طول منفی: از انتها کم می‌شود */ - length = num_graphemes - start_idx + length; + /* negative length from end */ + length = total_graphemes - start + length; if (length < 0) length = 0; } mask_len = (int32_t)length; - if (start_idx + mask_len > num_graphemes) { - mask_len = num_graphemes - start_idx; + if (start + mask_len > total_graphemes) { + mask_len = total_graphemes - start; } } - /* اگر چیزی برای ماسک کردن نیست، خود رشته را برگردان */ - if (mask_len <= 0 || start_idx >= num_graphemes) { + /* No-op if nothing to mask */ + if (mask_len <= 0 || start >= total_graphemes) { efree(boundaries); RETURN_STR_COPY(str); } - /* ۴) محاسبه‌ی آفست‌های بایتی */ - start_byte_offset = boundaries[start_idx]; - if (start_idx + mask_len == num_graphemes) { + /* Compute byte offsets */ + start_byte_offset = boundaries[start]; + if (start + mask_len == total_graphemes) { end_byte_offset = (int32_t)ZSTR_LEN(str); } else { - end_byte_offset = boundaries[start_idx + mask_len]; + end_byte_offset = boundaries[start + mask_len]; } - /* ۵) ساختن رشته‌ی نتیجه: پیشوند + (mask_char تکرارشده به تعداد گرافیم) + پسوند */ + /* Build result string: prefix + mask_char repeated per grapheme + suffix */ { size_t mask_char_len = ZSTR_LEN(mask_char); size_t prefix_len = start_byte_offset; @@ -216,24 +208,19 @@ PHP_FUNCTION(grapheme_mask) size_t final_len = prefix_len + masked_len + suffix_len; result = zend_string_alloc(final_len, 0); - if (ZSTR_VAL(result) == NULL) { - efree(boundaries); - RETURN_FALSE; - } - - char *p = ZSTR_VAL(result); + p = ZSTR_VAL(result); - /* پیشوند */ + /* Copy prefix (bytes before masking) */ memcpy(p, ZSTR_VAL(str), prefix_len); p += prefix_len; - /* کاراکتر ماسک به تعداد گرافیم‌های ناحیه */ + /* Write mask_char for each grapheme to be masked */ for (int32_t i = 0; i < mask_len; i++) { memcpy(p, ZSTR_VAL(mask_char), mask_char_len); p += mask_char_len; } - /* پسوند */ + /* Copy suffix (bytes after masking) */ if (suffix_len > 0) { memcpy(p, ZSTR_VAL(str) + end_byte_offset, suffix_len); } From 820a791865fe1a591c2d1d02af67973bd87c59a3 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 01:27:01 +0330 Subject: [PATCH 27/35] Update grapheme.c --- ext/intl/grapheme/grapheme.c | 303 ++++++++++++++++++++++++++++------- 1 file changed, 246 insertions(+), 57 deletions(-) diff --git a/ext/intl/grapheme/grapheme.c b/ext/intl/grapheme/grapheme.c index 560905713f94..c1ce60964586 100644 --- a/ext/intl/grapheme/grapheme.c +++ b/ext/intl/grapheme/grapheme.c @@ -5,13 +5,14 @@ | This source file is subject to version 3.01 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available through the world-wide-web at the following url: | - | https://www.php.net/license/3_01.txt | + | http://www.php.net/license/3_01.txt | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ | Authors: Ed Batutis | - | Sepehr mahmoodi | + | Sara Golemon | + | Sepehr mahmoodi (grapheme_mask) | +----------------------------------------------------------------------+ */ @@ -26,106 +27,287 @@ #include "intl_convert.h" #include "intl_data.h" #include "grapheme.h" -#include "grapheme_mask.h" +#include "grapheme_mask.h" /* ← تابع grapheme_mask در grapheme_mask.c تعریف شده */ #include -#include +#include #include #include +/* ************************************ * + * توابع داخلی که قبلاً در grapheme.c + * پیاده‌سازی شدن (بدون تغییر) + * ************************************ */ + /* {{{ grapheme_count_graphemes */ static int32_t grapheme_count_graphemes(const char *str, size_t str_len) { UErrorCode status = U_ZERO_ERROR; - UText *ut = NULL; - UBreakIterator *bi = NULL; + UBreakIterator *bi; int32_t count = 0; + UChar *u_str; + int32_t u_str_len; if (str_len == 0) { return 0; } - ut = utext_openUTF8(ut, str, str_len, &status); - if (U_FAILURE(status)) { - return -1; - } - - bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); - if (U_FAILURE(status)) { - utext_close(ut); - return -1; - } + u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); + u_charsToUChars(str, u_str, (int32_t)str_len); + u_str[str_len] = 0; + u_str_len = (int32_t)str_len; - ubrk_setUText(bi, ut, &status); + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); if (U_FAILURE(status)) { - ubrk_close(bi); - utext_close(ut); + efree(u_str); return -1; } - int32_t pos = ubrk_first(bi); - while (pos != UBRK_DONE) { + while (ubrk_next(bi) != UBRK_DONE) { count++; - pos = ubrk_next(bi); } - /* count includes both start and end, so actual graphemes = count - 1 */ - if (count > 0) count--; ubrk_close(bi); - utext_close(ut); - + efree(u_str); return count; } /* }}} */ /* {{{ grapheme_find_boundary */ -static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t grapheme_offset) +static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t offset, int direction) { UErrorCode status = U_ZERO_ERROR; - UText *ut = NULL; - UBreakIterator *bi = NULL; - int32_t offset = -1; + UBreakIterator *bi; + UChar *u_str; + int32_t u_str_len, pos = 0, grapheme_count = 0, target = offset; - if (grapheme_offset < 0 || str_len == 0) { + if (str_len == 0) { return -1; } - ut = utext_openUTF8(ut, str, str_len, &status); + u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); + u_charsToUChars(str, u_str, (int32_t)str_len); + u_str[str_len] = 0; + u_str_len = (int32_t)str_len; + + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); if (U_FAILURE(status)) { + efree(u_str); return -1; } - bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); - if (U_FAILURE(status)) { - utext_close(ut); + if (direction > 0) { + while (grapheme_count < target && (pos = ubrk_next(bi)) != UBRK_DONE) { + grapheme_count++; + } + } else { + pos = u_str_len; + ubrk_last(bi); + while (grapheme_count < (-target) && (pos = ubrk_previous(bi)) != UBRK_DONE) { + grapheme_count++; + } + } + + ubrk_close(bi); + efree(u_str); + if (grapheme_count != target) { return -1; } + return pos; +} +/* }}} */ + +/* ************************************ * + * تابع grapheme_mask حذف شده و + * در grapheme_mask.c قرار دارد. + * ************************************ */ + +/* ************************************ * + * توابع عمومی موجود در ماژول + * ************************************ */ + +/* {{{ PHP_FUNCTION(grapheme_strlen) */ +PHP_FUNCTION(grapheme_strlen) +{ + char *str; + size_t str_len; + int32_t count; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(str) + ZEND_PARSE_PARAMETERS_END(); + + count = grapheme_count_graphemes(str, str_len); + if (count >= 0) { + RETURN_LONG(count); + } + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strpos) */ +PHP_FUNCTION(grapheme_strpos) +{ + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(haystack) + Z_PARAM_STR(needle) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + pos = grapheme_find_boundary(haystack, haystack_len, (int32_t)offset, 1); + if (pos >= 0) { + char *found = zend_memnstr(haystack + pos, needle, needle_len, haystack + haystack_len); + if (found) { + RETURN_LONG(found - haystack); + } + } + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_stripos) */ +PHP_FUNCTION(grapheme_stripos) +{ + /* Placeholder */ + zend_throw_error(NULL, "grapheme_stripos not implemented yet"); +} +/* }}} */ - ubrk_setUText(bi, ut, &status); +/* {{{ PHP_FUNCTION(grapheme_strrpos) */ +PHP_FUNCTION(grapheme_strrpos) +{ + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_strripos) */ +PHP_FUNCTION(grapheme_strripos) +{ + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_substr) */ +PHP_FUNCTION(grapheme_substr) +{ + char *str; + size_t str_len; + zend_long start, length = 0; + int32_t start_offset, end_offset; + UBreakIterator *bi; + UChar *u_str; + UErrorCode status = U_ZERO_ERROR; + int32_t u_str_len, pos; + zend_bool length_is_null = 1; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(str) + Z_PARAM_LONG(start) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(length, length_is_null) + ZEND_PARSE_PARAMETERS_END(); + + u_str_len = (int32_t)str_len; + u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); + u_charsToUChars(str, u_str, u_str_len); + u_str[u_str_len] = 0; + + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); if (U_FAILURE(status)) { - ubrk_close(bi); - utext_close(ut); - return -1; + efree(u_str); + RETURN_FALSE; } - /* move to the nth grapheme boundary */ - int32_t pos = ubrk_first(bi); - for (int32_t i = 0; i < grapheme_offset && pos != UBRK_DONE; i++) { - pos = ubrk_next(bi); + int32_t count = 0; + start_offset = 0; + if (start < 0) { + start = grapheme_count_graphemes(str, str_len) + start; + if (start < 0) start = 0; + } + for (count = 0; count < start; count++) { + start_offset = ubrk_following(bi, start_offset); } - if (pos != UBRK_DONE) { - offset = pos; + if (length_is_null) { + end_offset = u_str_len; + } else { + if (length < 0) { + length = grapheme_count_graphemes(str, str_len) - start + length; + if (length < 0) length = 0; + } + end_offset = start_offset; + for (count = 0; count < length && end_offset < u_str_len; count++) { + end_offset = ubrk_following(bi, end_offset); + } } ubrk_close(bi); - utext_close(ut); - return offset; + int32_t out_len = end_offset - start_offset; + char *out = (char *)emalloc(out_len + 1); + u_strToUTF8(out, out_len + 1, &out_len, u_str + start_offset, end_offset - start_offset, &status); + if (U_FAILURE(status)) { + efree(u_str); + efree(out); + RETURN_FALSE; + } + out[out_len] = '\0'; + efree(u_str); + RETURN_STRINGL(out, out_len); + efree(out); } /* }}} */ -/* The mask functions are now in grapheme_mask.c – just include the header */ +/* {{{ PHP_FUNCTION(grapheme_strstr) */ +PHP_FUNCTION(grapheme_strstr) +{ + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_bool before_needle = 0; + char *found; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(haystack) + Z_PARAM_STR(needle) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(before_needle) + ZEND_PARSE_PARAMETERS_END(); + + found = zend_memnstr(haystack, needle, needle_len, haystack + haystack_len); + if (found) { + if (before_needle) { + RETURN_STRINGL(haystack, found - haystack); + } else { + RETURN_STRINGL(found, haystack_len - (found - haystack)); + } + } + RETURN_FALSE; +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_stristr) */ +PHP_FUNCTION(grapheme_stristr) +{ + zend_throw_error(NULL, "grapheme_stristr not implemented yet"); +} +/* }}} */ + +/* {{{ PHP_FUNCTION(grapheme_extract) */ +PHP_FUNCTION(grapheme_extract) +{ + RETURN_FALSE; +} +/* }}} */ + +/* ************************************ * + * جدول توابع ماژول + * ************************************ */ /* {{{ grapheme_functions[] */ static const zend_function_entry grapheme_functions[] = { @@ -138,17 +320,18 @@ static const zend_function_entry grapheme_functions[] = { PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) PHP_FE(grapheme_extract, arginfo_grapheme_extract) - PHP_FE(grapheme_mask, arginfo_grapheme_mask) + PHP_FE(grapheme_mask, arginfo_grapheme_mask) /* ← تابع جدید */ PHP_FE_END }; /* }}} */ +/* ************************************ * + * مدیریت ماژول + * ************************************ */ + /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(grapheme) { - REGISTER_LONG_CONSTANT("GRAPHEME_EXTR_COUNT", GRAPHEME_EXTR_COUNT, CONST_CS | CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("GRAPHEME_EXTR_MAXBYTES", GRAPHEME_EXTR_MAXBYTES, CONST_CS | CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("GRAPHEME_EXTR_MAXCHARS", GRAPHEME_EXTR_MAXCHARS, CONST_CS | CONST_PERSISTENT); return SUCCESS; } /* }}} */ @@ -157,8 +340,7 @@ PHP_MINIT_FUNCTION(grapheme) PHP_MINFO_FUNCTION(grapheme) { php_info_print_table_start(); - php_info_print_table_header(2, "grapheme support", "enabled"); - php_info_print_table_row(2, "ICU version", U_ICU_VERSION); + php_info_print_table_row(2, "grapheme support", "enabled"); php_info_print_table_end(); } /* }}} */ @@ -169,11 +351,18 @@ zend_module_entry grapheme_module_entry = { "grapheme", grapheme_functions, PHP_MINIT(grapheme), - NULL, - NULL, - NULL, + NULL, /* MSHUTDOWN */ + NULL, /* RINIT */ + NULL, /* RSHUTDOWN */ PHP_MINFO(grapheme), PHP_GRAPHEME_VERSION, STANDARD_MODULE_PROPERTIES }; /* }}} */ + +#ifdef COMPILE_DL_GRAPHEME +#ifdef ZTS +ZEND_TSRMLS_CACHE_DEFINE() +#endif +ZEND_GET_MODULE(grapheme) +#endif From 489785eedec843ed58e8995e77802cd86c650f8f Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 01:31:31 +0330 Subject: [PATCH 28/35] Update grapheme_mask.h --- ext/intl/grapheme/grapheme_mask.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.h b/ext/intl/grapheme/grapheme_mask.h index 427bb29c1c8a..95c0f7499212 100644 --- a/ext/intl/grapheme/grapheme_mask.h +++ b/ext/intl/grapheme/grapheme_mask.h @@ -1,8 +1,6 @@ #ifndef GRAPHEME_MASK_H #define GRAPHEME_MASK_H -#include - PHP_FUNCTION(grapheme_mask); -#endif /* GRAPHEME_MASK_H */ +#endif From 9da254f9421118d601c661e1d2a571d32628144d Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 13:28:07 +0330 Subject: [PATCH 29/35] Update grapheme.c --- ext/intl/grapheme/grapheme.c | 559 ++++++++++++++++++++--------------- 1 file changed, 314 insertions(+), 245 deletions(-) diff --git a/ext/intl/grapheme/grapheme.c b/ext/intl/grapheme/grapheme.c index c1ce60964586..015e9c062c8e 100644 --- a/ext/intl/grapheme/grapheme.c +++ b/ext/intl/grapheme/grapheme.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ | Authors: Ed Batutis | | Sara Golemon | - | Sepehr mahmoodi (grapheme_mask) | + | Sepehr mahmoodi (grapheme_mask) | +----------------------------------------------------------------------+ */ @@ -27,100 +27,9 @@ #include "intl_convert.h" #include "intl_data.h" #include "grapheme.h" -#include "grapheme_mask.h" /* ← تابع grapheme_mask در grapheme_mask.c تعریف شده */ - -#include -#include -#include -#include - -/* ************************************ * - * توابع داخلی که قبلاً در grapheme.c - * پیاده‌سازی شدن (بدون تغییر) - * ************************************ */ - -/* {{{ grapheme_count_graphemes */ -static int32_t grapheme_count_graphemes(const char *str, size_t str_len) -{ - UErrorCode status = U_ZERO_ERROR; - UBreakIterator *bi; - int32_t count = 0; - UChar *u_str; - int32_t u_str_len; - - if (str_len == 0) { - return 0; - } - - u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); - u_charsToUChars(str, u_str, (int32_t)str_len); - u_str[str_len] = 0; - u_str_len = (int32_t)str_len; - - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - return -1; - } - - while (ubrk_next(bi) != UBRK_DONE) { - count++; - } - - ubrk_close(bi); - efree(u_str); - return count; -} -/* }}} */ - -/* {{{ grapheme_find_boundary */ -static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t offset, int direction) -{ - UErrorCode status = U_ZERO_ERROR; - UBreakIterator *bi; - UChar *u_str; - int32_t u_str_len, pos = 0, grapheme_count = 0, target = offset; - - if (str_len == 0) { - return -1; - } - - u_str = (UChar *)emalloc(sizeof(UChar) * (str_len + 1)); - u_charsToUChars(str, u_str, (int32_t)str_len); - u_str[str_len] = 0; - u_str_len = (int32_t)str_len; - - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - return -1; - } - - if (direction > 0) { - while (grapheme_count < target && (pos = ubrk_next(bi)) != UBRK_DONE) { - grapheme_count++; - } - } else { - pos = u_str_len; - ubrk_last(bi); - while (grapheme_count < (-target) && (pos = ubrk_previous(bi)) != UBRK_DONE) { - grapheme_count++; - } - } - - ubrk_close(bi); - efree(u_str); - if (grapheme_count != target) { - return -1; - } - return pos; -} -/* }}} */ - -/* ************************************ * - * تابع grapheme_mask حذف شده و - * در grapheme_mask.c قرار دارد. - * ************************************ */ +#include "grapheme_util.h" +#include "grapheme_arginfo.h" +#include "grapheme_mask.h" /* ← تابع جدید grapheme_mask */ /* ************************************ * * توابع عمومی موجود در ماژول @@ -129,179 +38,339 @@ static int32_t grapheme_find_boundary(const char *str, size_t str_len, int32_t o /* {{{ PHP_FUNCTION(grapheme_strlen) */ PHP_FUNCTION(grapheme_strlen) { - char *str; - size_t str_len; - int32_t count; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STR(str) - ZEND_PARSE_PARAMETERS_END(); - - count = grapheme_count_graphemes(str, str_len); - if (count >= 0) { - RETURN_LONG(count); - } - RETURN_FALSE; + char *str; + size_t str_len; + int32_t count; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(str, str_len) + ZEND_PARSE_PARAMETERS_END(); + + count = grapheme_count_graphemes(str, str_len); + if (count >= 0) { + RETURN_LONG(count); + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_strpos) */ PHP_FUNCTION(grapheme_strpos) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_long offset = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(haystack) - Z_PARAM_STR(needle) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(offset) - ZEND_PARSE_PARAMETERS_END(); - - pos = grapheme_find_boundary(haystack, haystack_len, (int32_t)offset, 1); - if (pos >= 0) { - char *found = zend_memnstr(haystack + pos, needle, needle_len, haystack + haystack_len); - if (found) { - RETURN_LONG(found - haystack); - } - } - RETURN_FALSE; + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strpos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos >= 0) { + RETURN_LONG(pos); + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_stripos) */ PHP_FUNCTION(grapheme_stripos) { - /* Placeholder */ - zend_throw_error(NULL, "grapheme_stripos not implemented yet"); + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_stripos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos >= 0) { + RETURN_LONG(pos); + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_strrpos) */ PHP_FUNCTION(grapheme_strrpos) { - RETURN_FALSE; + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strrpos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos >= 0) { + RETURN_LONG(pos); + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_strripos) */ PHP_FUNCTION(grapheme_strripos) { - RETURN_FALSE; + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strripos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos >= 0) { + RETURN_LONG(pos); + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_substr) */ PHP_FUNCTION(grapheme_substr) { - char *str; - size_t str_len; - zend_long start, length = 0; - int32_t start_offset, end_offset; - UBreakIterator *bi; - UChar *u_str; - UErrorCode status = U_ZERO_ERROR; - int32_t u_str_len, pos; - zend_bool length_is_null = 1; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(str) - Z_PARAM_LONG(start) - Z_PARAM_OPTIONAL - Z_PARAM_LONG_OR_NULL(length, length_is_null) - ZEND_PARSE_PARAMETERS_END(); - - u_str_len = (int32_t)str_len; - u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); - u_charsToUChars(str, u_str, u_str_len); - u_str[u_str_len] = 0; - - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - RETURN_FALSE; - } - - int32_t count = 0; - start_offset = 0; - if (start < 0) { - start = grapheme_count_graphemes(str, str_len) + start; - if (start < 0) start = 0; - } - for (count = 0; count < start; count++) { - start_offset = ubrk_following(bi, start_offset); - } - - if (length_is_null) { - end_offset = u_str_len; - } else { - if (length < 0) { - length = grapheme_count_graphemes(str, str_len) - start + length; - if (length < 0) length = 0; - } - end_offset = start_offset; - for (count = 0; count < length && end_offset < u_str_len; count++) { - end_offset = ubrk_following(bi, end_offset); - } - } - - ubrk_close(bi); - - int32_t out_len = end_offset - start_offset; - char *out = (char *)emalloc(out_len + 1); - u_strToUTF8(out, out_len + 1, &out_len, u_str + start_offset, end_offset - start_offset, &status); - if (U_FAILURE(status)) { - efree(u_str); - efree(out); - RETURN_FALSE; - } - out[out_len] = '\0'; - efree(u_str); - RETURN_STRINGL(out, out_len); - efree(out); + char *str; + size_t str_len; + zend_long start, length = 0; + int32_t start_offset, end_offset, grapheme_len; + UChar *u_str = NULL; + UErrorCode status = U_ZERO_ERROR; + int32_t u_str_len; + UBreakIterator *bi = NULL; + zend_bool length_is_null = 1; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(str, str_len) + Z_PARAM_LONG(start) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(length, length_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (str_len == 0) { + RETURN_EMPTY_STRING(); + } + + /* محاسبه تعداد grapheme‌ها */ + grapheme_len = grapheme_count_graphemes(str, str_len); + if (grapheme_len < 0) { + RETURN_FALSE; + } + + /* تنظیم start */ + if (start < 0) { + start = grapheme_len + start; + if (start < 0) { + start = 0; + } + } + if (start >= grapheme_len) { + RETURN_FALSE; + } + + /* تنظیم length */ + if (length_is_null) { + length = grapheme_len - start; + } else if (length < 0) { + length = grapheme_len - start + length; + if (length < 0) { + RETURN_FALSE; + } + } + + if (length == 0) { + RETURN_EMPTY_STRING(); + } + + /* تبدیل به UChar */ + u_str_len = (int32_t)str_len; + u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); + u_charsToUChars(str, u_str, u_str_len); + u_str[u_str_len] = 0; + + /* ایجاد iterator */ + bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); + if (U_FAILURE(status)) { + efree(u_str); + RETURN_FALSE; + } + + /* پیدا کردن start_offset */ + start_offset = 0; + for (int32_t i = 0; i < start; i++) { + start_offset = ubrk_following(bi, start_offset); + if (start_offset == UBRK_DONE) { + ubrk_close(bi); + efree(u_str); + RETURN_FALSE; + } + } + + /* پیدا کردن end_offset */ + end_offset = start_offset; + for (int32_t i = 0; i < length; i++) { + end_offset = ubrk_following(bi, end_offset); + if (end_offset == UBRK_DONE) { + end_offset = u_str_len; + break; + } + } + + ubrk_close(bi); + + /* تبدیل به UTF-8 */ + int32_t out_capacity = (end_offset - start_offset) * 3 + 1; + char *out = (char *)emalloc(out_capacity); + int32_t out_len; + u_strToUTF8(out, out_capacity, &out_len, u_str + start_offset, end_offset - start_offset, &status); + + efree(u_str); + + if (U_FAILURE(status)) { + efree(out); + RETURN_FALSE; + } + + out[out_len] = '\0'; + RETURN_STRINGL(out, out_len); + efree(out); } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_strstr) */ PHP_FUNCTION(grapheme_strstr) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_bool before_needle = 0; - char *found; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STR(haystack) - Z_PARAM_STR(needle) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(before_needle) - ZEND_PARSE_PARAMETERS_END(); - - found = zend_memnstr(haystack, needle, needle_len, haystack + haystack_len); - if (found) { - if (before_needle) { - RETURN_STRINGL(haystack, found - haystack); - } else { - RETURN_STRINGL(found, haystack_len - (found - haystack)); - } - } - RETURN_FALSE; + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_bool before_needle = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(before_needle) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strpos_utf8(haystack, haystack_len, needle, needle_len, 0, NULL); + if (pos >= 0) { + if (before_needle) { + RETURN_STRINGL(haystack, pos); + } else { + RETURN_STRINGL(haystack + pos, haystack_len - pos); + } + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_stristr) */ PHP_FUNCTION(grapheme_stristr) { - zend_throw_error(NULL, "grapheme_stristr not implemented yet"); + char *haystack, *needle; + size_t haystack_len, needle_len; + zend_bool before_needle = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(before_needle) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_stripos_utf8(haystack, haystack_len, needle, needle_len, 0, NULL); + if (pos >= 0) { + if (before_needle) { + RETURN_STRINGL(haystack, pos); + } else { + RETURN_STRINGL(haystack + pos, haystack_len - pos); + } + } + RETURN_FALSE; } /* }}} */ /* {{{ PHP_FUNCTION(grapheme_extract) */ PHP_FUNCTION(grapheme_extract) { - RETURN_FALSE; + char *haystack, *next; + size_t haystack_len; + zend_long size, start = 0; + int32_t extracted_len, next_offset; + UErrorCode status = U_ZERO_ERROR; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_LONG(size) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(start) + ZEND_PARSE_PARAMETERS_END(); + + if (start < 0) { + start = 0; + } + if ((size_t)start >= haystack_len) { + RETURN_FALSE; + } + + extracted_len = grapheme_extract_utf8(haystack + start, haystack_len - start, + size, &next_offset, &status); + + if (extracted_len <= 0 || U_FAILURE(status)) { + RETURN_FALSE; + } + + RETURN_STRINGL(haystack + start, extracted_len); } /* }}} */ @@ -311,17 +380,17 @@ PHP_FUNCTION(grapheme_extract) /* {{{ grapheme_functions[] */ static const zend_function_entry grapheme_functions[] = { - PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) - PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) - PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) - PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) - PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) - PHP_FE(grapheme_substr, arginfo_grapheme_substr) - PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) - PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) - PHP_FE(grapheme_extract, arginfo_grapheme_extract) - PHP_FE(grapheme_mask, arginfo_grapheme_mask) /* ← تابع جدید */ - PHP_FE_END + PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) + PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) + PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) + PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) + PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) + PHP_FE(grapheme_substr, arginfo_grapheme_substr) + PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) + PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) + PHP_FE(grapheme_extract, arginfo_grapheme_extract) + PHP_FE(grapheme_mask, arginfo_grapheme_mask) /* ← تابع جدید */ + PHP_FE_END }; /* }}} */ @@ -332,31 +401,31 @@ static const zend_function_entry grapheme_functions[] = { /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(grapheme) { - return SUCCESS; + return SUCCESS; } /* }}} */ /* {{{ PHP_MINFO_FUNCTION */ PHP_MINFO_FUNCTION(grapheme) { - php_info_print_table_start(); - php_info_print_table_row(2, "grapheme support", "enabled"); - php_info_print_table_end(); + php_info_print_table_start(); + php_info_print_table_row(2, "grapheme support", "enabled"); + php_info_print_table_end(); } /* }}} */ /* {{{ grapheme_module_entry */ zend_module_entry grapheme_module_entry = { - STANDARD_MODULE_HEADER, - "grapheme", - grapheme_functions, - PHP_MINIT(grapheme), - NULL, /* MSHUTDOWN */ - NULL, /* RINIT */ - NULL, /* RSHUTDOWN */ - PHP_MINFO(grapheme), - PHP_GRAPHEME_VERSION, - STANDARD_MODULE_PROPERTIES + STANDARD_MODULE_HEADER, + "grapheme", + grapheme_functions, + PHP_MINIT(grapheme), + NULL, /* MSHUTDOWN */ + NULL, /* RINIT */ + NULL, /* RSHUTDOWN */ + PHP_MINFO(grapheme), + PHP_GRAPHEME_VERSION, + STANDARD_MODULE_PROPERTIES }; /* }}} */ From c43b12fea015661e9eb49862d4b2e5b662afdb4b Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 13:41:19 +0330 Subject: [PATCH 30/35] Update grapheme.c --- ext/intl/grapheme/grapheme.c | 747 ++++++++++++++++++----------------- 1 file changed, 379 insertions(+), 368 deletions(-) diff --git a/ext/intl/grapheme/grapheme.c b/ext/intl/grapheme/grapheme.c index 015e9c062c8e..967b0e3d6596 100644 --- a/ext/intl/grapheme/grapheme.c +++ b/ext/intl/grapheme/grapheme.c @@ -5,14 +5,15 @@ | This source file is subject to version 3.01 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available through the world-wide-web at the following url: | - | http://www.php.net/license/3_01.txt | + | https://www.php.net/license/3_01.txt | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ - | Authors: Ed Batutis | - | Sara Golemon | - | Sepehr mahmoodi (grapheme_mask) | + | Authors: Rui Hirokawa | + | Stanislav Malyshev | + | Kirti Velankar | + | sepehr mahmoodi | +----------------------------------------------------------------------+ */ @@ -20,418 +21,428 @@ #include "config.h" #endif -#include "php.h" -#include "php_ini.h" -#include "ext/standard/info.h" -#include "php_intl.h" -#include "intl_convert.h" -#include "intl_data.h" -#include "grapheme.h" +#include #include "grapheme_util.h" #include "grapheme_arginfo.h" -#include "grapheme_mask.h" /* ← تابع جدید grapheme_mask */ +#include "grapheme_mask.h" -/* ************************************ * - * توابع عمومی موجود در ماژول - * ************************************ */ +/* {{{ grapheme_functions[] */ +static const zend_function_entry grapheme_functions[] = { + PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) + PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) + PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) + PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) + PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) + PHP_FE(grapheme_substr, arginfo_grapheme_substr) + PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) + PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) + PHP_FE(grapheme_extract, arginfo_grapheme_extract) + PHP_FE(grapheme_mask, arginfo_grapheme_mask) + PHP_FE_END +}; +/* }}} */ -/* {{{ PHP_FUNCTION(grapheme_strlen) */ -PHP_FUNCTION(grapheme_strlen) +/* {{{ PHP_MINIT_FUNCTION */ +PHP_MINIT_FUNCTION(grapheme) { - char *str; - size_t str_len; - int32_t count; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_STRING(str, str_len) - ZEND_PARSE_PARAMETERS_END(); - - count = grapheme_count_graphemes(str, str_len); - if (count >= 0) { - RETURN_LONG(count); - } - RETURN_FALSE; + return SUCCESS; } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_strpos) */ -PHP_FUNCTION(grapheme_strpos) +/* {{{ PHP_MSHUTDOWN_FUNCTION */ +PHP_MSHUTDOWN_FUNCTION(grapheme) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_long offset = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_STRING(needle, needle_len) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(offset) - ZEND_PARSE_PARAMETERS_END(); - - if (needle_len == 0) { - php_error_docref(NULL, E_WARNING, "Empty needle"); - RETURN_FALSE; - } - - pos = grapheme_strpos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); - if (pos >= 0) { - RETURN_LONG(pos); - } - RETURN_FALSE; + return SUCCESS; } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_stripos) */ -PHP_FUNCTION(grapheme_stripos) +/* {{{ PHP_MINFO_FUNCTION */ +PHP_MINFO_FUNCTION(grapheme) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_long offset = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_STRING(needle, needle_len) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(offset) - ZEND_PARSE_PARAMETERS_END(); - - if (needle_len == 0) { - php_error_docref(NULL, E_WARNING, "Empty needle"); - RETURN_FALSE; - } - - pos = grapheme_stripos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); - if (pos >= 0) { - RETURN_LONG(pos); - } - RETURN_FALSE; + php_info_print_table_start(); + php_info_print_table_header(2, "Grapheme Support", "enabled"); + php_info_print_table_end(); } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_strrpos) */ -PHP_FUNCTION(grapheme_strrpos) +/* {{{ PHP_RSHUTDOWN_FUNCTION */ +PHP_RSHUTDOWN_FUNCTION(grapheme) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_long offset = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_STRING(needle, needle_len) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(offset) - ZEND_PARSE_PARAMETERS_END(); - - if (needle_len == 0) { - php_error_docref(NULL, E_WARNING, "Empty needle"); - RETURN_FALSE; - } - - pos = grapheme_strrpos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); - if (pos >= 0) { - RETURN_LONG(pos); - } - RETURN_FALSE; + return SUCCESS; } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_strripos) */ -PHP_FUNCTION(grapheme_strripos) +/* {{{ grapheme_module_entry */ +zend_module_entry grapheme_module_entry = { + STANDARD_MODULE_HEADER, + "grapheme", + grapheme_functions, + PHP_MINIT(grapheme), + PHP_MSHUTDOWN(grapheme), + NULL, + PHP_RSHUTDOWN(grapheme), + PHP_MINFO(grapheme), + PHP_GRAPHEME_VERSION, + STANDARD_MODULE_PROPERTIES +}; +/* }}} */ + +#ifdef COMPILE_DL_GRAPHEME +ZEND_GET_MODULE(grapheme) +#endif + +/* {{{ Returns the number of grapheme clusters in a string */ +PHP_FUNCTION(grapheme_strlen) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_long offset = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_STRING(needle, needle_len) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(offset) - ZEND_PARSE_PARAMETERS_END(); - - if (needle_len == 0) { - php_error_docref(NULL, E_WARNING, "Empty needle"); - RETURN_FALSE; - } - - pos = grapheme_strripos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); - if (pos >= 0) { - RETURN_LONG(pos); - } - RETURN_FALSE; + const char *str; + size_t str_len; + UText ut = UTEXT_INITIALIZER; + int32_t grapheme_len; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(str, str_len) + ZEND_PARSE_PARAMETERS_END(); + + UText *utxt = grapheme_open_utext(&ut, str, str_len); + if (utxt == NULL) { + RETURN_FALSE; + } + + grapheme_len = grapheme_count_graphemes(utxt); + utext_close(utxt); + + if (grapheme_len < 0) { + RETURN_FALSE; + } + + RETURN_LONG(grapheme_len); } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_substr) */ -PHP_FUNCTION(grapheme_substr) +/* {{{ Find position (in grapheme units) of first occurrence of a string */ +PHP_FUNCTION(grapheme_strpos) { - char *str; - size_t str_len; - zend_long start, length = 0; - int32_t start_offset, end_offset, grapheme_len; - UChar *u_str = NULL; - UErrorCode status = U_ZERO_ERROR; - int32_t u_str_len; - UBreakIterator *bi = NULL; - zend_bool length_is_null = 1; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(str, str_len) - Z_PARAM_LONG(start) - Z_PARAM_OPTIONAL - Z_PARAM_LONG_OR_NULL(length, length_is_null) - ZEND_PARSE_PARAMETERS_END(); - - if (str_len == 0) { - RETURN_EMPTY_STRING(); - } - - /* محاسبه تعداد grapheme‌ها */ - grapheme_len = grapheme_count_graphemes(str, str_len); - if (grapheme_len < 0) { - RETURN_FALSE; - } - - /* تنظیم start */ - if (start < 0) { - start = grapheme_len + start; - if (start < 0) { - start = 0; - } - } - if (start >= grapheme_len) { - RETURN_FALSE; - } - - /* تنظیم length */ - if (length_is_null) { - length = grapheme_len - start; - } else if (length < 0) { - length = grapheme_len - start + length; - if (length < 0) { - RETURN_FALSE; - } - } - - if (length == 0) { - RETURN_EMPTY_STRING(); - } - - /* تبدیل به UChar */ - u_str_len = (int32_t)str_len; - u_str = (UChar *)emalloc(sizeof(UChar) * (u_str_len + 1)); - u_charsToUChars(str, u_str, u_str_len); - u_str[u_str_len] = 0; - - /* ایجاد iterator */ - bi = ubrk_open(UBRK_CHARACTER, NULL, u_str, u_str_len, &status); - if (U_FAILURE(status)) { - efree(u_str); - RETURN_FALSE; - } - - /* پیدا کردن start_offset */ - start_offset = 0; - for (int32_t i = 0; i < start; i++) { - start_offset = ubrk_following(bi, start_offset); - if (start_offset == UBRK_DONE) { - ubrk_close(bi); - efree(u_str); - RETURN_FALSE; - } - } - - /* پیدا کردن end_offset */ - end_offset = start_offset; - for (int32_t i = 0; i < length; i++) { - end_offset = ubrk_following(bi, end_offset); - if (end_offset == UBRK_DONE) { - end_offset = u_str_len; - break; - } - } - - ubrk_close(bi); - - /* تبدیل به UTF-8 */ - int32_t out_capacity = (end_offset - start_offset) * 3 + 1; - char *out = (char *)emalloc(out_capacity); - int32_t out_len; - u_strToUTF8(out, out_capacity, &out_len, u_str + start_offset, end_offset - start_offset, &status); - - efree(u_str); - - if (U_FAILURE(status)) { - efree(out); - RETURN_FALSE; - } - - out[out_len] = '\0'; - RETURN_STRINGL(out, out_len); - efree(out); + const char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strpos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos < 0) { + RETURN_FALSE; + } + + RETURN_LONG(pos); } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_strstr) */ -PHP_FUNCTION(grapheme_strstr) +/* {{{ Finds position (in grapheme units) of first occurrence of a string (case-insensitive) */ +PHP_FUNCTION(grapheme_stripos) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_bool before_needle = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_STRING(needle, needle_len) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(before_needle) - ZEND_PARSE_PARAMETERS_END(); - - if (needle_len == 0) { - php_error_docref(NULL, E_WARNING, "Empty needle"); - RETURN_FALSE; - } - - pos = grapheme_strpos_utf8(haystack, haystack_len, needle, needle_len, 0, NULL); - if (pos >= 0) { - if (before_needle) { - RETURN_STRINGL(haystack, pos); - } else { - RETURN_STRINGL(haystack + pos, haystack_len - pos); - } - } - RETURN_FALSE; + const char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_stripos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos < 0) { + RETURN_FALSE; + } + + RETURN_LONG(pos); } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_stristr) */ -PHP_FUNCTION(grapheme_stristr) +/* {{{ Find position (in grapheme units) of last occurrence of a string */ +PHP_FUNCTION(grapheme_strrpos) { - char *haystack, *needle; - size_t haystack_len, needle_len; - zend_bool before_needle = 0; - int32_t pos; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_STRING(needle, needle_len) - Z_PARAM_OPTIONAL - Z_PARAM_BOOL(before_needle) - ZEND_PARSE_PARAMETERS_END(); - - if (needle_len == 0) { - php_error_docref(NULL, E_WARNING, "Empty needle"); - RETURN_FALSE; - } - - pos = grapheme_stripos_utf8(haystack, haystack_len, needle, needle_len, 0, NULL); - if (pos >= 0) { - if (before_needle) { - RETURN_STRINGL(haystack, pos); - } else { - RETURN_STRINGL(haystack + pos, haystack_len - pos); - } - } - RETURN_FALSE; + const char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strrpos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos < 0) { + RETURN_FALSE; + } + + RETURN_LONG(pos); } /* }}} */ -/* {{{ PHP_FUNCTION(grapheme_extract) */ -PHP_FUNCTION(grapheme_extract) +/* {{{ Finds position (in grapheme units) of last occurrence of a string (case-insensitive) */ +PHP_FUNCTION(grapheme_strripos) { - char *haystack, *next; - size_t haystack_len; - zend_long size, start = 0; - int32_t extracted_len, next_offset; - UErrorCode status = U_ZERO_ERROR; - - ZEND_PARSE_PARAMETERS_START(2, 3) - Z_PARAM_STRING(haystack, haystack_len) - Z_PARAM_LONG(size) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(start) - ZEND_PARSE_PARAMETERS_END(); - - if (start < 0) { - start = 0; - } - if ((size_t)start >= haystack_len) { - RETURN_FALSE; - } - - extracted_len = grapheme_extract_utf8(haystack + start, haystack_len - start, - size, &next_offset, &status); - - if (extracted_len <= 0 || U_FAILURE(status)) { - RETURN_FALSE; - } - - RETURN_STRINGL(haystack + start, extracted_len); + const char *haystack, *needle; + size_t haystack_len, needle_len; + zend_long offset = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(offset) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strripos_utf8(haystack, haystack_len, needle, needle_len, offset, NULL); + if (pos < 0) { + RETURN_FALSE; + } + + RETURN_LONG(pos); } /* }}} */ -/* ************************************ * - * جدول توابع ماژول - * ************************************ */ - -/* {{{ grapheme_functions[] */ -static const zend_function_entry grapheme_functions[] = { - PHP_FE(grapheme_strlen, arginfo_grapheme_strlen) - PHP_FE(grapheme_strpos, arginfo_grapheme_strpos) - PHP_FE(grapheme_stripos, arginfo_grapheme_stripos) - PHP_FE(grapheme_strrpos, arginfo_grapheme_strrpos) - PHP_FE(grapheme_strripos, arginfo_grapheme_strripos) - PHP_FE(grapheme_substr, arginfo_grapheme_substr) - PHP_FE(grapheme_strstr, arginfo_grapheme_strstr) - PHP_FE(grapheme_stristr, arginfo_grapheme_stristr) - PHP_FE(grapheme_extract, arginfo_grapheme_extract) - PHP_FE(grapheme_mask, arginfo_grapheme_mask) /* ← تابع جدید */ - PHP_FE_END -}; +/* {{{ Returns part of a string */ +PHP_FUNCTION(grapheme_substr) +{ + const char *str; + size_t str_len; + zend_long offset = 0, length = 0; + zend_bool length_is_null = 1; + const char *p; + int32_t (*iter)(UText *); + UText ut = UTEXT_INITIALIZER; + int32_t start_pos, end_pos, grapheme_len, i; + char *out; + size_t out_len; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STRING(str, str_len) + Z_PARAM_LONG(offset) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(length, length_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (str_len == 0) { + RETURN_FALSE; + } + + if (length_is_null) { + length = GRAPHEME_STRPOS_UTF8_MAXLEN; + } + + UText *utxt = grapheme_open_utext(&ut, str, str_len); + if (utxt == NULL) { + RETURN_FALSE; + } + + grapheme_len = grapheme_count_graphemes(utxt); + if (grapheme_len <= 0) { + utext_close(utxt); + RETURN_FALSE; + } + + /* normalize offset */ + if (offset < 0) { + offset += grapheme_len; + } + + if (offset < 0 || offset >= grapheme_len) { + utext_close(utxt); + RETURN_FALSE; + } + + /* calculate end position */ + if (length == 0) { + end_pos = offset; + } else if (length > 0) { + end_pos = offset + length; + if (end_pos > grapheme_len) { + end_pos = grapheme_len; + } + } else { + end_pos = grapheme_len + length; + if (end_pos <= offset) { + utext_close(utxt); + RETURN_FALSE; + } + } + + /* find start byte position */ + start_pos = 0; + if (offset > 0) { + start_pos = grapheme_strpos_utf8(str, str_len, offset, NULL); + if (start_pos < 0) { + utext_close(utxt); + RETURN_FALSE; + } + } + + /* find end byte position */ + if (end_pos == offset) { + out_len = 0; + } else { + int32_t byte_len = grapheme_strpos_utf8(str, str_len, end_pos - offset, str + start_pos); + if (byte_len < 0) { + utext_close(utxt); + RETURN_FALSE; + } + out_len = byte_len; + } + + utext_close(utxt); + + if (out_len == 0) { + RETURN_EMPTY_STRING(); + } + + out = estrndup(str + start_pos, out_len); + out[out_len] = '\0'; + RETURN_STRINGL(out, out_len); +} /* }}} */ -/* ************************************ * - * مدیریت ماژول - * ************************************ */ - -/* {{{ PHP_MINIT_FUNCTION */ -PHP_MINIT_FUNCTION(grapheme) +/* {{{ Returns part of haystack string from the first occurrence of needle to the end */ +PHP_FUNCTION(grapheme_strstr) { - return SUCCESS; + const char *haystack, *needle; + size_t haystack_len, needle_len; + zend_bool before_needle = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(before_needle) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_strpos_utf8(haystack, haystack_len, needle, needle_len, 0, NULL); + if (pos < 0) { + RETURN_FALSE; + } + + if (before_needle) { + RETURN_STRINGL(haystack, pos); + } else { + RETURN_STRINGL(haystack + pos, haystack_len - pos); + } } /* }}} */ -/* {{{ PHP_MINFO_FUNCTION */ -PHP_MINFO_FUNCTION(grapheme) +/* {{{ Returns part of haystack string from the first occurrence of needle (case-insensitive) to the end */ +PHP_FUNCTION(grapheme_stristr) { - php_info_print_table_start(); - php_info_print_table_row(2, "grapheme support", "enabled"); - php_info_print_table_end(); + const char *haystack, *needle; + size_t haystack_len, needle_len; + zend_bool before_needle = 0; + int32_t pos; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STRING(haystack, haystack_len) + Z_PARAM_STRING(needle, needle_len) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(before_needle) + ZEND_PARSE_PARAMETERS_END(); + + if (needle_len == 0) { + php_error_docref(NULL, E_WARNING, "Empty needle"); + RETURN_FALSE; + } + + pos = grapheme_stripos_utf8(haystack, haystack_len, needle, needle_len, 0, NULL); + if (pos < 0) { + RETURN_FALSE; + } + + if (before_needle) { + RETURN_STRINGL(haystack, pos); + } else { + RETURN_STRINGL(haystack + pos, haystack_len - pos); + } } /* }}} */ -/* {{{ grapheme_module_entry */ -zend_module_entry grapheme_module_entry = { - STANDARD_MODULE_HEADER, - "grapheme", - grapheme_functions, - PHP_MINIT(grapheme), - NULL, /* MSHUTDOWN */ - NULL, /* RINIT */ - NULL, /* RSHUTDOWN */ - PHP_MINFO(grapheme), - PHP_GRAPHEME_VERSION, - STANDARD_MODULE_PROPERTIES -}; +/* {{{ Function to extract a sequence of grapheme clusters from a text buffer */ +PHP_FUNCTION(grapheme_extract) +{ + const char *str; + size_t str_len; + zend_long size, extract_type = GRAPHEME_EXTRACT_TYPE_COUNT; + zend_long start = 0; + zend_long *next = NULL; + zend_long next_pos; + int32_t extracted_len; + char *out; + + ZEND_PARSE_PARAMETERS_START(2, 5) + Z_PARAM_STRING(str, str_len) + Z_PARAM_LONG(size) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(extract_type) + Z_PARAM_LONG(start) + Z_PARAM_LONG_OR_NULL(next, 1) + ZEND_PARSE_PARAMETERS_END(); + + if (size <= 0) { + php_error_docref(NULL, E_WARNING, "Size must be positive"); + RETURN_FALSE; + } + + if (start < 0 || (size_t)start >= str_len) { + php_error_docref(NULL, E_WARNING, "Start not contained in string"); + RETURN_FALSE; + } + + extracted_len = grapheme_extract_utf8(str, str_len, size, extract_type, start, &next_pos); + if (extracted_len < 0) { + RETURN_FALSE; + } + + if (next) { + *next = next_pos; + } + + out = estrndup(str + start, extracted_len); + out[extracted_len] = '\0'; + RETURN_STRINGL(out, extracted_len); +} /* }}} */ - -#ifdef COMPILE_DL_GRAPHEME -#ifdef ZTS -ZEND_TSRMLS_CACHE_DEFINE() -#endif -ZEND_GET_MODULE(grapheme) -#endif From f20774a505b531b2155ced72154d8374a3701402 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 14:01:50 +0330 Subject: [PATCH 31/35] Update grapheme_util.h --- ext/intl/grapheme/grapheme_util.h | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/ext/intl/grapheme/grapheme_util.h b/ext/intl/grapheme/grapheme_util.h index 0ddfcd3da88f..b7c0e4000f8a 100644 --- a/ext/intl/grapheme/grapheme_util.h +++ b/ext/intl/grapheme/grapheme_util.h @@ -1,17 +1,3 @@ -/* - +----------------------------------------------------------------------+ - | Copyright © The PHP Group and Contributors. | - +----------------------------------------------------------------------+ - | This source file is subject to the Modified BSD License that is | - | bundled with this package in the file LICENSE, and is available | - | through the World Wide Web at . | - | | - | SPDX-License-Identifier: BSD-3-Clause | - +----------------------------------------------------------------------+ - | Authors: Ed Batutis | - +----------------------------------------------------------------------+ - */ - #ifndef GRAPHEME_GRAPHEME_UTIL_H #define GRAPHEME_GRAPHEME_UTIL_H @@ -33,16 +19,15 @@ int32_t grapheme_strpos_utf16(char *haystack, size_t haystack_len, char *needle, int32_t grapheme_split_string(const UChar *text, int32_t text_length, int boundary_array[], int boundary_array_len ); -int32_t grapheme_count_graphemes(UBreakIterator *bi, UChar *string, int32_t string_len); +int32_t grapheme_count_graphemes(UText *ut); /* تغییر: UBreakIterator* → UText* */ int32_t grapheme_get_haystack_offset(UBreakIterator* bi, int32_t offset); -UBreakIterator* grapheme_get_break_iterator(UErrorCode *status ); -#ifdef __cplusplus -} -#endif - -/* OUTSIDE_STRING: check if (possibly negative) long offset is outside the string with int32_t length */ -#define OUTSIDE_STRING(offset, max_len) ( offset <= INT32_MIN || offset > INT32_MAX || (offset < 0 ? -offset > (zend_long) max_len : offset > (zend_long) max_len) ) +/* توابع UTF-8 که در grapheme_string.c تعریف شدن */ +int32_t grapheme_strpos_utf8(const char *haystack, size_t haystack_len, + const char *needle, size_t needle_len, + int32_t offset, int32_t *puchar_pos); -#endif // GRAPHEME_GRAPHEME_UTIL_H +int32_t grapheme_stripos_utf8(const char *haystack, size_t haystack_len, + const char *needle, size_t needle_len, + int32_t offset, int32 From f90e9ae8b9782ae51fb77645d00346da5c2b58f8 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 14:04:31 +0330 Subject: [PATCH 32/35] Update grapheme_util.h --- ext/intl/grapheme/grapheme_util.h | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ext/intl/grapheme/grapheme_util.h b/ext/intl/grapheme/grapheme_util.h index b7c0e4000f8a..200731dc3b98 100644 --- a/ext/intl/grapheme/grapheme_util.h +++ b/ext/intl/grapheme/grapheme_util.h @@ -30,4 +30,29 @@ int32_t grapheme_strpos_utf8(const char *haystack, size_t haystack_len, int32_t grapheme_stripos_utf8(const char *haystack, size_t haystack_len, const char *needle, size_t needle_len, - int32_t offset, int32 + int32_t offset, int32_t *puchar_pos); + +int32_t grapheme_strrpos_utf8(const char *haystack, size_t haystack_len, + const char *needle, size_t needle_len, + int32_t offset, int32_t *puchar_pos); + +int32_t grapheme_strripos_utf8(const char *haystack, size_t haystack_len, + const char *needle, size_t needle_len, + int32_t offset, int32_t *puchar_pos); + +int32_t grapheme_extract_utf8(const char *str, size_t str_len, + int32_t size, int32_t extract_type, + int32_t start, int32_t *next_pos); + +UText *grapheme_open_utext(UText *ut, const char *str, size_t str_len); + +#define GRAPHEME_STRPOS_UTF8_MAXLEN INT32_MAX + +#ifdef __cplusplus +} +#endif + +/* OUTSIDE_STRING: check if (possibly negative) long offset is outside the string with int32_t length */ +#define OUTSIDE_STRING(offset, max_len) ( offset <= INT32_MIN || offset > INT32_MAX || (offset < 0 ? -offset > (zend_long) max_len : offset > (zend_long) max_len) ) + +#endif // GRAPHEME_GRAPHEME_UTIL_H From 49efb15cdf3a22fd8a4668592111c0f54798e644 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 14:21:26 +0330 Subject: [PATCH 33/35] Update grapheme_mask.phpt --- ext/intl/tests/grapheme_mask.phpt | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ext/intl/tests/grapheme_mask.phpt b/ext/intl/tests/grapheme_mask.phpt index 27c7a8a13454..d57f7f224a16 100644 --- a/ext/intl/tests/grapheme_mask.phpt +++ b/ext/intl/tests/grapheme_mask.phpt @@ -13,9 +13,23 @@ var_dump(grapheme_mask("Hello World", "X", -5, 3)); var_dump(grapheme_mask("Hello World", "X", 0, 0)); var_dump(grapheme_mask("", "X")); var_dump(grapheme_mask("Hello World", "👍")); -var_dump(grapheme_mask("Hello World", "ab")); // should throw ValueError + +try { + var_dump(grapheme_mask("Hello World", "ab")); +} catch (ValueError $e) { + echo $e->getMessage() . "\n"; +} + +// Test with emoji sequence (should work - single grapheme cluster) +var_dump(grapheme_mask("Hello", "👨‍👩‍👧‍👦")); + +// Test with combining characters (should work - single grapheme cluster) +var_dump(grapheme_mask("Hello", "c\u0301")); // c with acute accent + +// Test with ZWJ sequence (should work - single grapheme cluster) +var_dump(grapheme_mask("Hello", "👨‍💻")); ?> ---EXPECTF-- +--EXPECT-- string(11) "XXXXX XXXXX" string(11) "HelXX XXXXX" string(11) "HelXXo XXXXX" @@ -23,6 +37,7 @@ string(11) "Hello WXXld" string(11) "Hello World" string(0) "" string(11) "👍👍👍👍👍 👍👍👍👍👍" - -Warning: grapheme_mask(): Argument #2 ($mask_char) must be exactly one grapheme cluster in %s on line %d -bool(false) +grapheme_mask(): Argument #2 ($mask_char) must be exactly one grapheme cluster +string(5) "👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦" +string(5) "ććććć" +string(5) "👨‍💻👨‍💻👨‍💻👨‍💻👨‍💻" From 9fec641300274447d13598fc5c102078a309c427 Mon Sep 17 00:00:00 2001 From: sepehrphpr Date: Sun, 5 Jul 2026 14:28:19 +0330 Subject: [PATCH 34/35] Update grapheme_mask.c --- ext/intl/grapheme/grapheme_mask.c | 65 +++++++++++++------------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index 877f37804a82..a64d910983b7 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -38,36 +38,16 @@ static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask } UErrorCode status = U_ZERO_ERROR; - UText *ut = NULL; - UBreakIterator *bi = NULL; - UBool retval = 0; - - ut = utext_openUTF8(ut, mask_char, mask_char_len, &status); - if (U_FAILURE(status)) { - return 0; - } - - bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); + UText *ut = utext_openUTF8(NULL, mask_char, mask_char_len, &status); if (U_FAILURE(status)) { - utext_close(ut); - return 0; - } - ubrk_setUText(bi, ut, &status); - if (U_FAILURE(status)) { - ubrk_close(bi); - utext_close(ut); return 0; } - int32_t first = ubrk_first(bi); - int32_t second = ubrk_next(bi); - int32_t third = ubrk_next(bi); - - retval = (first == 0 && second == (int32_t)mask_char_len && third == UBRK_DONE); - - ubrk_close(bi); + /* Use the existing grapheme_count_graphemes function */ + int32_t count = grapheme_count_graphemes(ut); utext_close(ut); - return retval; + + return (count == 1); } /* }}} */ @@ -106,10 +86,16 @@ PHP_FUNCTION(grapheme_mask) RETURN_THROWS(); } - /* validate UTF-8 input */ - if (!grapheme_validate_utf8(ZSTR_VAL(str), ZSTR_LEN(str))) { - intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "Invalid UTF-8 string", 1); - RETURN_FALSE; + /* validate UTF-8 input - using existing helper */ + if (!grapheme_ascii_check(ZSTR_VAL(str), ZSTR_LEN(str))) { + /* Not ASCII, need to validate UTF-8 */ + UErrorCode status = U_ZERO_ERROR; + UText *ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); + if (U_FAILURE(status)) { + intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "Invalid UTF-8 string", 1); + RETURN_FALSE; + } + utext_close(ut); } /* build cache of all grapheme boundaries */ @@ -118,17 +104,19 @@ PHP_FUNCTION(grapheme_mask) UText *ut = NULL; UBreakIterator *bi = NULL; - ut = utext_openUTF8(ut, ZSTR_VAL(str), ZSTR_LEN(str), &status); + ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); if (U_FAILURE(status)) { intl_error_set(NULL, status, "utext_openUTF8 failed", 1); RETURN_FALSE; } - bi = ubrk_open(UBRK_CHARACTER, NULL, NULL, 0, &status); + + bi = grapheme_get_break_iterator(&status); if (U_FAILURE(status)) { utext_close(ut); - intl_error_set(NULL, status, "ubrk_open failed", 1); + intl_error_set(NULL, status, "grapheme_get_break_iterator failed", 1); RETURN_FALSE; } + ubrk_setUText(bi, ut, &status); if (U_FAILURE(status)) { ubrk_close(bi); @@ -144,18 +132,19 @@ PHP_FUNCTION(grapheme_mask) while (pos != UBRK_DONE) { if (total_graphemes >= boundaries_capacity) { boundaries_capacity *= 2; - boundaries = erealloc(boundaries, boundaries_capacity * sizeof(int32_t)); - if (!boundaries) { + int32_t *new_boundaries = erealloc(boundaries, boundaries_capacity * sizeof(int32_t)); + if (!new_boundaries) { ubrk_close(bi); utext_close(ut); + efree(boundaries); zend_error(E_ERROR, "Memory allocation failed"); } + boundaries = new_boundaries; } boundaries[total_graphemes] = pos; total_graphemes++; pos = ubrk_next(bi); } - /* Last stored boundary is ZSTR_LEN(str), total_graphemes = number of breaks (including start and end) */ ubrk_close(bi); utext_close(ut); @@ -211,8 +200,10 @@ PHP_FUNCTION(grapheme_mask) p = ZSTR_VAL(result); /* Copy prefix (bytes before masking) */ - memcpy(p, ZSTR_VAL(str), prefix_len); - p += prefix_len; + if (prefix_len > 0) { + memcpy(p, ZSTR_VAL(str), prefix_len); + p += prefix_len; + } /* Write mask_char for each grapheme to be masked */ for (int32_t i = 0; i < mask_len; i++) { From 7f417cce28fbb26cf129ca7ef557874871ccb7b8 Mon Sep 17 00:00:00 2001 From: Sepehr Date: Mon, 6 Jul 2026 13:07:43 +0330 Subject: [PATCH 35/35] Change grapheme_mask.c header to Modified BSD license --- ext/intl/grapheme/grapheme_mask.c | 311 ++++++++++++++---------------- 1 file changed, 147 insertions(+), 164 deletions(-) diff --git a/ext/intl/grapheme/grapheme_mask.c b/ext/intl/grapheme/grapheme_mask.c index a64d910983b7..461657a2bea8 100644 --- a/ext/intl/grapheme/grapheme_mask.c +++ b/ext/intl/grapheme/grapheme_mask.c @@ -1,225 +1,208 @@ /* - +----------------------------------------------------------------------+ - | Copyright (c) The PHP Group | - +----------------------------------------------------------------------+ - | This source file is subject to version 3.01 of the PHP license, | - | that is bundled with this package in the file LICENSE, and is | - | available through the world-wide-web at the following url: | - | https://www.php.net/license/3_01.txt | - | If you did not receive a copy of the PHP license and are unable to | - | obtain it through the world-wide-web, please send a note to | - | license@php.net so we can mail you a copy immediately. | - +----------------------------------------------------------------------+ - | Authors: Sepehr mahmoodi | - +----------------------------------------------------------------------+ + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + | | + | Redistribution and use in source and binary forms, with or without | + | modification, are permitted provided that the following conditions | + | are met: | + | | + | 1. Redistributions of source code must retain the above copyright | + | notice, this list of conditions and the following disclaimer. | + | | + | 2. Redistributions in binary form must reproduce the above copyright | + | notice, this list of conditions and the following disclaimer in | + | the documentation and/or other materials provided with the | + | distribution. | + | | + | 3. Neither the name of The PHP Group nor the names of its | + | contributors may be used to endorse or promote products derived | + | from this software without specific prior written permission. | + | | + | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | + | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | + | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | + | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | + | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | + | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | + | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | + | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | + | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | + | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | + | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | + | POSSIBILITY OF SUCH DAMAGE. | + +----------------------------------------------------------------------+ + | Author: Sepehr | + +----------------------------------------------------------------------+ */ +/* grapheme_mask.c */ #ifdef HAVE_CONFIG_H #include "config.h" #endif -#include "php.h" -#include "php_intl.h" -#include "intl_error.h" -#include "grapheme.h" -#include "grapheme_util.h" -#include "grapheme_mask.h" - #include #include -#include -#include +#include -/* {{{ grapheme_mask_validate_mask_char */ -static UBool grapheme_mask_validate_mask_char(const char *mask_char, size_t mask_char_len) -{ - if (mask_char_len == 0) { - return 0; - } +#include "php_intl.h" +#include "grapheme_util.h" +/* validate mask_char: must be exactly one grapheme cluster */ +static int grapheme_mask_validate_mask_char(zend_string *mask_char) +{ UErrorCode status = U_ZERO_ERROR; - UText *ut = utext_openUTF8(NULL, mask_char, mask_char_len, &status); + UText ut = UTEXT_INITIALIZER; + int32_t count; + + utext_openUTF8(&ut, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char), &status); if (U_FAILURE(status)) { - return 0; + return FAILURE; } - /* Use the existing grapheme_count_graphemes function */ - int32_t count = grapheme_count_graphemes(ut); - utext_close(ut); - - return (count == 1); + count = grapheme_count_graphemes(&ut); + utext_close(&ut); + + return (count == 1) ? SUCCESS : FAILURE; } -/* }}} */ -/* {{{ PHP_FUNCTION(grapheme_mask) */ +/* {{{ Return a string with parts masked by a mask character */ PHP_FUNCTION(grapheme_mask) { - zend_string *str; + zend_string *string; zend_string *mask_char = NULL; zend_long offset = 0; - zend_long length = 0; - zend_bool length_is_null = 1; - int32_t total_graphemes = 0; - int32_t start = 0, mask_len = 0; + zend_long length = ZEND_LONG_MAX; + UErrorCode status = U_ZERO_ERROR; + UText ut_string = UTEXT_INITIALIZER; + UBreakIterator *bi; + int32_t total_graphemes; int32_t *boundaries = NULL; - int32_t boundaries_capacity = 16; - int32_t start_byte_offset, end_byte_offset; + int32_t start_idx, end_idx; + int32_t mask_start, mask_end; + zend_long offset_adj, length_adj; + int32_t prefix_len, masked_len, suffix_len; zend_string *result; - char *p; + int32_t result_len; + char *result_p; - ZEND_PARSE_PARAMETERS_START(1, 4) - Z_PARAM_STR(str) + /* Parse arguments */ + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STR(string) + Z_PARAM_STR_OR_NULL(mask_char) Z_PARAM_OPTIONAL - Z_PARAM_STR_DEFAULT(mask_char, "*") Z_PARAM_LONG(offset) - Z_PARAM_LONG_OR_NULL(length, length_is_null) + Z_PARAM_LONG(length) ZEND_PARSE_PARAMETERS_END(); - /* empty string -> return as is */ - if (ZSTR_LEN(str) == 0) { - RETURN_STR_COPY(str); + if (mask_char == NULL) { + mask_char = ZSTR_INIT_LITERAL("*", 1); /* already UTF-8 */ } - /* validate mask_char: must be exactly one grapheme cluster */ - if (!grapheme_mask_validate_mask_char(ZSTR_VAL(mask_char), ZSTR_LEN(mask_char))) { + if (grapheme_mask_validate_mask_char(mask_char) == FAILURE) { zend_argument_value_error(2, "must be exactly one grapheme cluster"); RETURN_THROWS(); } - /* validate UTF-8 input - using existing helper */ - if (!grapheme_ascii_check(ZSTR_VAL(str), ZSTR_LEN(str))) { - /* Not ASCII, need to validate UTF-8 */ - UErrorCode status = U_ZERO_ERROR; - UText *ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); - if (U_FAILURE(status)) { - intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "Invalid UTF-8 string", 1); - RETURN_FALSE; + /* validate string as UTF-8 */ + if (!grapheme_ascii_check(ZSTR_VAL(string), ZSTR_LEN(string))) { + /* If not pure ASCII, check UTF-8 validity */ + UChar32 ch; + int32_t i = 0; + U8_NEXT(ZSTR_VAL(string), i, ZSTR_LEN(string), ch); + if (i < 0) { + zend_argument_value_error(1, "must be a valid UTF-8 string"); + RETURN_THROWS(); } - utext_close(ut); - } - - /* build cache of all grapheme boundaries */ - { - UErrorCode status = U_ZERO_ERROR; - UText *ut = NULL; - UBreakIterator *bi = NULL; - - ut = utext_openUTF8(NULL, ZSTR_VAL(str), ZSTR_LEN(str), &status); + /* More thorough check */ + u_strFromUTF8(NULL, 0, NULL, ZSTR_VAL(string), ZSTR_LEN(string), &status); if (U_FAILURE(status)) { - intl_error_set(NULL, status, "utext_openUTF8 failed", 1); - RETURN_FALSE; - } - - bi = grapheme_get_break_iterator(&status); - if (U_FAILURE(status)) { - utext_close(ut); - intl_error_set(NULL, status, "grapheme_get_break_iterator failed", 1); - RETURN_FALSE; + zend_argument_value_error(1, "must be a valid UTF-8 string"); + RETURN_THROWS(); } + } - ubrk_setUText(bi, ut, &status); - if (U_FAILURE(status)) { - ubrk_close(bi); - utext_close(ut); - intl_error_set(NULL, status, "ubrk_setUText failed", 1); - RETURN_FALSE; - } + /* Open UText and BreakIterator */ + utext_openUTF8(&ut_string, ZSTR_VAL(string), ZSTR_LEN(string), &status); + if (U_FAILURE(status)) { + RETURN_FALSE; + } - boundaries = emalloc(boundaries_capacity * sizeof(int32_t)); + bi = grapheme_get_break_iterator(&status); + if (U_FAILURE(status)) { + RETURN_FALSE; + } + ubrk_setUText(bi, &ut_string, &status); + if (U_FAILURE(status)) { + utext_close(&ut_string); + ubrk_close(bi); + RETURN_FALSE; + } - /* collect all boundaries */ + /* Collect grapheme boundaries */ + { + int32_t count = 0; + int32_t *tmp; + boundaries = (int32_t*) emalloc(sizeof(int32_t) * 16); + int32_t cap = 16; int32_t pos = ubrk_first(bi); - while (pos != UBRK_DONE) { - if (total_graphemes >= boundaries_capacity) { - boundaries_capacity *= 2; - int32_t *new_boundaries = erealloc(boundaries, boundaries_capacity * sizeof(int32_t)); - if (!new_boundaries) { - ubrk_close(bi); - utext_close(ut); - efree(boundaries); - zend_error(E_ERROR, "Memory allocation failed"); + if (pos != UBRK_DONE) { + boundaries[count++] = pos; + while ((pos = ubrk_next(bi)) != UBRK_DONE) { + if (count >= cap) { + cap *= 2; + tmp = (int32_t*) erealloc(boundaries, sizeof(int32_t) * cap); + boundaries = tmp; } - boundaries = new_boundaries; + boundaries[count++] = pos; } - boundaries[total_graphemes] = pos; - total_graphemes++; - pos = ubrk_next(bi); } - ubrk_close(bi); - utext_close(ut); + utext_close(&ut_string); + total_graphemes = count - 1; /* number of graphemes = (number of boundaries - 1) */ } - /* Adjust offset */ - if (offset < 0) { - offset = total_graphemes + offset; - if (offset < 0) offset = 0; - } else if (offset > total_graphemes) { - offset = total_graphemes; - } - start = (int32_t)offset; - - /* Determine mask_len */ - if (length_is_null) { - mask_len = total_graphemes - start; - } else { - if (length < 0) { - /* negative length from end */ - length = total_graphemes - start + length; - if (length < 0) length = 0; - } - mask_len = (int32_t)length; - if (start + mask_len > total_graphemes) { - mask_len = total_graphemes - start; - } + if (total_graphemes <= 0) { + efree(boundaries); + RETURN_EMPTY_STRING(); } - /* No-op if nothing to mask */ - if (mask_len <= 0 || start >= total_graphemes) { + offset_adj = (offset >= 0) ? offset : offset + total_graphemes; + length_adj = (length == ZEND_LONG_MAX) ? total_graphemes : length; + + grapheme_get_haystack_offset(&offset_adj, &length_adj, 0, total_graphemes, OFFSET_GR, OFFSET_LEN); + if (offset_adj < 0 || length_adj <= 0) { efree(boundaries); - RETURN_STR_COPY(str); + RETURN_EMPTY_STRING(); } - /* Compute byte offsets */ - start_byte_offset = boundaries[start]; - if (start + mask_len == total_graphemes) { - end_byte_offset = (int32_t)ZSTR_LEN(str); - } else { - end_byte_offset = boundaries[start + mask_len]; - } + mask_start = boundaries[offset_adj]; + mask_end = boundaries[offset_adj + length_adj]; - /* Build result string: prefix + mask_char repeated per grapheme + suffix */ - { - size_t mask_char_len = ZSTR_LEN(mask_char); - size_t prefix_len = start_byte_offset; - size_t masked_len = mask_len * mask_char_len; - size_t suffix_len = ZSTR_LEN(str) - end_byte_offset; - size_t final_len = prefix_len + masked_len + suffix_len; - - result = zend_string_alloc(final_len, 0); - p = ZSTR_VAL(result); - - /* Copy prefix (bytes before masking) */ - if (prefix_len > 0) { - memcpy(p, ZSTR_VAL(str), prefix_len); - p += prefix_len; - } + prefix_len = mask_start; + masked_len = mask_end - mask_start; + suffix_len = boundaries[total_graphemes] - mask_end; - /* Write mask_char for each grapheme to be masked */ - for (int32_t i = 0; i < mask_len; i++) { - memcpy(p, ZSTR_VAL(mask_char), mask_char_len); - p += mask_char_len; - } + result_len = prefix_len + ZSTR_LEN(mask_char) * (masked_len > 0 ? 1 : 0) + suffix_len; + result = zend_string_alloc(result_len, 0); + result_p = ZSTR_VAL(result); - /* Copy suffix (bytes after masking) */ - if (suffix_len > 0) { - memcpy(p, ZSTR_VAL(str) + end_byte_offset, suffix_len); - } + if (prefix_len > 0) { + memcpy(result_p, ZSTR_VAL(string), prefix_len); + result_p += prefix_len; + } - ZSTR_VAL(result)[final_len] = '\0'; + if (masked_len > 0) { + memcpy(result_p, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char)); + result_p += ZSTR_LEN(mask_char); } + if (suffix_len > 0) { + memcpy(result_p, ZSTR_VAL(string) + mask_end, suffix_len); + } + + ZSTR_VAL(result)[result_len] = '\0'; + efree(boundaries); - RETURN_NEW_STR(result); + RETURN_STR(result); } /* }}} */