[PHP-CVS] [php-src] master: ext/intl: Add SpoofChecker::getBidiSkeleton() (#23017)
[email protected] (Weilin Du via GitHub) Tue, 4 Aug 2026 19:27:06 +0000
| Newsgroups | php.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: Weilin Du (LamentXU123)
Committer: GitHub (web-flow)
Pusher: LamentXU123
Date: 2026-08-05T03:27:03+08:00
Commit: https://github.com/php/php-src/commit/9905878a4ad71b70ef467bf3b590ba77e335c549
Raw diff: https://github.com/php/php-src/commit/9905878a4ad71b70ef467bf3b590ba77e335c549.diff
ext/intl: Add SpoofChecker::getBidiSkeleton() (#23017)
This implement the SpoofChecker::getBidiSkeleton() function.
Changed paths:
A ext/intl/tests/spoofchecker_bidi_skeleton.phpt
M NEWS
M UPGRADING
M ext/intl/spoofchecker/spoofchecker.stub.php
M ext/intl/spoofchecker/spoofchecker_arginfo.h
M ext/intl/spoofchecker/spoofchecker_main.cpp
Diff:
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..bb1dfe6bfc76 100644
--- a/ext/intl/spoofchecker/spoofchecker_main.cpp
+++ b/ext/intl/spoofchecker/spoofchecker_main.cpp
@@ -225,6 +225,70 @@ 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 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));
+ 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);
+ 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_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),
+ "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..c9b8a1fca1ba
--- /dev/null
+++ b/ext/intl/tests/spoofchecker_bidi_skeleton.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Spoofchecker::getBidiSkeleton()
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (version_compare(INTL_ICU_VERSION, '74.0') < 0) die('skip for ICU >= 74.0'); ?>
+--FILE--
+<?php
+$checker = new Spoofchecker();
+
+var_dump($checker->getBidiSkeleton(Spoofchecker::LTR, ""));
+
+try {
+ $checker->getBidiSkeleton(Spoofchecker::RTL + 1, "a");
+} catch (ValueError $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+/* 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")
+);
+
+/* 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);
+?>
+--EXPECT--
+string(0) ""
+ValueError: Spoofchecker::getBidiSkeleton(): Argument #1 ($direction) must be either Spoofchecker::LTR or Spoofchecker::RTL
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+bool(false)
+bool(true)