@@ -2257,7 +2257,7 @@ ZEND_API zend_result ZEND_FASTCALL compare_function(zval *result, zval *op1, zva
22572257}
22582258/* }}} */
22592259
2260- static int compare_long_to_string (zend_long lval , zend_string * str ) /* {{{ */
2260+ static int compare_long_to_string (zend_long lval , zend_string * str , bool transitive_mode ) /* {{{ */
22612261{
22622262 zend_long str_lval ;
22632263 double str_dval ;
@@ -2274,7 +2274,7 @@ static int compare_long_to_string(zend_long lval, zend_string *str) /* {{{ */
22742274 /* String is non-numeric. In transitive mode, enforce consistent ordering.
22752275 * Empty string < numeric < non-numeric string.
22762276 * Since str is non-numeric, check if it's empty. */
2277- if (UNEXPECTED (EG ( transitive_compare_mode ) )) {
2277+ if (UNEXPECTED (transitive_mode )) {
22782278 /* Empty string comes before everything */
22792279 if (ZSTR_LEN (str ) == 0 ) {
22802280 return 1 ; /* lval > empty string */
@@ -2291,7 +2291,7 @@ static int compare_long_to_string(zend_long lval, zend_string *str) /* {{{ */
22912291}
22922292/* }}} */
22932293
2294- static int compare_double_to_string (double dval , zend_string * str ) /* {{{ */
2294+ static int compare_double_to_string (double dval , zend_string * str , bool transitive_mode ) /* {{{ */
22952295{
22962296 zend_long str_lval ;
22972297 double str_dval ;
@@ -2310,7 +2310,7 @@ static int compare_double_to_string(double dval, zend_string *str) /* {{{ */
23102310 /* String is non-numeric. In transitive mode, enforce consistent ordering.
23112311 * Empty string < numeric < non-numeric string.
23122312 * Since str is non-numeric, check if it's empty. */
2313- if (UNEXPECTED (EG ( transitive_compare_mode ) )) {
2313+ if (UNEXPECTED (transitive_mode )) {
23142314 /* Empty string comes before everything */
23152315 if (ZSTR_LEN (str ) == 0 ) {
23162316 return 1 ; /* dval > empty string */
@@ -2331,6 +2331,8 @@ ZEND_API int ZEND_FASTCALL zend_compare(zval *op1, zval *op2) /* {{{ */
23312331{
23322332 bool converted = false;
23332333 zval op1_copy , op2_copy ;
2334+
2335+ bool transitive_mode = UNEXPECTED (EG (transitive_compare_mode ));
23342336
23352337 while (1 ) {
23362338 switch (TYPE_PAIR (Z_TYPE_P (op1 ), Z_TYPE_P (op2 ))) {
@@ -2375,24 +2377,24 @@ ZEND_API int ZEND_FASTCALL zend_compare(zval *op1, zval *op2) /* {{{ */
23752377 return Z_STRLEN_P (op1 ) == 0 ? 0 : 1 ;
23762378
23772379 case TYPE_PAIR (IS_LONG , IS_STRING ):
2378- return compare_long_to_string (Z_LVAL_P (op1 ), Z_STR_P (op2 ));
2380+ return compare_long_to_string (Z_LVAL_P (op1 ), Z_STR_P (op2 ), transitive_mode );
23792381
23802382 case TYPE_PAIR (IS_STRING , IS_LONG ):
2381- return - compare_long_to_string (Z_LVAL_P (op2 ), Z_STR_P (op1 ));
2383+ return - compare_long_to_string (Z_LVAL_P (op2 ), Z_STR_P (op1 ), transitive_mode );
23822384
23832385 case TYPE_PAIR (IS_DOUBLE , IS_STRING ):
23842386 if (zend_isnan (Z_DVAL_P (op1 ))) {
23852387 return 1 ;
23862388 }
23872389
2388- return compare_double_to_string (Z_DVAL_P (op1 ), Z_STR_P (op2 ));
2390+ return compare_double_to_string (Z_DVAL_P (op1 ), Z_STR_P (op2 ), transitive_mode );
23892391
23902392 case TYPE_PAIR (IS_STRING , IS_DOUBLE ):
23912393 if (zend_isnan (Z_DVAL_P (op2 ))) {
23922394 return 1 ;
23932395 }
23942396
2395- return - compare_double_to_string (Z_DVAL_P (op2 ), Z_STR_P (op1 ));
2397+ return - compare_double_to_string (Z_DVAL_P (op2 ), Z_STR_P (op1 ), transitive_mode );
23962398
23972399 case TYPE_PAIR (IS_OBJECT , IS_NULL ):
23982400 return 1 ;
@@ -3449,26 +3451,31 @@ ZEND_API int ZEND_FASTCALL zendi_smart_strcmp(zend_string *s1, zend_string *s2)
34493451 zend_long lval1 = 0 , lval2 = 0 ;
34503452 double dval1 = 0.0 , dval2 = 0.0 ;
34513453
3454+ /* Handle empty strings */
3455+ if (UNEXPECTED (s1 -> len == 0 || s2 -> len == 0 )) {
3456+ if (UNEXPECTED (EG (transitive_compare_mode ))) {
3457+ if (s1 -> len == 0 && s2 -> len == 0 ) return 0 ;
3458+ return s1 -> len == 0 ? -1 : 1 ;
3459+ }
3460+ goto string_cmp ;
3461+ }
3462+
3463+ /* Skip numeric parsing if both strings start with letters */
3464+ unsigned char c1 = (unsigned char )s1 -> val [0 ];
3465+ unsigned char c2 = (unsigned char )s2 -> val [0 ];
3466+
3467+ if (((c1 >= 'a' && c1 <= 'z' ) || (c1 >= 'A' && c1 <= 'Z' )) &&
3468+ ((c2 >= 'a' && c2 <= 'z' ) || (c2 >= 'A' && c2 <= 'Z' ))) {
3469+ goto string_cmp ;
3470+ }
3471+
34523472 ret1 = is_numeric_string_ex (s1 -> val , s1 -> len , & lval1 , & dval1 , false, & oflow1 , NULL );
34533473 ret2 = is_numeric_string_ex (s2 -> val , s2 -> len , & lval2 , & dval2 , false, & oflow2 , NULL );
34543474
3455- /* When in transitive comparison mode (used by SORT_REGULAR), enforce transitivity
3456- * by consistently ordering numeric vs non-numeric strings. */
3475+ /* In transitive mode, enforce numeric < non-numeric ordering */
34573476 if (UNEXPECTED (EG (transitive_compare_mode ))) {
34583477 int type_mismatch = (!!ret1 ) ^ (!!ret2 );
34593478 if (UNEXPECTED (type_mismatch )) {
3460- /* One is numeric, one is not.
3461- * Special case: empty strings are non-numeric but sort BEFORE numeric strings.
3462- * Order: empty < numeric < non-numeric (matches PHP 8+ comparison semantics) */
3463- bool is_empty1 = (s1 -> len == 0 );
3464- bool is_empty2 = (s2 -> len == 0 );
3465-
3466- if (is_empty1 || is_empty2 ) {
3467- /* If one is empty, empty comes first */
3468- return is_empty1 ? -1 : 1 ;
3469- }
3470-
3471- /* Neither is empty: numeric < non-numeric */
34723479 return ret1 ? -1 : 1 ;
34733480 }
34743481 }
0 commit comments