Починил ucfirst в PHP

В отпуске дошли руки посмотреть и починить баг в функции ucfirst — по задумке она должна приводить в верхний регистр первый символ однобайтовой строки, учитывая текущую локаль. Всё бы хорошо, но с буквой «я» кодовой страницы 1251 это не срабатывало — она оставалась в нижнем регистре.

Интересно, что похожая функция — strtoupper тут работает прекрасно, то есть вот такой код на текущих версиях ПХП выдаёт false:

if (setlocale(LC_CTYPE, "ru_RU.cp1251") === false) {
    throw new RuntimeException();
}
// буква «я» имеет в кодировке «1251» код 255
var_dump(ucfirst(chr(255)) === strtoupper(chr(255));

Поскольку эта беда уже второй раз ломает тесты в нашем продукте (программисты об этой особенности просто забывают), решил глянуть в исходники. Внутри ПХП обе функции — проблемная и рабочая, используют вызов toupper, в документации к которой человеческим языком сказано, что передаваемый параметр должен иметь тип unsigned char.

Функция strtoupper как раз использует unsigned char, а ucfirst — char (который signed). Документация к toupper такую вольность по отношению к типам осуждает — там прямым текстом написано, что могут быть проблемы, так как внутри входной параметр будет расширен до int, знак перенесётся на старший разряд и всё сломается.

Что, собственно, и происходит с буквой «я», код которой как раз получается отрицательным. Удивительно, что с другими «отрицательными» буквами всё каким-то чудом работает.

В общем, мой патч приняли в версию 7.3, и как только выйдут новые версии поддерживаемых веток интерпретаторов, мы, наконец, перестанем спотыкаться об этот баг.

Поделиться
Отправить
 312   3 мес   php   программирование
6 комментариев
hshhhhh.name 3 мес

setlocale(LC_CTYPE, "ru_RU.cp1251")

Вы меня извините за снобизм, но ЗАЧЕМ? Разве существуют кодировки кроме utf8?

Евгений Степанищев 3 мес

Причина более чем банальна — экономия денег. Однобайтовая кодировка — это почти в два раза меньший объём дорогущих SSD на сотнях серверов (или тысячах? я что-то давно не знаю сколько у нас их), вдвое меньший I/O, меньшие затраты на CPU (например, все взятия по индексу в UTF-8 становятся O(n), а не O(1)), ну и так далее.

PastorGL 3 мес

на прошлой работе в проекте тоже поначалу была однобайтная кодировка. и в базе, и в UI, и везде. а что, продукт жил в США, так что всё, что за пределами Latin1, попросту выкидывалось как ненужное. зачем тратиться на utf8, если всё равно актуален только американский английский?

а потом однажды решили выйти на мир, и локализовать продукт на 11 языков. пы-дыщь! не вечно же стартапом сидеть, надо же и в ынторпрайз вырастать, миллиарды для инвесторов зарабатывать.

моя команда занималась переписыванием 1.5 MLoC кода на жабе и JS 11 месяцев в отдельной utf8 ветке. продукт в это время продолжал развиваться, поэтому последние 9 месяцев я лично каждый день синхронизировал изменения в обе стороны. проще было бы вообще выкинуть нахрен все кишки, и написать заново, но нельзя из-за объёма. и худшего кошмара, чем итоговый коммит на 600 тыщ строк, я за все 20 с лишним лет своей карьеры не припомню.

но ничего. ничего таки не сломали. но если бы изначально не экономили на спичках, не пришлось бы почти год тратить на переписывание.

Евгений Степанищев 3 мес

Ну да, такое тоже бывает. Но в нашем случае вероятность такого крайне невелика — продукт сильно заточен под российское законодательство, отвязывать его — проблема куда более серьёзная, чем смена кодировки и куда менее понятная.

Ну и у нас не на спичках экономия, как можно наверное понять из предыдущего моего комментария.

hshhhhh.name 3 мес

Причина более чем банальна — экономия денег.

звучит разумно и от этого ещё больнее :)

Евгений Степанищев 3 мес

Да нам не особо и больно :) Не знаю почему у многих такое отвращение к этой кодировке ) На ПХП до сих приходится использовать костыли, чтобы работать с многобайтовыми кодировками, так что с cp1251 наоборот — даже проще :)

hshhhhh.name 3 мес

Не знаю почему у многих такое отвращение к этой кодировке

испытываю отвращение к любым кодировкам кроме utf8
как и к любым таймзонам кроме utc

так что с cp1251 наоборот — даже проще :)

а вы в браузер тоже cp1251 показываете? ведь для того же json надо уже конвертировать, например.

Евгений Степанищев 3 мес

испытываю отвращение к любым кодировкам кроме utf8

ЮТФ-8 — худшая кодировка, на мой взгляд ) Сделанная как раз с целью экономии на спичках и совместимости с легаси :)

ведь для того же json надо уже конвертировать, например.

Зачем?

Алексей 3 мес

Какая альтернатива UTF-8 для международных текстов? Всё остальное объективно хуже на мой взгляд:

  • UTF-8 не требует жуткого BOM в начале файлов
  • по содержимому можно надёжно определить, что текст закодирован в UTF-8 (если кодировка не известна)
  • уже упомянутая обратная совместимость с ASCII

Наконец, как вы живёте без Unicode смайликов? 😉

Евгений Степанищев 3 мес

UCS-4, конечно. Очевидный выбор, вроде. BOM не является обязательным, и в UTF-8 он может применяться точно так же, см. табличку:

In the table <BOM> indicates that the byte order is determined by a byte order mark, if present at the beginning of the data stream, otherwise it is big-endian.

Следующий аргумент:

по содержимому можно надёжно определить, что текст закодирован в UTF-8 (если кодировка не известна)

Готов поспорить, что не всегда. И не понимаю зачем это нужно сейчас.

уже упомянутая обратная совместимость с ASCII

Почему это вообще для нас какое-то достоинство?

AlexD 2 мес

Спасибо вам за патч! =)

Евгений Степанищев 2 мес

Рад, что смог ещё кому-то помочь :)

Популярное