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