Skip to content

Commit 718c4fd

Browse files
ext/intl: Fix Collator sort conversion error handling (#22893)
Report UTF-8/UTF-16 conversion failures from Collator sort comparisons through the intl error mechanism. Previously, these paths emitted a warning and continued with an empty string. Keep the input array unchanged when sorting cannot complete, preserve the active collator state across nested sorts, and stop invoking further comparisons after a conversion failure. Co-authored-by: David CARLIER <devnexen@gmail.com>
1 parent 29c237b commit 718c4fd

17 files changed

Lines changed: 674 additions & 21 deletions

NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ PHP NEWS
77
(Weilin Du)
88
. Added gmp_powm_sec(). (Weilin Du)
99

10+
- Intl:
11+
. Fixed Collator::sort(), collator_sort(), Collator::asort(), and
12+
collator_asort() to report UTF-8/UTF-16 conversion errors through the intl
13+
error handler instead of emitting a warning and continuing with an empty
14+
string. (Weilin Du)
15+
1016
30 Jul 2026, PHP 8.6.0alpha3
1117

1218
- Core:

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ PHP 8.6 UPGRADE NOTES
7676
IntlDateFormatter::localtime()/datefmt_localtime() now raise a TypeError
7777
when the offset argument is not of type int instead of silently converting
7878
the value.
79+
. Collator::sort(), collator_sort(), Collator::asort(), and
80+
collator_asort() now report UTF-8/UTF-16 conversion failures during
81+
comparison through the intl error mechanism and return false. With
82+
intl.use_exceptions enabled, these failures throw IntlException. Previously,
83+
these paths emitted a warning and compared the value as an empty string.
7984

8085
- PCNTL:
8186
. pcntl_alarm() now raises a ValueError if the seconds argument is

ext/intl/collator/collator_convert.cpp

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ extern "C" {
3838
return retval; \
3939
}
4040

41+
static void collator_set_conversion_error(UErrorCode status, const char *message)
42+
{
43+
if (U_SUCCESS(status)) {
44+
status = U_MEMORY_ALLOCATION_ERROR;
45+
}
46+
47+
ZEND_ASSERT(INTL_G(current_collator_error) != nullptr);
48+
intl_error *err = INTL_G(current_collator_error);
49+
intl_error_reset(err);
50+
err->code = status;
51+
err->custom_error_message = zend_string_init(
52+
message, strlen(message), false);
53+
}
54+
4155
/* {{{ collator_convert_hash_item_from_utf8_to_utf16 */
4256
static void collator_convert_hash_item_from_utf8_to_utf16(
4357
HashTable* hash, zval *hashData, zend_string *hashKey, zend_ulong hashIndex,
@@ -166,11 +180,12 @@ U_CFUNC zval* collator_convert_zstr_utf16_to_utf8( zval* utf16_zval, zval *rv )
166180
u8str = intl_convert_utf16_to_utf8(
167181
(UChar*) Z_STRVAL_P(utf16_zval), UCHARS( Z_STRLEN_P(utf16_zval) ), &status );
168182
if( !u8str ) {
169-
php_error( E_WARNING, "Error converting utf16 to utf8 in collator_convert_zval_utf16_to_utf8()" );
170-
ZVAL_EMPTY_STRING( rv );
171-
} else {
172-
ZVAL_NEW_STR( rv, u8str );
183+
collator_set_conversion_error(status, "Error converting string from UTF-16 to UTF-8");
184+
ZVAL_NULL( rv );
185+
return nullptr;
173186
}
187+
188+
ZVAL_NEW_STR( rv, u8str );
174189
return rv;
175190
}
176191
/* }}} */
@@ -183,11 +198,9 @@ U_CFUNC zend_string *collator_convert_zstr_utf8_to_utf16(zend_string *utf8_str)
183198
zend_string *zstr = intl_convert_utf8_to_utf16_zstr(
184199
ZSTR_VAL(utf8_str), ZSTR_LEN(utf8_str),
185200
&status);
186-
// FIXME Or throw error or use intl internal error handler
187201
if (U_FAILURE(status)) {
188-
php_error(E_WARNING,
189-
"Error casting object to string in collator_convert_zstr_utf8_to_utf16()");
190-
zstr = ZSTR_EMPTY_ALLOC();
202+
collator_set_conversion_error(status, "Error converting string from UTF-8 to UTF-16");
203+
return nullptr;
191204
}
192205

193206
return zstr;
@@ -214,23 +227,31 @@ U_CFUNC zval* collator_convert_object_to_string( zval* obj, zval *rv )
214227
{
215228
/* cast_object failed => bail out. */
216229
zval_ptr_dtor( zstr );
230+
ZVAL_NULL( zstr );
231+
if( EG(exception) ) {
232+
return nullptr;
233+
}
217234
COLLATOR_CONVERT_RETURN_FAILED( obj );
218235
}
219236

220237
/* Object wasn't successfully converted => bail out. */
221238
if( zstr == nullptr )
222239
{
240+
if( EG(exception) ) {
241+
return nullptr;
242+
}
223243
COLLATOR_CONVERT_RETURN_FAILED( obj );
224244
}
225245

226246
/* Convert the string to UTF-16. */
227247
zend_string *converted_str = intl_convert_utf8_to_utf16_zstr(
228248
Z_STRVAL_P( zstr ), Z_STRLEN_P( zstr ),
229249
&status );
230-
// FIXME Or throw error or use intl internal error handler
231250
if( U_FAILURE( status ) ) {
232-
php_error( E_WARNING, "Error casting object to string in collator_convert_object_to_string()" );
233-
converted_str = ZSTR_EMPTY_ALLOC();
251+
collator_set_conversion_error(status, "Error converting object string from UTF-8 to UTF-16");
252+
zval_ptr_dtor( zstr );
253+
ZVAL_NULL( zstr );
254+
return nullptr;
234255
}
235256

236257
/* Cleanup zstr to hold utf16 string. */
@@ -339,7 +360,11 @@ U_CFUNC zend_string *collator_zval_to_string(zval *arg)
339360
return zend_string_copy(Z_STR_P(arg));
340361
}
341362

