Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
37a5e5e
Create grapheme_mask.c
sepehrphpr Jul 2, 2026
1938a52
Update grapheme_mask.c
sepehrphpr Jul 2, 2026
fecd4a9
Update grapheme_mask.c
sepehrphpr Jul 2, 2026
95d549f
Create grapheme_mask.h
sepehrphpr Jul 2, 2026
36ac29e
Create grapheme_mask.stub.php
sepehrphpr Jul 2, 2026
b14420a
Update grapheme_mask.stub.php
sepehrphpr Jul 2, 2026
971ec07
Update grapheme_mask.c
sepehrphpr Jul 4, 2026
c7fa946
Update grapheme_mask.stub.php
sepehrphpr Jul 4, 2026
966dd8b
Update grapheme_mask.c
sepehrphpr Jul 4, 2026
d107f75
Create grapheme.c
sepehrphpr Jul 4, 2026
4c29b8d
Update grapheme.h
sepehrphpr Jul 4, 2026
7d0291e
Delete ext/intl/grapheme/grapheme_mask.stub.php
sepehrphpr Jul 4, 2026
54ceb1e
Update grapheme.h
sepehrphpr Jul 4, 2026
ec703c6
Update grapheme_mask.h
sepehrphpr Jul 4, 2026
d31f906
Create grapheme.stub.php
sepehrphpr Jul 4, 2026
95124a8
Create grapheme_arginfo.h
sepehrphpr Jul 4, 2026
20bbf6e
Create grapheme_mask.phpt
sepehrphpr Jul 4, 2026
706966c
Update grapheme_mask.c
sepehrphpr Jul 4, 2026
4a0f186
Update grapheme_mask.h
sepehrphpr Jul 4, 2026
e415d03
Update grapheme_arginfo.h
sepehrphpr Jul 4, 2026
93edc63
Update grapheme.stub.php
sepehrphpr Jul 4, 2026
642b3d5
Update grapheme_mask.phpt
sepehrphpr Jul 4, 2026
38e3803
Update grapheme_mask.c
sepehrphpr Jul 4, 2026
fc7a4d4
Update grapheme.c
sepehrphpr Jul 4, 2026
ae803d4
Update grapheme_mask.c
sepehrphpr Jul 4, 2026
be6e1ea
Update grapheme_mask.c
sepehrphpr Jul 4, 2026
820a791
Update grapheme.c
sepehrphpr Jul 4, 2026
489785e
Update grapheme_mask.h
sepehrphpr Jul 4, 2026
9da254f
Update grapheme.c
sepehrphpr Jul 5, 2026
c43b12f
Update grapheme.c
sepehrphpr Jul 5, 2026
f20774a
Update grapheme_util.h
sepehrphpr Jul 5, 2026
f90e9ae
Update grapheme_util.h
sepehrphpr Jul 5, 2026
49efb15
Update grapheme_mask.phpt
sepehrphpr Jul 5, 2026
9fec641
Update grapheme_mask.c
sepehrphpr Jul 5, 2026
7f417cc
Change grapheme_mask.c header to Modified BSD license
sepehrphpr Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
448 changes: 448 additions & 0 deletions ext/intl/grapheme/grapheme.c

Large diffs are not rendered by default.

1,186 changes: 1,171 additions & 15 deletions ext/intl/grapheme/grapheme.h

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions ext/intl/grapheme/grapheme.stub.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function grapheme_strlen(string $string, int $form = GRAPHEME_EXTR_COUNT): int|false {}
function grapheme_strpos(string $haystack, string $needle, int $offset = 0, int $form = GRAPHEME_EXTR_COUNT): int|false {}
function grapheme_stripos(string $haystack, string $needle, int $offset = 0, int $form = GRAPHEME_EXTR_COUNT): int|false {}
function grapheme_strrpos(string $haystack, string $needle, int $offset = 0, int $form = GRAPHEME_EXTR_COUNT): int|false {}
function grapheme_strripos(string $haystack, string $needle, int $offset = 0, int $form = GRAPHEME_EXTR_COUNT): int|false {}
function grapheme_substr(string $string, int $offset, ?int $length = null, int $form = GRAPHEME_EXTR_COUNT): string|false {}
function grapheme_strstr(string $haystack, string $needle, bool $before_needle = false, int $form = GRAPHEME_EXTR_COUNT): string|false {}
function grapheme_stristr(string $haystack, string $needle, bool $before_needle = false, int $form = GRAPHEME_EXTR_COUNT): string|false {}
function grapheme_extract(string $haystack, int $size, int $type = GRAPHEME_EXTR_COUNT, int $start = 0, int &$next = null, int $form = GRAPHEME_EXTR_COUNT): string|false {}
function grapheme_mask(string $string, string $mask_char = "*", int $offset = 0, ?int $length = null): string|false {}
6 changes: 6 additions & 0 deletions ext/intl/grapheme/grapheme_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

