Commit 9905878a4ad for php.net
commit 9905878a4ad71b70ef467bf3b590ba77e335c549
Author: Weilin Du <weilindu@php.net>
Date: Wed Aug 5 03:27:03 2026 +0800
ext/intl: Add SpoofChecker::getBidiSkeleton() (#23017)
This implement the SpoofChecker::getBidiSkeleton() function.
diff --git a/NEWS b/NEWS
index 460051eed71..b8bd4280baa 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 b988847c8f5..9bbe6d42c80 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 f3de2fcfcf1..1ffc61faaaf 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 cc4274d8bfe..380f2721355 100644
Binary files a/ext/intl/spoofchecker/spoofchecker_arginfo.h and b/ext/intl/spoofchecker/spoofchecker_arginfo.h differ
diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp
index d80ada93660..bb1dfe6bfc7 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 00000000000..c9b8a1fca1b
--- /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)