342-
zend_string *utf8_str = zval_get_string(arg);
363+
zend_string *utf8_str = zval_try_get_string(arg);
364+
if (utf8_str == nullptr) {
365+
return nullptr;
366+
}
367+
343368
zend_string *utf16_str = collator_convert_zstr_utf8_to_utf16(utf8_str);
344369
zend_string_release(utf8_str);
345370
return utf16_str;

ext/intl/collator/collator_sort.cpp

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ static const size_t DEF_SORT_KEYS_INDX_BUF_INCREMENT = 1048576;
5050

5151
static const size_t DEF_UTF16_BUF_SIZE = 1024;
5252

53+
static void collator_report_sort_error(Collator_object *co, const intl_error *sort_error)
54+
{
55+
if (sort_error->custom_error_message) {
56+
intl_errors_set(
57+
COLLATOR_ERROR_P(co), sort_error->code,
58+
ZSTR_VAL(sort_error->custom_error_message));
59+
} else {
60+
intl_errors_set_code(COLLATOR_ERROR_P(co), sort_error->code);
61+
}
62+
}
63+
5364
/* {{{ collator_regular_compare_function */
5465
static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
5566
{
@@ -59,12 +70,20 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
5970
zval norm1, norm2;
6071
zval *num1_p = nullptr, *num2_p = nullptr;
6172
zval *norm1_p = nullptr, *norm2_p = nullptr;
62-
zval *str1_p, *str2_p;
73+
zval *str1_p = nullptr, *str2_p = nullptr;
6374

6475
ZVAL_NULL(&str1);
6576
str1_p = collator_convert_object_to_string( op1, &str1 );
77+
if( str1_p == nullptr ) {
78+
return FAILURE;
79+
}
80+
6681
ZVAL_NULL(&str2);
6782
str2_p = collator_convert_object_to_string( op2, &str2 );
83+
if( str2_p == nullptr ) {
84+
rc = FAILURE;
85+
goto cleanup;
86+
}
6887

6988
/* If both args are strings AND either of args is not numeric string
7089
* then use ICU-compare. Otherwise PHP-compare. */
@@ -90,9 +109,17 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
90109
* just convert it to utf8.
91110
*/
92111
norm1_p = collator_convert_zstr_utf16_to_utf8( str1_p, &norm1 );
112+
if( norm1_p == nullptr ) {
113+
rc = FAILURE;
114+
goto cleanup;
115+
}
93116

94117
/* num2 is not set but str2 is string => do normalization. */
95118
norm2_p = collator_normalize_sort_argument( str2_p, &norm2 );
119+
if( norm2_p == nullptr ) {
120+
rc = FAILURE;
121+
goto cleanup;
122+
}
96123
}
97124
else
98125
{
@@ -109,25 +136,40 @@ static int collator_regular_compare_function(zval *result, zval *op1, zval *op2)
109136
{
110137
/* num1 is not set if str1 or str2 is not a string => do normalization. */
111138
norm1_p = collator_normalize_sort_argument( str1_p, &norm1 );
139+
if( norm1_p == nullptr ) {
140+
rc = FAILURE;
141+
goto cleanup;
142+
}
112143

113144
/* if num1 is not set then num2 is not set as well => do normalization. */
114145
norm2_p = collator_normalize_sort_argument( str2_p, &norm2 );
146+
if( norm2_p == nullptr ) {
147+
rc = FAILURE;
148+
goto cleanup;
149+
}
115150
}
116151

117152
rc = compare_function( result, norm1_p, norm2_p );
153+
}
118154

