From 721167e0a4819a966c248c8cfc6a60ee02fe951c Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 3 Aug 2026 11:54:37 +0100 Subject: [PATCH] ext/intl: introduce SpoofChecker::areBidiConfusable. Adding a new more refined spoofchecker method in addition of the existing Spoofchecker::areConfusable which takes in account the text direction left to right and right to left, along with the Spoofchecker::LTR and Spoofchecker::RTL constants. The self and typed references tests are extended accordingly, guarded at runtime rather than through a SKIPIF, so that the older ICU releases keep their coverage. --- ext/intl/spoofchecker/spoofchecker.stub.php | 14 +++++ ext/intl/spoofchecker/spoofchecker_arginfo.h | 31 ++++++++++- ext/intl/spoofchecker/spoofchecker_main.cpp | 43 +++++++++++++++ .../tests/spoofchecker_self_references.phpt | 5 ++ .../tests/spoofchecker_typed_references.phpt | 14 +++++ ext/intl/tests/spoofchecker_ubidi.phpt | 54 +++++++++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 ext/intl/tests/spoofchecker_ubidi.phpt diff --git a/ext/intl/spoofchecker/spoofchecker.stub.php b/ext/intl/spoofchecker/spoofchecker.stub.php index 51a9c7d39074..f3de2fcfcf10 100644 --- a/ext/intl/spoofchecker/spoofchecker.stub.php +++ b/ext/intl/spoofchecker/spoofchecker.stub.php @@ -49,6 +49,13 @@ class Spoofchecker public const int SIMPLE_CASE_INSENSITIVE = UNKNOWN; #endif +#if U_ICU_VERSION_MAJOR_NUM >= 74 + /** @cvalue UBIDI_LTR */ + public const int LTR = UNKNOWN; + /** @cvalue UBIDI_RTL */ + public const int RTL = UNKNOWN; +#endif + public function __construct() {} /** @@ -72,4 +79,11 @@ public function setChecks(int $checks): void {} /** @tentative-return-type */ public function setRestrictionLevel(int $level): void {} public function setAllowedChars(string $pattern, int $patternOptions = 0): void {} + +#if U_ICU_VERSION_MAJOR_NUM >= 74 + /** + * @param int $errorCode + */ + public function areBidiConfusable(int $direction, string $string1, string $string2, &$errorCode = null): bool {} +#endif } diff --git a/ext/intl/spoofchecker/spoofchecker_arginfo.h b/ext/intl/spoofchecker/spoofchecker_arginfo.h index 8704065e479b..cc4274d8bfe6 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: 4634f8ef9157fb3670a2ddc5e3246340660fc68c */ + * Stub hash: 528a85d28312688d9ba2c78ede2f07356bbff0f6 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Spoofchecker___construct, 0, 0, 0) ZEND_END_ARG_INFO() @@ -32,6 +32,15 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Spoofchecker_setAllowedCha ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, patternOptions, IS_LONG, 0, "0") ZEND_END_ARG_INFO() +#if U_ICU_VERSION_MAJOR_NUM >= 74 +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) + ZEND_ARG_TYPE_INFO(0, string2, IS_STRING, 0) + ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, errorCode, "null") +ZEND_END_ARG_INFO() +#endif + ZEND_METHOD(Spoofchecker, __construct); ZEND_METHOD(Spoofchecker, isSuspicious); ZEND_METHOD(Spoofchecker, areConfusable); @@ -39,6 +48,9 @@ ZEND_METHOD(Spoofchecker, setAllowedLocales); ZEND_METHOD(Spoofchecker, setChecks); ZEND_METHOD(Spoofchecker, setRestrictionLevel); ZEND_METHOD(Spoofchecker, setAllowedChars); +#if U_ICU_VERSION_MAJOR_NUM >= 74 +ZEND_METHOD(Spoofchecker, areBidiConfusable); +#endif static const zend_function_entry class_Spoofchecker_methods[] = { ZEND_ME(Spoofchecker, __construct, arginfo_class_Spoofchecker___construct, ZEND_ACC_PUBLIC) @@ -48,6 +60,9 @@ static const zend_function_entry class_Spoofchecker_methods[] = { ZEND_ME(Spoofchecker, setChecks, arginfo_class_Spoofchecker_setChecks, ZEND_ACC_PUBLIC) 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, areBidiConfusable, arginfo_class_Spoofchecker_areBidiConfusable, ZEND_ACC_PUBLIC) +#endif ZEND_FE_END }; @@ -175,6 +190,20 @@ static zend_class_entry *register_class_Spoofchecker(void) zend_declare_typed_class_constant(class_entry, const_SIMPLE_CASE_INSENSITIVE_name, &const_SIMPLE_CASE_INSENSITIVE_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); zend_string_release_ex(const_SIMPLE_CASE_INSENSITIVE_name, true); #endif +#if U_ICU_VERSION_MAJOR_NUM >= 74 + + zval const_LTR_value; + ZVAL_LONG(&const_LTR_value, UBIDI_LTR); + zend_string *const_LTR_name = zend_string_init_interned("LTR", sizeof("LTR") - 1, true); + zend_declare_typed_class_constant(class_entry, const_LTR_name, &const_LTR_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(const_LTR_name, true); + + zval const_RTL_value; + ZVAL_LONG(&const_RTL_value, UBIDI_RTL); + zend_string *const_RTL_name = zend_string_init_interned("RTL", sizeof("RTL") - 1, true); + zend_declare_typed_class_constant(class_entry, const_RTL_name, &const_RTL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(const_RTL_name, true); +#endif return class_entry; } diff --git a/ext/intl/spoofchecker/spoofchecker_main.cpp b/ext/intl/spoofchecker/spoofchecker_main.cpp index 1e8e9ac2a78d..d80ada93660f 100644 --- a/ext/intl/spoofchecker/spoofchecker_main.cpp +++ b/ext/intl/spoofchecker/spoofchecker_main.cpp @@ -223,3 +223,46 @@ U_CFUNC PHP_METHOD(Spoofchecker, setAllowedChars) php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); } } + +#if U_ICU_VERSION_MAJOR_NUM >= 74 +/* {{{ Checks if a given text contains any confusable characters, for a given text direction */ +U_CFUNC PHP_METHOD(Spoofchecker, areBidiConfusable) +{ + uint32_t ret = 0; + zend_long direction; + zend_string *s1, *s2; + zval *error_code = NULL; + SPOOFCHECKER_METHOD_INIT_VARS; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_LONG(direction) + Z_PARAM_STR(s1) + Z_PARAM_STR(s2) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(error_code) + 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(s1) > INT32_MAX || ZSTR_LEN(s2) > INT32_MAX)) { + SPOOFCHECKER_ERROR_CODE(co) = U_BUFFER_OVERFLOW_ERROR; + } else { + ret = uspoof_areBidiConfusableUTF8(co->uspoof, (UBiDiDirection)direction, ZSTR_VAL(s1), (int32_t)ZSTR_LEN(s1), ZSTR_VAL(s2), (int32_t)ZSTR_LEN(s2), SPOOFCHECKER_ERROR_CODE_P(co)); + } + if (U_FAILURE(SPOOFCHECKER_ERROR_CODE(co))) { + php_error_docref(NULL, E_WARNING, "(%d) %s", SPOOFCHECKER_ERROR_CODE(co), u_errorName(SPOOFCHECKER_ERROR_CODE(co))); + RETURN_TRUE; + } + + if (error_code) { + ZEND_TRY_ASSIGN_REF_LONG(error_code, ret); + } + RETVAL_BOOL(ret != 0); +} +/* }}} */ +#endif diff --git a/ext/intl/tests/spoofchecker_self_references.phpt b/ext/intl/tests/spoofchecker_self_references.phpt index d2feaa77a32f..7b1dc3778839 100644 --- a/ext/intl/tests/spoofchecker_self_references.phpt +++ b/ext/intl/tests/spoofchecker_self_references.phpt @@ -11,6 +11,11 @@ $checker->isSuspicious("", $checker); $checker = new Spoofchecker(); $checker->areConfusable("", "", $checker); +if (version_compare(INTL_ICU_VERSION, '74.0') >= 0) { + $checker = new Spoofchecker(); + $checker->areBidiConfusable(Spoofchecker::LTR, "", "", $checker); +} + echo "Done\n"; ?> diff --git a/ext/intl/tests/spoofchecker_typed_references.phpt b/ext/intl/tests/spoofchecker_typed_references.phpt index 5508497072da..5d1fa4554ce0 100644 --- a/ext/intl/tests/spoofchecker_typed_references.phpt +++ b/ext/intl/tests/spoofchecker_typed_references.phpt @@ -23,6 +23,20 @@ $checker = new Spoofchecker(); $checker->areConfusable("", "", $test->x); var_dump($test); +if (version_compare(INTL_ICU_VERSION, '74.0') >= 0) { + $test = new Test; + $test->x = ""; + + $checker = new Spoofchecker(); + $checker->areBidiConfusable(Spoofchecker::LTR, "", "", $test->x); + /* Asserted quietly rather than dumped, so that the expected output stays + the same on ICU < 74, where the method does not exist. */ + if ($test->x !== "1") { + echo "unexpected value: "; + var_dump($test->x); + } +} + ?> --EXPECT-- object(Test)#1 (1) { diff --git a/ext/intl/tests/spoofchecker_ubidi.phpt b/ext/intl/tests/spoofchecker_ubidi.phpt new file mode 100644 index 000000000000..6e1f7b35fc64 --- /dev/null +++ b/ext/intl/tests/spoofchecker_ubidi.phpt @@ -0,0 +1,54 @@ +--TEST-- +Spoofchecker::areBidiConfusable() checks if strings are confusable in a given direction. +--EXTENSIONS-- +intl +--SKIPIF-- += 74.0'); ?> +--FILE-- +areBidiConfusable(Spoofchecker::RTL + 1, "a", "a"); +} catch (ValueError $e) { + echo $e->getMessage() . PHP_EOL; +} + +/* "A1" and "A1" both display as "A1" in a left to right + * context, but differ in a right to left one. */ +var_dump($s->areBidiConfusable(Spoofchecker::LTR, "A1\u{05D0}", "A\u{05D0}1")); +var_dump($s->areBidiConfusable(Spoofchecker::RTL, "A1\u{05D0}", "A\u{05D0}1")); + +/* Mirror case: confusable in a right to left context only. */ +var_dump($s->areBidiConfusable(Spoofchecker::LTR, "\u{05D0}A_1", "\u{05D0}1_A")); +var_dump($s->areBidiConfusable(Spoofchecker::RTL, "\u{05D0}A_1", "\u{05D0}1_A")); + +/* Neither direction reorders these into each other. */ +var_dump($s->areBidiConfusable(Spoofchecker::LTR, "Mark_", "_Mark")); +var_dump($s->areBidiConfusable(Spoofchecker::RTL, "Mark_", "_Mark")); + +/* areConfusable() ignores the text direction and misses both cases above. */ +var_dump($s->areConfusable("A1\u{05D0}", "A\u{05D0}1")); +var_dump($s->areConfusable("\u{05D0}A_1", "\u{05D0}1_A")); + +$errorCode = null; +var_dump($s->areBidiConfusable(Spoofchecker::LTR, "A1\u{05D0}", "A\u{05D0}1", $errorCode)); +var_dump($errorCode === Spoofchecker::MIXED_SCRIPT_CONFUSABLE); + +var_dump($s->areBidiConfusable(Spoofchecker::LTR, "Mark_", "_Mark", $errorCode)); +var_dump($errorCode); +?> +--EXPECT-- +Spoofchecker::areBidiConfusable(): Argument #1 ($direction) must be either Spoofchecker::LTR or Spoofchecker::RTL +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) +bool(false) +int(0)