[php-src] Issue #24552: Case-insensitive substr_compare, ksort, krsort are still locale-dependent
| From: | sirreal | Date: | Thu, 22 Oct 2026 15:53:23 +0000 |
| Subject: | [php-src] Issue #24552: Case-insensitive substr_compare, ksort, krsort are still locale-dependent | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253102@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24552
Author: sirreal
### Description
The following code:
```php
<?php
foreach (['C', 'tr_TR.UTF-8'] as $locale) {
if (setlocale(LC_CTYPE, $locale) === false) {
throw new RuntimeException("Locale unavailable: $locale");
}
$values = ['I', 'h'];
sort($values, SORT_STRING | SORT_FLAG_CASE);
$keys = ['I' => 0, 'h' => 0];
ksort($keys, SORT_STRING | SORT_FLAG_CASE);
echo $locale, "\n";
echo ' stripos: ', var_export(stripos('I', 'i'), true),
"\n";
echo ' strncasecmp: ', strncasecmp('I', 'i', 1),
"\n";
echo ' substr_compare: ', substr_compare('I', 'i', 0, 1, true),
"\n";
echo ' sort: ', implode(',', $values), "\n";
echo ' ksort: ', implode(',', array_keys($keys)), "\n";
}
```
Resulted in this output:
```
C
stripos: 0
strncasecmp: 0
substr_compare: 0
sort: h,I
ksort: h,I
tr_TR.UTF-8
stripos: 0
strncasecmp: 0
substr_compare: -32
sort: h,I
ksort: I,h
```
But I expected this output instead:
```
C
stripos: 0
strncasecmp: 0
substr_compare: 0
sort: h,I
ksort: h,I
tr_TR.UTF-8
stripos: 0
strncasecmp: 0
substr_compare: 0
sort: h,I
ksort: h,I
```
[Reproduction link](https://3v4l.org/8A33E)
---
PHP 8.2 switched
strtolower(), stripos(), sort() with
SORT_STRING | SORT_FLAG_CASE
[#](https://www.php.net/manual/en/array.constants.php#constant.sort-flag-case) and related functions
to ASCII case conversion, but substr_compare() with $case_insensitive =
true and ksort()/krsort() with SORT_STRING |
SORT_FLAG_CASE still follow LC_CTYPE.
The output above is from PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12. PHP 8.1.34 prints the same, except
that stripos() and sort() also follow the locale (false and
I,h). 8.2 changed those two but not substr_compare() or
ksort().
<details>
<summary>Reproducing on PHP 8.1 to 8.5 with Docker</summary>
The official php:*-cli images have no tr_TR.UTF-8 locale, so each run
generates it first. With the snippet above saved as repro.php:
```sh
for v in 8.1 8.2 8.3 8.4 8.5; do
docker run --rm -i "php:$v-cli" sh -c '
apt-get update -qq >/dev/null &&
apt-get install -y -qq locales >/dev/null 2>&1 &&
localedef -i tr_TR -f UTF-8 tr_TR.UTF-8 &&
php -v | head -n 1 &&
php' < repro.php
done
```
</details>
The 8.2 [UPGRADING](https://github.com/php/php-src/blob/PHP-8.2/UPGRADING) notes say sorting with
SORT_FLAG_CASE uses ASCII case conversion, and the [strtolower-ascii
RFC](https://wiki.php.net/rfc/strtolower-ascii) says case-insensitive substr_compare()
already did.
On master, substr_compare() calls zend_binary_strncasecmp_l(), and
php_array_key_compare_string_case_unstable_i(), which ksort() and
krsort() use, calls zend_binary_strcasecmp_l(). Both helpers fold case
with zend_tolower(), which is the C library's tolower() and follows
LC_CTYPE. Under glibc's tr_TR.UTF-8,
tolower('I') is 'I'.
Switching these two call sites to zend_binary_strncasecmp() and
zend_binary_strcasecmp() would give the documented behavior.
See https://github.com/php/php-src/pull/7506.
### PHP Version
```plain
PHP 8.5.12 (cli) (built: Oct 21 2026 15:53:40) (NTS)
Copyright (c) The PHP Group
Built by https://github.com/docker-library/php
Zend Engine v4.5.12, Copyright (c) Zend Technologies
with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies
```
### Operating System
Debian GNU/Linux 13 (trixie), glibc 2.41 (php:8.5-cli Docker image)