Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

Починил 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, и как только выйдут новые версии поддерживаемых веток интерпретаторов, мы, наконец, перестанем спотыкаться об этот баг.

6 комментариев
hshhhhh.name 2020

setlocale(LC_CTYPE, "ru_RU.cp1251")

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

Евгений Степанищев 2020

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

PastorGL 2020

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

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

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

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

Евгений Степанищев 2020

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

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

hshhhhh.name 2020

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

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

Евгений Степанищев 2020

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

hshhhhh.name 2020

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

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

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

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

Евгений Степанищев 2020

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

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

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

Зачем?

Алексей 2020

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

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

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

Евгений Степанищев 2020

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 2020

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

Евгений Степанищев 2020

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