[php-src] master: ext/intl: Add SpoofChecker::getBidiSkeleton() (#23017)

Weilin Du via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.