From 80cfb932aa7c7b24c61da395c396d45c228cb803 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 4 Aug 2026 17:37:47 +0800 Subject: [PATCH 1/4] ext/intl: Add SpoofChecker::getBidiSkeleton() --- NEWS | 1 + UPGRADING | 3 ++ ext/intl/spoofchecker/spoofchecker.stub.php | 2 + ext/intl/spoofchecker/spoofchecker_arginfo.h | 9 +++- ext/intl/spoofchecker/spoofchecker_main.cpp | 53 +++++++++++++++++++ .../tests/spoofchecker_bidi_skeleton.phpt | 38 +++++++++++++ 6 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 ext/intl/tests/spoofchecker_bidi_skeleton.phpt diff --git a/NEWS b/NEWS index 460051eed719..b8bd4280baaf 100644 --- a/NEWS +++ b/NEWS @@ -21,6 +21,7 @@ PHP NEWS . Implemented GH-20255 (Add a predefined calendar constant in IntlDateFormatter for the proleptic gregorian calendar). (David Carlier) . Added SpoofChecker::areBidiConfusable(). (David Carlier) + . Added SpoofChecker::getBidiSkeleton(). (Weilin Du) - Reflection: . Added ReflectionAttribute::inNamespace(), diff --git a/UPGRADING b/UPGRADING index b988847c8f53..9bbe6d42c804 100644 --- a/UPGRADING +++ b/UPGRADING @@ -332,6 +332,8 @@ PHP 8.6 UPGRADE NOTES confusable for a given text direction, along with the SpoofChecker::LTR and SpoofChecker::RTL direction constants. It is supported from icu 74. + . Added SpoofChecker::getBidiSkeleton() to generate a confusable skeleton for + a given text direction. It is supported from icu 74. - IO: . Added new polling API. @@ -488,6 +490,7 @@ PHP 8.6 UPGRADE NOTES . Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue() RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue . SpoofChecker::areBidiConfusable() + . SpoofChecker::getBidiSkeleton() - mysqli: . Added mysqli::quote_string() and mysqli_quote_string(). diff --git a/ext/intl/spoofchecker/spoofchecker.stub.php b/ext/intl/spoofchecker/spoofchecker.stub.php index f3de2fcfcf10..1ffc61faaaf5 100644 --- a/ext/intl/spoofchecker/spoofchecker.stub.php +++ b/ext/intl/spoofchecker/spoofchecker.stub.php @@ -81,6 +81,8 @@ public function setRestrictionLevel(int $level): void {} public function setAllowedChars(string $pattern, int $patternOptions = 0): void {} #if U_ICU_VERSION_MAJOR_NUM >= 74 + public function getBidiSkeleton(int $direction, string $string): string|false {} + /** * @param int $errorCode */ diff --git a/ext/intl/spoofchecker/spoofchecker_arginfo.h b/ext/intl/spoofchecker/spoofchecker_arginfo.h index cc4274d8bfe6..380f27213558 100644 --- a/ext/intl/spoofchecker/spoofchecker_arginfo.h +++ b/ext/intl/spoofchecker/spoofchecker_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit spoofchecker.stub.php instead. - * Stub hash: 528a85d28312688d9ba2c78ede2f07356bbff0f6 */ + * Stub hash: cb99c1be997a0b11e23b6ae61c27fa2ecd6e403f */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Spoofchecker___construct, 0, 0, 0) ZEND_END_ARG_INFO() @@ -33,6 +33,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Spoofchecker_setAllowedCha ZEND_END_ARG_INFO() #if U_ICU_VERSION_MAJOR_NUM >= 74 +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_Spoofchecker_getBidiSkeleton, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, direction, 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_class_Spoofchecker_areBidiConfusable, 0, 3, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, direction, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, string1, IS_STRING, 0) @@ -49,6 +54,7 @@ ZEND_METHOD(Spoofchecker, setChecks); ZEND_METHOD(Spoofchecker, setRestrictionLevel); ZEND_METHOD(Spoofchecker, setAllowedChars); #if U_ICU_VERSION_MAJOR_NUM >= 74 +ZEND_METHOD(Spoofchecker, getBidiSkeleton); ZEND_METHOD(Spoofchecker, areBidiConfusable); #endif @@ -61,6 +67,7 @@ static const zend_function_entry class_Spoofchecker_methods[] = { ZEND_ME(Spoofchecker, setRestrictionLevel, arginfo_class_Spoofchecker_setRestrictionLevel, ZEND_ACC_PUBLIC) ZEND_ME(Spoofchecker, setAllowedChars, arginfo_class_Spoofchecker_setAllowedChars, ZEND_ACC_PUBLIC) #if U_ICU_VERSION_MAJOR_NUM >= 74 + ZEND_ME(Spoofchecker, getBidiSkeleton, arginfo_class_Spoofchecker_getBidiSkeleton, ZEND_ACC_PUBLIC) ZEND_ME(Spoofchecker, areBidiConfusable, arginfo_class_Spoofchecker_areBidiConfusable, ZEND_ACC_PUBLIC) #endif ZEND_FE_END diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp index d80ada93660f..df58488bfc65 100644 --- a/ext/intl/spoofchecker/spoofchecker_main.cpp +++ b/ext/intl/spoofchecker/spoofchecker_main.cpp @@ -225,6 +225,59 @@ U_CFUNC PHP_METHOD(Spoofchecker, setAllowedChars) } #if U_ICU_VERSION_MAJOR_NUM >= 74 +/* {{{ Get the confusable skeleton for an identifier in a given text direction */ +U_CFUNC PHP_METHOD(Spoofchecker, getBidiSkeleton) +{ + zend_long direction; + zend_string *string; + SPOOFCHECKER_METHOD_INIT_VARS; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_LONG(direction) + Z_PARAM_STR(string) + ZEND_PARSE_PARAMETERS_END(); + + SPOOFCHECKER_METHOD_FETCH_OBJECT; + + if (direction != UBIDI_LTR && direction != UBIDI_RTL) { + zend_argument_value_error(1, "must be either Spoofchecker::LTR or Spoofchecker::RTL"); + RETURN_THROWS(); + } + + if (UNEXPECTED(ZSTR_LEN(string) > INT32_MAX)) { + SPOOFCHECKER_ERROR_CODE(co) = U_BUFFER_OVERFLOW_ERROR; + intl_errors_set(SPOOFCHECKER_ERROR_P(co), SPOOFCHECKER_ERROR_CODE(co), + "Failed to generate skeleton"); + RETURN_FALSE; + } + + int32_t result_len = uspoof_getBidiSkeletonUTF8( + co->uspoof, (UBiDiDirection) direction, ZSTR_VAL(string), (int32_t) ZSTR_LEN(string), + nullptr, 0, SPOOFCHECKER_ERROR_CODE_P(co)); + if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co)) && SPOOFCHECKER_ERROR_CODE(co) != U_BUFFER_OVERFLOW_ERROR) { + intl_errors_set(SPOOFCHECKER_ERROR_P(co), SPOOFCHECKER_ERROR_CODE(co), + "Failed to generate skeleton"); + RETURN_FALSE; + } + + zend_string *result = zend_string_alloc(result_len, false); + SPOOFCHECKER_ERROR_CODE(co) = U_ZERO_ERROR; + result_len = uspoof_getBidiSkeletonUTF8( + co->uspoof, (UBiDiDirection) direction, ZSTR_VAL(string), (int32_t) ZSTR_LEN(string), + ZSTR_VAL(result), result_len, SPOOFCHECKER_ERROR_CODE_P(co)); + if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + zend_string_release(result); + intl_errors_set(SPOOFCHECKER_ERROR_P(co), SPOOFCHECKER_ERROR_CODE(co), + "Failed to generate skeleton"); + RETURN_FALSE; + } + SPOOFCHECKER_ERROR_CODE(co) = U_ZERO_ERROR; + ZSTR_LEN(result) = result_len; + ZSTR_VAL(result)[result_len] = '\0'; + RETURN_STR(result); +} +/* }}} */ + /* {{{ Checks if a given text contains any confusable characters, for a given text direction */ U_CFUNC PHP_METHOD(Spoofchecker, areBidiConfusable) { diff --git a/ext/intl/tests/spoofchecker_bidi_skeleton.phpt b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt new file mode 100644 index 000000000000..1aa541566305 --- /dev/null +++ b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt @@ -0,0 +1,38 @@ +--TEST-- +Spoofchecker::getBidiSkeleton() +--EXTENSIONS-- +intl +--SKIPIF-- += 74.0'); ?> +--FILE-- +getBidiSkeleton(Spoofchecker::LTR, "")); + +try { + $checker->getBidiSkeleton(Spoofchecker::RTL + 1, "a"); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +/* These identifiers are confusable in a left-to-right context only. */ +var_dump( + $checker->getBidiSkeleton(Spoofchecker::LTR, "A1\u{05D0}") === + $checker->getBidiSkeleton(Spoofchecker::LTR, "A\u{05D0}1") +); +var_dump( + $checker->getBidiSkeleton(Spoofchecker::RTL, "A1\u{05D0}") === + $checker->getBidiSkeleton(Spoofchecker::RTL, "A\u{05D0}1") +); + +var_dump($checker->getBidiSkeleton(Spoofchecker::LTR, "\x80")); +var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND); +?> +--EXPECT-- +string(0) "" +Spoofchecker::getBidiSkeleton(): Argument #1 ($direction) must be either Spoofchecker::LTR or Spoofchecker::RTL +bool(true) +bool(false) +bool(false) +bool(true) From 3d4b9538c9015d09046a1738e9642aecd770acc8 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 4 Aug 2026 20:25:28 +0800 Subject: [PATCH 2/4] better error logics --- ext/intl/spoofchecker/spoofchecker_main.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp index df58488bfc65..bb1dfe6bfc76 100644 --- a/ext/intl/spoofchecker/spoofchecker_main.cpp +++ b/ext/intl/spoofchecker/spoofchecker_main.cpp @@ -247,10 +247,20 @@ U_CFUNC PHP_METHOD(Spoofchecker, getBidiSkeleton) if (UNEXPECTED(ZSTR_LEN(string) > INT32_MAX)) { SPOOFCHECKER_ERROR_CODE(co) = U_BUFFER_OVERFLOW_ERROR; intl_errors_set(SPOOFCHECKER_ERROR_P(co), SPOOFCHECKER_ERROR_CODE(co), - "Failed to generate skeleton"); + "Failed to convert input string to UTF-16"); RETURN_FALSE; } + int32_t utf16_len; + u_strFromUTF8(nullptr, 0, &utf16_len, ZSTR_VAL(string), (int32_t) ZSTR_LEN(string), + SPOOFCHECKER_ERROR_CODE_P(co)); + if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co)) && SPOOFCHECKER_ERROR_CODE(co) != U_BUFFER_OVERFLOW_ERROR) { + intl_errors_set(SPOOFCHECKER_ERROR_P(co), SPOOFCHECKER_ERROR_CODE(co), + "Failed to convert input string to UTF-16"); + RETURN_FALSE; + } + SPOOFCHECKER_ERROR_CODE(co) = U_ZERO_ERROR; + int32_t result_len = uspoof_getBidiSkeletonUTF8( co->uspoof, (UBiDiDirection) direction, ZSTR_VAL(string), (int32_t) ZSTR_LEN(string), nullptr, 0, SPOOFCHECKER_ERROR_CODE_P(co)); @@ -261,10 +271,11 @@ U_CFUNC PHP_METHOD(Spoofchecker, getBidiSkeleton) } zend_string *result = zend_string_alloc(result_len, false); + int32_t result_capacity = result_len < INT32_MAX ? result_len + 1 : result_len; SPOOFCHECKER_ERROR_CODE(co) = U_ZERO_ERROR; result_len = uspoof_getBidiSkeletonUTF8( co->uspoof, (UBiDiDirection) direction, ZSTR_VAL(string), (int32_t) ZSTR_LEN(string), - ZSTR_VAL(result), result_len, SPOOFCHECKER_ERROR_CODE_P(co)); + ZSTR_VAL(result), result_capacity, SPOOFCHECKER_ERROR_CODE_P(co)); if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { zend_string_release(result); intl_errors_set(SPOOFCHECKER_ERROR_P(co), SPOOFCHECKER_ERROR_CODE(co), From dc38b23f0cd928dc6aac91b97d1e73abf570407e Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 4 Aug 2026 22:22:38 +0800 Subject: [PATCH 3/4] test error classes --- ext/intl/tests/spoofchecker_bidi_skeleton.phpt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/intl/tests/spoofchecker_bidi_skeleton.phpt b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt index 1aa541566305..66dce545d65a 100644 --- a/ext/intl/tests/spoofchecker_bidi_skeleton.phpt +++ b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt @@ -13,7 +13,7 @@ var_dump($checker->getBidiSkeleton(Spoofchecker::LTR, "")); try { $checker->getBidiSkeleton(Spoofchecker::RTL + 1, "a"); } catch (ValueError $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } /* These identifiers are confusable in a left-to-right context only. */ @@ -31,7 +31,7 @@ var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND); ?> --EXPECT-- string(0) "" -Spoofchecker::getBidiSkeleton(): Argument #1 ($direction) must be either Spoofchecker::LTR or Spoofchecker::RTL +ValueError: Spoofchecker::getBidiSkeleton(): Argument #1 ($direction) must be either Spoofchecker::LTR or Spoofchecker::RTL bool(true) bool(false) bool(false) From 5a790aa770b7684b6227684cd7b5428176925871 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 5 Aug 2026 01:43:07 +0800 Subject: [PATCH 4/4] Add test case in feedback Co-Authored-By: David CARLIER --- ext/intl/tests/spoofchecker_bidi_skeleton.phpt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ext/intl/tests/spoofchecker_bidi_skeleton.phpt b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt index 66dce545d65a..c9b8a1fca1ba 100644 --- a/ext/intl/tests/spoofchecker_bidi_skeleton.phpt +++ b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt @@ -26,6 +26,16 @@ var_dump( $checker->getBidiSkeleton(Spoofchecker::RTL, "A\u{05D0}1") ); +/* These identifiers are confusable in a right-to-left context only. */ +var_dump( + $checker->getBidiSkeleton(Spoofchecker::LTR, "\u{05D0}A_1") === + $checker->getBidiSkeleton(Spoofchecker::LTR, "\u{05D0}1_A") +); +var_dump( + $checker->getBidiSkeleton(Spoofchecker::RTL, "\u{05D0}A_1") === + $checker->getBidiSkeleton(Spoofchecker::RTL, "\u{05D0}1_A") +); + var_dump($checker->getBidiSkeleton(Spoofchecker::LTR, "\x80")); var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND); ?> @@ -36,3 +46,5 @@ bool(true) bool(false) bool(false) bool(true) +bool(false) +bool(true)