155+
cleanup:
156+
if( norm1_p )
119157
zval_ptr_dtor( norm1_p );
158+
159+
if( norm2_p )
120160
zval_ptr_dtor( norm2_p );
121-
}
122161

123162
if( num1_p )
124163
zval_ptr_dtor( num1_p );
125164

126165
if( num2_p )
127166
zval_ptr_dtor( num2_p );
128167

129-
zval_ptr_dtor( str1_p );
130-
zval_ptr_dtor( str2_p );
168+
if( str1_p )
169+
zval_ptr_dtor( str1_p );
170+
171+
if( str2_p )
172+
zval_ptr_dtor( str2_p );
131173

132174
return rc;
133175
}
@@ -170,9 +212,16 @@ static int collator_numeric_compare_function(zval *result, zval *op1, zval *op2)
170212
*/
171213
static int collator_icu_compare_function(zval *result, zval *op1, zval *op2)
172214
{
173-
int rc = SUCCESS;
174215
zend_string *str1 = collator_zval_to_string(op1);
216+
if( str1 == nullptr ) {
217+
return FAILURE;
218+
}
219+
175220
zend_string *str2 = collator_zval_to_string(op2);
221+
if( str2 == nullptr ) {
222+
zend_string_release(str1);
223+
return FAILURE;
224+
}
176225

177226
/* Compare the strings using ICU. */
178227
ZEND_ASSERT(INTL_G(current_collator) != nullptr);
@@ -184,7 +233,7 @@ static int collator_icu_compare_function(zval *result, zval *op1, zval *op2)
184233
zend_string_release(str1);
185234
zend_string_release(str2);
186235

187-
return rc;
236+
return SUCCESS;
188237
}
189238
/* }}} */
190239

@@ -197,6 +246,11 @@ static int collator_compare_func(Bucket *f, Bucket *s)
197246
zval *first = &f->val;
198247
zval *second = &s->val;
199248

249+
ZEND_ASSERT(INTL_G(current_collator_error) != nullptr);
250+
if( EG(exception) || U_FAILURE( INTL_ERROR_CODE(*INTL_G(current_collator_error)) ) ) {
251+
return 0;
252+
}
253+
200254
if( INTL_G(compare_func)( &result, first, second) == FAILURE )
201255
return 0;
202256