208 changes: 208 additions & 0 deletions ext/intl/grapheme/grapheme_mask.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
+----------------------------------------------------------------------+
| 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 <sepehrphpr@gmail.com> |
+----------------------------------------------------------------------+
*/

/* grapheme_mask.c */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <unicode/ubrk.h>
#include <unicode/utext.h>
#include <unicode/ustring.h>

#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_INITIALIZER;
int32_t count;

utext_openUTF8(&ut, ZSTR_VAL(mask_char), ZSTR_LEN(mask_char), &status);
if (U_FAILURE(status)) {
return FAILURE;
}

count = grapheme_count_graphemes(&ut);
utext_close(&ut);

return (count == 1) ? SUCCESS : FAILURE;
}

/* {{{ Return a string with parts masked by a mask character */
PHP_FUNCTION(grapheme_mask)
{
zend_string *string;
zend_string *mask_char = NULL;
zend_long offset = 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 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;
int32_t result_len;
char *result_p;

/* Parse arguments */
ZEND_PARSE_PARAMETERS_START(2, 4)
Z_PARAM_STR(string)
Z_PARAM_STR_OR_NULL(mask_char)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(offset)
Z_PARAM_LONG(length)
ZEND_PARSE_PARAMETERS_END();

if (mask_char == NULL) {
mask_char = ZSTR_INIT_LITERAL("*", 1); /* already UTF-8 */
}

if (grapheme_mask_validate_mask_char(mask_char) == FAILURE) {
zend_argument_value_error(2, "must be exactly one grapheme cluster");
RETURN_THROWS();
}

/* 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();
}
/* More thorough check */
u_strFromUTF8(NULL, 0, NULL, ZSTR_VAL(string), ZSTR_LEN(string), &status);
if (U_FAILURE(status)) {
zend_argument_value_error(1, "must be a valid UTF-8 string");
RETURN_THROWS();
}
}

/* Open UText and BreakIterator */
utext_openUTF8(&ut_string, ZSTR_VAL(string), ZSTR_LEN(string), &status);
if (U_FAILURE(status)) {
RETURN_FALSE;
}

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 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);
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[count++] = pos;
}
}
ubrk_close(bi);
utext_close(&ut_string);
total_graphemes = count - 1; /* number of graphemes = (number of boundaries - 1) */
}

if (total_graphemes <= 0) {
efree(boundaries);
RETURN_EMPTY_STRING();
}

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_EMPTY_STRING();
}

mask_start = boundaries[offset_adj];
mask_end = boundaries[offset_adj + length_adj];

prefix_len = mask_start;
masked_len = mask_end - mask_start;
suffix_len = boundaries[total_graphemes] - mask_end;

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);

if (prefix_len > 0) {
memcpy(result_p, ZSTR_VAL(string), prefix_len);
result_p += prefix_len;
}

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_STR(result);
}
/* }}} */
6 changes: 6 additions & 0 deletions ext/intl/grapheme/grapheme_mask.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef GRAPHEME_MASK_H
#define GRAPHEME_MASK_H

PHP_FUNCTION(grapheme_mask);

#endif
42 changes: 26 additions & 16 deletions ext/intl/grapheme/grapheme_util.h
Original file line number Diff line number Diff line change
@@ -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 <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Ed Batutis <ed@batutis.com> |
+----------------------------------------------------------------------+
*/

#ifndef GRAPHEME_GRAPHEME_UTIL_H
#define GRAPHEME_GRAPHEME_UTIL_H

Expand All @@ -33,11 +19,35 @@ 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 );
/* توابع 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);

int32_t grapheme_stripos_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_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
Expand Down
43 changes: 43 additions & 0 deletions ext/intl/tests/grapheme_mask.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
grapheme_mask() - Basic functionality
--SKIPIF--
<?php
if (!extension_loaded('intl')) die('skip intl extension not enabled');
?>
--FILE--
<?php
var_dump(grapheme_mask("Hello World", "X"));
var_dump(grapheme_mask("Hello World", "X", 3));
var_dump(grapheme_mask("Hello World", "X", 3, 2));
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", "👍"));

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", "👨‍💻"));
?>
--EXPECT--
string(11) "XXXXX XXXXX"
string(11) "HelXX XXXXX"
string(11) "HelXXo XXXXX"
string(11) "Hello WXXld"
string(11) "Hello World"
string(0) ""
string(11) "👍👍👍👍👍 👍👍👍👍👍"
grapheme_mask(): Argument #2 ($mask_char) must be exactly one grapheme cluster
string(5) "👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦"
string(5) "ććććć"
string(5) "👨‍💻👨‍💻👨‍💻👨‍💻👨‍💻"