@@ -260,6 +314,9 @@ static collator_compare_func_t collator_get_compare_function( const zend_long so
260314
static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
261315
{
262316
UCollator* saved_collator;
317+
intl_error* saved_collator_error;
318+
intl_error sort_error;
319+
collator_compare_func_t saved_compare_func;
263320
zval* array = nullptr;
264321
HashTable* hash = nullptr;
265322
zend_array* sorted = nullptr;
@@ -282,9 +339,6 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
282339
RETURN_THROWS();
283340
}
284341

285-
/* Set 'compare function' according to sort flags. */
286-
INTL_G(compare_func) = collator_get_compare_function( sort_flags );
287-
288342
hash = Z_ARRVAL_P( array );
289343

290344
/* Copy array, so the in-place modifications will not be visible to the callback function */
@@ -297,15 +351,38 @@ static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
297351
}
298352
COLLATOR_CHECK_STATUS( co, "Error converting hash from UTF-8 to UTF-16" );
299353

354+
intl_error_init( &sort_error );
355+
300356
/* Save specified collator in the request-global (?) variable. */
301357
saved_collator = INTL_G( current_collator );
358+
saved_collator_error = INTL_G( current_collator_error );
359+
saved_compare_func = INTL_G( compare_func );
302360
INTL_G( current_collator ) = co->ucoll;
361+
INTL_G( current_collator_error ) = &sort_error;
362+
INTL_G( compare_func ) = collator_get_compare_function( sort_flags );
303363

304364
/* Sort specified array. */
305365
zend_hash_sort( sorted, collator_compare_func, renumber );
306366

307367
/* Restore saved collator. */
308368
INTL_G( current_collator ) = saved_collator;
369+
INTL_G( current_collator_error ) = saved_collator_error;
370+
INTL_G( compare_func ) = saved_compare_func;
371+
372+
if( EG(exception) ) {
373+
zend_array_destroy( sorted );
374+
intl_error_reset( &sort_error );
375+
RETURN_THROWS();
376+
}
377+
378+
if( U_FAILURE( INTL_ERROR_CODE(sort_error) ) ) {
379+
zend_array_destroy( sorted );
380+
collator_report_sort_error(co, &sort_error);
381+
intl_error_reset( &sort_error );
382+
RETURN_FALSE;
383+
}
384+
385+
intl_error_reset( &sort_error );
309386

310387
/* Convert strings in the specified array back to UTF-8. */
311388
collator_convert_hash_from_utf16_to_utf8( sorted, COLLATOR_ERROR_CODE_P( co ) );

ext/intl/php_intl.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ PHP_RINIT_FUNCTION( intl )
278278
PHP_RSHUTDOWN_FUNCTION( intl )
279279
{
280280
INTL_G(current_collator) = NULL;
281+
INTL_G(current_collator_error) = NULL;
281282
if (INTL_G(grapheme_iterator)) {
282283
grapheme_close_global_iterator( );
283284
INTL_G(grapheme_iterator) = NULL;

ext/intl/php_intl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ extern zend_module_entry intl_module_entry;
4545

4646
ZEND_BEGIN_MODULE_GLOBALS(intl)
4747
struct UCollator *current_collator;
48+
intl_error *current_collator_error;
4849
char* default_locale;
4950
collator_compare_func_t compare_func;
5051
UBreakIterator* grapheme_iterator;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
Collator::sort() reports conversion errors from comparison callbacks
3+
--EXTENSIONS--
4+
intl
5+
--FILE--
6+
<?php
7+
class BadString {
8+
public function __toString(): string {
9+
return "\xFF";
10+
}
11+
}
12+
13+
$coll = new Collator('en_US');
14+
$array = ['b', new BadString(), 'a'];
15+
16+
var_dump($coll->sort($array, Collator::SORT_STRING));
17+
var_dump(intl_get_error_code() === U_INVALID_CHAR_FOUND);
18+
echo intl_get_error_message(), "\n";
19+
var_dump($coll->getErrorCode() === U_INVALID_CHAR_FOUND);
20+
echo $coll->getErrorMessage(), "\n";
21+
?>
22+
--EXPECT--
23+
bool(false)
24+
bool(true)
25+
Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND
26+
bool(true)
27+
Collator::sort(): Error converting string from UTF-8 to UTF-16: U_INVALID_CHAR_FOUND

0 commit comments

Comments
 (0)