php

PHP: что ты такое?

Мне сегодня показали вот такой код и спросили, знал ли я, что так можно. Речь идёт о вызове $this::w00t(). То есть мы используем $this как бы в статическом контексте. Мне бы такое в голову не пришло.

Код ниже, понятное дело, чисто демонстрационный.

Нет, я не знал, но, как ни удивительно, эта конструкция работает! Причём работает уже очень давно — это не свежее веяние: она появилась ещё в версии 5.4!

(new class() {
    public function w00t():void {
        echo "WHAT THE HELL ARE YOU?\n";
    }

    public function test():void {
        $this::w00t();
    }
})->test();

Дополнено: работает как эквивалент static, что ожидаемо, конечно.

4 комментария

Коммунистический массив

Я тут в мемах, которые кинули бывшие коллеги, увидел «коммунистическую сортировку» — она каждому присваивает среднее значение, и не смог пройти мимо. Написал «коммунистический массив»:

class CommunistArray extends \ArrayObject
{
    public function __construct(array $array = [])
    {
        parent::__construct(self::createCommunistArray($array));
    }

    public function offsetSet(mixed $key, mixed $value): void
    {
        parent::offsetSet($key, $value);
        $this->redistribute();
    }

    public function offsetUnset(mixed $key): void
    {
        $first = $this->getIterator()->key();

        if ($first !== null) {
            $this[$first] += $this[$key];
            parent::offsetUnset($key);
            $this->redistribute();
        }
    }

    private static function createCommunistArray(array $array): array
    {
        if ($array) {
            $avg = array_sum($array) / sizeof($array);
            return array_fill_keys(array_keys($array), $avg);
        }

        return [];
    }

    private function redistribute(): void
    {
        (void) $this->exchangeArray(self::createCommunistArray((array) $this));
    }
}

Он делает всех равными и перераспределяет блага автоматически:

$comrades = new CommunistArray([0b1, 0o0, 3e1, 0x45]);

print_r($comrades); # 25, 25, 25, 25

$comrades[2] = 4;
print_r($comrades); # 19.75, 19.75, 19.75, 19.75

unset($comrades[0]);
print_r($comrades); # 24.6875, 24.6875, 24.6875

8 комментариев

Возвращение Text_Figlet

Нейросеть «Опус 4.6», конечно, жрёт невероятное количество денег.

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

У меня уже много лет чешутся руки переделать мою старенькую библиотеку Text_Figlet. Это, наверное, самое популярное моё детище — она время от времени встречается мне в каких-то продуктах и даже входит в виде пакета в некоторые дистрибутивы «Линукса».

Библиотека написана на ПХП и умеет выводить текст специальными фиглет-шрифтами, составленными из символов. Та моя версия, помимо этого, умеет делать так называемый smushing — объединение соседних букв по специальным правилам. Этот эффект виден в примере ниже.

«Смашинг» был сделан только по горизонтали; вертикальный алгоритм я в своё время не дописал, и этот факт — одна из причин, почему мне захотелось вернуться к библиотеке.

 _  _     _ _                        _    _ _ 
| || |___| | |___    __ __ _____ _ _| |__| | |
| __ / -_) | / _ \_  \ V  V / _ \ '_| / _` |_|
|_||_\___|_|_\___( )  \_/\_/\___/_| |_\__,_(_)
                 |/                           

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

Какое-то офигительное чувство — когда задуманное так быстро превращается в код. Да, его до сих пор приходится просматривать, чтобы подсказывать нейросети, что переделать, но, в общем-то, меня это не коробит. Зато скорость программирования вырастает многократно. Я даже не могу предположить, сколько ещё откладывал бы переделку этой библиотеки, если бы не было настолько продвинутых нейросетей.

10 комментариев

PHP: (binary)

И, чтобы, как говорится «два раза не вставать», расскажу про самую бесполезную конструкцию в ПХП. Я знаю, что не так много народу знают про (unset), про которую я ворчал вчера, а про эту вещицу, как мне кажется, знает ещё меньше программистов на ПХП.

Одна из неприятных вещей, которые остались в ПХП с давних времён — работа встроенных функций и конструкций только со строками в однобайтовых кодировках. С этим можно жить — есть библиотеки, которые позволяют замести эту проблему под ковёр, но сам язык помогать вам в этом не будет.

Какое-то время назад с этим пытались что-то сделать — в шестой, так и не вышедшей версии ПХП строки решили разделить на два типа — binary, это однобайтовые строки, которые остались в языке до сих пор и string — строки с Юникодом.

К сожалению, работу доделать не осилили, очень уж было сложно, особенно тяжело шло с расширениями. В итоге версия так и не вышла, но так как о ней широко раструбили, пришлось даже пропустить номер — после ПХП 5 вышла сразу седьмая версия.

Но некоторые отголоски этой не совершившейся революции всё-таки можно обнаружить и в современных версиях ПХП.

В версии 8.5 по сих пор есть преобразование типа (binary), которое является алиасом к (string), но выдаёт предупреждение об устаревшей конструкции. А кроме этого, у строки есть префикс b: b"string", придуманный для того, чтобы создавать в ПХП6 однобайтовые строки. В современных версиях ПХП он, разумеется, ничего со строкой не делает.

И эту всю красоту из языка почему-то не удалили, а от (unset) избавились. Никакой логики.

4 комментария

PHP: (unset) и (void)

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

Как мне кажется, самая большая проблема языка сейчас — это его процедурное прошлое, которое торчит изо всех щелей. Но есть и менее заметная, при этом системная проблема: у языка нет «великодушного пожизненного диктатора», как у Пайтона или Линукса, или более «сыгранной» команды, определяющей долгосрочные цели в развитии языка.

Некоторые разумные, в общем-то, вещи забрасываются (например, SPL — стандартная библиотека), некоторые — не дожимаются до конца (например, в языке появились обнуляемые типы, но приведение одного типа к другому этого не учитывает).

Я хочу поворчать на одну из таких мелких тем.

В ПХП с незапамятных времён была конструкция (unset), которая приводила любой тип к NULL. Не самое очевидное название, поэтому, наверное, и применял её мало кто — не всем очевидно чем она могла бы быть полезна.

Тем не менее, я со временем догадался, что можно её приспособить, чтобы обозначать, что чем-либо возвращаемое значение я не просто забыл как-то использовать, а вполне сознательно выкинул. Что-то вроде такого:

class Smth
{
    private function saveSmth($stat)
    {
      // тут какие-то действия
      // прошло успешно, но выше где-то может быть и false
      return true;
    }

    public function doSmth($action, $stat)
    {
       // тут какой-то код
       // результат вызова метода ниже мы игнорируем
       (unset) $this->saveSmth($stat);
       return true;
     }
}

Мне казалось и до сих пор кажется, что это полезный паттерн. В «Гоу», например, в этом случае нужно присвоить возвращаемое значение специальной переменной _, иначе компилятор ругается.

И вот, много лет назад, в версии 7.2 использование (unset) в коде стало вызывать предупреждение об устаревшей конструкции. Я тогда попытался как-то повлиять на это, но не осилил пробиться к людям принимающим решение и махнул рукой. Позже в версии 8.0 конструкцию убрали вовсе и много лет не в языке было хорошего способа делать то же самое.

А в версии 8.5 внезапно та же идея возвращается под новым именем, только теперь нужно писать (void), а не (unset).

Да, имя более логичное, да функционал расширен — появился ещё и новый атрибут, да синтаксис изменён — результат (unset) можно было чем-то присвоить, с (void) так поступать нельзя. Но почему столько лет назад выплеснули с водой ребёнка и вообще никак не смотрели в эту сторону?

3 комментария

Митап по PHP

Признаюсь, я как-то подрастерял былой пыл в ведении моих записей, поэтому отлыниваю написать даже о самых ярких событиях, к которым я недавно имел отношение. Например, в прошлую субботу, 19 апреля наша компания устаивала митап по языку программирования PHP, а я был одним из её центральных организаторов.

Если судить по отзывам, событие чрезвычайно всем понравилось. Многие сказали, что давно не участвовали в чём-либо настолько хорошо проведённом, хотя, на мой взгляд, огрехи тоже были. В конце-концов, это первый митап, который мы сделали своими силами.

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

Мы начали примерно за два месяца, тем не менее, кое-что пришлось делать буквально в последнюю неделю — например, только в последнюю неделю стало понятно сколько примерно народу придёт, потому что люди записывались и записывались. От количества зависело сколько арендовать стульев для сидения и столов для кофе-брейка, а так же количество еды.

Я очень переживал за эти вещи, потому что при форс-мажоре мы бы просто не успели найти новых подрядчиков.

Большое спасибо докладчиками — Александру Макарову, Максиму Пестову и Станиславу Кошевому за то, что вызвались поучаствовать и интересные доклады.

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

Два мужчины в белых футболках смотрят на экран смартфона; один держит бутылку воды, другой — часы на запястье, на фоне зелёных растений и оранжевых труб.
Я (справа) объясняю что-то Максиму Пестову — одному из выступающих на нашем митапе
Спикер в чёрной футболке и кепке выступает перед аудиторией в светлом зале с растениями; на экране — код с заголовком «array_merge за циклом» и примерами PHP-кода.
Александр Макаров, один из лидеров разработки фреймворка yii выступает на нашем митапе

2 комментария

Анонимные функции в PHP

Относительно недавно в ПХП появился новый способ создания анонимных функций. Раньше мы писали array_filter($strs, 'strlen'), теперь можем писать array_filter($strs, strlen(...)).

В первом случае у нас указывается строка, во втором — анонимная функция. Последнее намного лучше, конечно, потому что помогает строгой типизации и лучше понимается редакторами кода.

Когда этот синтаксис появился, в моей голове он стал частным случаем стрелочного синтаксиса. В самом деле, ведь fn($t) => sin($t) и sin(...) — это одно и то же. Но недавно я понял, что у второго синтаксиса есть побочный эффект. По какой-то причине раньше мне это было не очевидно.

Если посмотреть на код ниже, всё должно стать понятно, но если нет, то я поясню.

Тут мы видим класс, конструктор которого печатает код, находящийся в строке, которая его вызвала. Ниже класса создаются две анонимные функции, одна — стрелочная через fn(), вторая — через многоточие.

Этот код позволяет проиллюстрировать разницу.

В случае создания анонимной функции через fn(), конструктор будет вызван каждый раз заново на любой вызов функции.

В случае синтаксиса с многоточием, конструктор будет вызван сразу при создании, а при вызове, в том числе многократном, он вызываться не будет.

То есть каждый способ имеет особенности, которые надо учитывать.

class Sample
{
    public function __construct()
    {
        $bt = debug_backtrace();
        ['line' => $line, 'file' => $filename] = end($bt);

        echo file($filename)[$line - 1];
    }

    public function method(): void
    {
    }
}

$a = fn() => (new Sample)->method(); // не выведет ничего
$b = (new Sample)->method(...); // выведется «$b = (new Sample)->method(...);»

$a(); $a(); // выведется «$a(); $a();» два раза
$b(); $b(); // не выведет ничего

5 комментариев

Библиотека itertools для PHP

В качестве развлечения написал тесты к своей старой библиотеке itertools для ПХП и выяснил, что в ней полным-полно ошибок. В итоге, переписал всё на класс, исправил ошибки, добавил функции, которые появились в оригинальной библиотеке за это время, обмазал всё типами, вставил тесты и поддержку «композера».

Пример использования теперь стал выглядеть вот так:

use Itertools\Itertools as it;

foreach (it::islice(it::cycle('ABC'), 10) as $element) {
    echo $element;
}

2 комментария

Простейший HTTP-прокси

У Романа Парпалака прочитал про «простейший HTTP-прокси». Ну нет, действительно простейший HTTP-прокси как-то я изобрёл, когда надо было обновить сервер у одного из заказчиков, а там интернета не было. Я тогда запустил этот прокси у себя и прокинул соединение к себе через SSH-туннель.

Выглядит он так:

<?php readfile($_SERVER["REQUEST_URI"]);

Запускается так:

php -S 127.0.0.1:8080 proxy.php

А вот так работает:

Скриншот терминала: PHP-сервер на localhost:8080, curl-запрос к lib.ru с конвертацией koi8-r, вывод с HTML-заголовком «Lib.Ru: Библиотека Максима Мошкова».

4 комментария

Модуль scalar_objects

Очень мне нравится идея навести порядок в функциях ПХП при помощи присобачивания к примитивным типам что-то вроде методов.

К именованию встроенных функций есть много претензий:

  • различный стиль наименований: html_​entity_​decode, но htmlentities;
  • неинтуитивность некоторых названий: strcspn, strpbrk;
  • разный порядок параметров: strpos($haystack, $needle), array_search($needle, $haystack);

Считается, что это один из серьёзных недостатков ПХП. Напрашивается очевидное решение — сделать всем проблемным функциям алиасы, но так язык замусорится ещё сильнее.

А вот добавив псевдометоды у примитивных типов, можно сразу сразу сделать хорошо. Один из контрибьютеров даже сделал модуль, который показывает как это могло бы выглядеть:

declare(strict_types=1);

class IntHandler {
    public static function abs(int $self): int {
        return abs($self);
    }

    public static function div(int $self, int $other): int {
        return intdiv($self, $other);
    }

    public static function sqrt(int $self): float {
	return sqrt($self);
    }
}

register_primitive_type_handler('int', 'IntHandler');

$i = -5;
var_dump($i->abs()->div(2)->sqrt()); // 1.4142135623730951

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

Кроме того, повышается читаемость. Сравните два подхода

$result = substr(strtr(rtrim($className, '_'), '\\', '_'), 15);
$result = $className->trimRight('_')->replace('\\', '_')->slice(15);

Как по мне, очень хорошее решение, совершенно непонятно почему оно ещё не в языке.

Оставить комментарий

Бессонный хакатон: где экран?

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

У меня какое-то время не было хороших идей как бы это сделать, всё что мне приходило в голову — это перебрать координаты и найти несколько примыкающих к друг другу палок определённого размера и цвета, которые окаймляют искомый экран.

Такое решение наводило на меня тоску, но потом вспомнилось, что в графической библиотеке ПХП есть фильтр, который выделяет края и я решил глянуть не будет ли от него пользы.

Эмулятор ZX Spectrum в браузере Opera: меню «ZX-РЕВЮ 11-12 1997» с пунктами «Описание оболочки», «Путеводитель», «Авторская разработка» и др.; внизу — виртуальная клавиатура.
Окно браузера, в котором запущен эмулятор; наша задача — понять где тут экран эмулируемого компьютера

Код для наложения фильтров выглядит следующим образом:

$im = imagecreatefrompng($screenshot_file);

imagefilter($im, IMG_FILTER_GRAYSCALE);
imagefilter($im, IMG_FILTER_EDGEDETECT);

Мне казалось, что на получившемся изображении можно искать четыре точки экрана на известном расстоянии, но оказалось, что у такого метода есть ложные срабатывания. Поэтому я стал перебирать на экране все горизонтальные белые линии, пока не встречаю две параллельные нужной мне длины.

Эмулятор ZX Spectrum в браузере Opera: меню «ZX-РЕВЮ 11-12 1997» с пунктами «Описание оболочки», «Путеводитель» и др., внизу — виртуальная клавиатура.
Окно браузера после применения фильтров: переведено в ч/б, выделены края

Таким образом очень надёжно находится граница экрана эмулятора, от которой на фиксированном расстоянии находится искомый экран. В итоге получается вот такой код:

const FRAME_W = 1039;
const FRAME_H = 808;
const FRAME_COLOR = 0xFFFFFF;
const FRAME_BORDER = 30;

$im = imagecreatefrompng($_SERVER["argv"][1]);

[$w, $h] = [imagesx($im), imagesy($im)];
imagefilter($im, IMG_FILTER_GRAYSCALE);
imagefilter($im, IMG_FILTER_EDGEDETECT);

for ($y = 0; $y <= $h - FRAME_H; $y++) {
    for ($x = 0; $x <= $w - FRAME_W; $x++) {
        $is_frame = imagecolorat($im, $x, $y) === FRAME_COLOR;

        for ($i = 1; $i<FRAME_W; $i+=2) {
            $is_frame = 
                $is_frame &&
                imagecolorat($im, $x + $i, $y) === FRAME_COLOR &&
                imagecolorat($im, $x + $i, $y + FRAME_H - 1) === FRAME_COLOR;

            if (!$is_frame) {
                break;
            }
        }

        if ($is_frame) {
            printf(
                "%d,%d\n",
                intdiv($x, 2) + FRAME_BORDER,
                intdiv($y, 2) + FRAME_BORDER
            );
            exit(0);
        }
    }
}
exit(1);

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

Оставить комментарий

CVE 2023-0567

В ПХП нашли достаточно серьёзный, на мой взгляд, баг — на некоторых повреждённых хешах функция password_verify возвращает true на любой пароль. Получается, хитрый хакер может оставить себе тайный ход — поменять в базе хеш на битый, и пользователь ничего не заметит — пароль-то по-прежнему подходит.

Вот как это выглядит:

var_dump(password_verify("foo", '$2y$04$00000000$')); // true
var_dump(password_verify("bar", '$2y$04$00000000$')); // true

Удивляет, что один из разработчиков попытался защитить текущее поведение интерпретатора:

The documentation for password_verify() clearly states that the $hash has to be valid:

> hash
> A hash created by password_hash().

I fail to see how this is a bug if you feed it garbage; also the documentation to crypt states:

> Using characters outside of this range in the salt will cause crypt() to return a zero-length string. The two digit cost parameter is the base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithm and must be in range 04-31, values outside this range will cause crypt() to fail.

PHP clearly behaves as described.

Что бы ни было написано в документации, возвращать true на любой пароль из функции проверки правильности пароля — очень плохая идея, как мне кажется, это должно быть довольно очевидно.

При анализе оказалось, что баг занесён ещё в 2008-м году (!), причём исследовавший эту историю разработчик хоть и осторожен в выражениях, всё же похоже считает, что уязвимость появилась неслучайно:

PHP’s implementation of crypt_blowfish differs from the upstream Openwall version by adding a «PHP Hack», which allows one to cut short the BCrypt salt by including a $ character within the characters that represent the salt.

Hashes that are affected by the «PHP Hack» may erroneously validate any password as valid when used with password_verify and when comparing the return value of crypt() against the input.

The PHP Hack exists since the first version of PHP’s own crypt_blowfish implementation that was added in 1e820ec.

No clear reason is given for the PHP Hack’s existence. This commit removes it, because BCrypt hashes containing a $ character in their salt are not valid BCrypt hashes.

Оставить комментарий

Анекдот на Пайтоне

У меня есть друг, которому я регулярно шлю шутки про «Вархаммер». Сам в этой теме не разбираюсь, но хочется думать, что его они веселят. А я — тот самый друг, которому шлют то же, но на тему программирования. Пару раз даже присылали мои собственные шутеечки.

Вчера прислали известный неприличный анекдот про Василия Ивановича и Петьку, но написанный на «Пайтоне». И хотя у меня довольно пуританский блог, анекдот так вырвиглазно написан, что пришлось его переписать.

Вот что мне прислали:

class Chapaev:
    nyuans = True
    def fuck(Class):
        Class.nyuans = True
        Class.ass.append('Dick')

class Petyka:
    nyuans = False
    ass = []

if Chapaev.nyuans == True and Petyka.nyuans == False:
    Chapaev.fuck(Petyka)

Как по мне, плохо написано примерно всё, мне очень не понравилось, так что держите, вот вам моя версия, которая, кстати, как мне кажется, более точно отражает суть анекдота:

Chapaev = []
Petka   = []
dick    = [20, Chapaev]

Petka.insert(*dick)

isNuance = Chapaev in Petka and Petka not in Chapaev
print(isNuance) // True

Ну и пока у меня ещё был запал, переписал на ПХП известнейший мем из фильма «ДМБ»:

class Gopher implements Countable
{
    public function count(): int
    {
        return 0;
    }
}

$gopher = new Gopher;
var_dump(count($gopher)); // 0
var_dump(empty($gopher)); // false

2 комментария

AVIF в GD

Вчера ночью чах, в очередной раз, над форматом AVIF — мысль была в качестве эксперимента добавить его в наш документооборот — продукт, разработкой которого я управляю.

Сейчас, чтобы показать документ в браузере, мы его конвертируем в JPEG — дёшево, сердито и везде поддерживается. Но страницы получаются большими (200 с лишком килобайт каждая) и иногда грузятся дольше, чем хотелось бы. Вот я и придумал проверить гипотезу, что AVIF тут поможет.

Пока примотал реализацию на соплях, как концепт. Результаты, кстати, хорошие, можно посмотреть на скриншоте разницу:

Список файлов с расширениями .jpg и .jpg.avif, правами доступа, размером, датой и временем изменения; имена файлов выделены розовым.
Разница в размере файлов в двух форматах — JPEG и AVIF

Но пока мотал, наткнулся на прекрасное. 🤦🏻‍♂️

В ПХП есть несколько модулей для работы с картинками, но самых распространённых два — GD и Imagick. Я начал с GD, так как на виртуалке он собран с поддержкой AVIF (а Imagick — нет), по крайней мере сам модуль так утверждает:

var_dump(gd_info()['AVIF Support']); // true
var_dump(imagetypes() & IMG_AVIF); // 256

Так же существует функция imageavif, что хороший знак — модуль устроен так, что если поддержки какого-то формата нет, то функция просто отсутствует.

Но при попытке сконвертировать картинку из JPEG в AVIF создавался файл нулевой длины, хотя функция возвращала true:

var_dump(imageavif(imagecreatefromjpeg('0.jpg'), '0.jpg.avif')); // true

GD это очень старый модуль, он родился, когда в PHP главенствовал процедурный модуль, поэтому его суть — функции; в PHP, если мы вызываем функции и что-то идёт не так, мы можем узнать что именно при помощи функции error_get_last. Помогла она и на этот раз:

array(4) {
  ["type"]=>
  int(2)
  ["message"]=>
  string(50) "imageavif(): AVIF image support has been disabled
"
  ["file"]=>
  string(17) "Command line code"
  ["line"]=>
  int(1)
}

Я погуглил ошибку, но не смог быстро разобраться что с ней сделать, поэтому переключился на Imagick. Там всё завелось без особых проблем — заменил шестую версию на седьмую и поставил недостающие пакеты:

yum shell
> remove php-pecl-imagick-im6
> install php-pecl-imagick-im7
> install ImageMagick7-heic
> install libheif
> run

В этом модуле, кстати, проверка на поддержку AVIF делается вот таким немного неочевидным способом:

$isAVIFSupported = Imagick::queryformats("AVIF") !== [];

2 комментария

Определение ширины и высоты файла JPEG

В «Эгее» есть какой-то плавающий баг, из-за которого иногда не заливаются файлы JPEG. Илья, автор «Эгеи», пытался с ним разобраться, но до конца понять проблему не смог.

Пока рабочая теория такая, что функция ПХП getimagesize (она возвращает тип изображения и его высоту с шириной) иногда выдаёт неправильные значения.

Опровергнуть или подтвердить эту теорию не так-то и просто — баг проявляется очень редко, но можно попытаться считывать размеры каким-то другим способом и посмотреть исчезнет проблема или нет.

Вот Илья и попросил написать меня такой код на чистом ПХП. Надеюсь это позволит продвинуться в изучении проблемы — код вошёл в одиннадцатую версию «Эгеи».

Выложу его у себя тоже, чтобы не потерялся, ну и может кому-то ещё пригодится:

function readJPEGWxH(string $filename): array
{
    $file = new SplFileObject($filename);

    if (!$file->flock(LOCK_SH)) {
        throw new RuntimeException('Cannot lock the file');
    }

    if ($file->fread(2) !== "\xFF\xD8") {
        throw new RuntimeException('Unknown format');
    }

    for (;;) {
        $decoded = @unpack('H4segment/nlen', $file->fread(4));

        if ($decoded === false) {
            throw new RuntimeException('Unknown format');
        }

        ['segment' => $segment, 'len' => $len] = $decoded;

        // SOFn (Start Of Frame)?
        if (preg_match('/^ffc[0-3]/', $segment)) {
            $segdata = @unpack('Cbits/nheight/nwidth', $file->fread(5));

            if ($segdata === false) {
                throw new RuntimeException('Unknown format');
            }

            return [$segdata['width'], $segdata['height']];
        }

        // len = full lenght of segment except segment magic code
        if ($file->fseek($len - 2, SEEK_CUR) !== 0) {
            throw new RuntimeException('Cannot find start of frame');
        }
    }
}

5 комментариев

Десятичная запятая

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

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

Так и не вспомнил, зато на память пришёл забавный баг, который был когда-то в четвёртом ПХП: если установить русскую локаль, появлялась возможность складывать числа в строках, используя десятичную запятую:

<?php
var_dump('0,4' + '0,7');
setlocale(LC_ALL, 'ru_RU.koi8r');
var_dump('0,4' + '0,7');

Как можно убедиться онлайн, результат будет следующим:

int(0)
float(1,1)

Работает в версиях 4.0—4.2, в 4.3 уже не повторяется. Я не помню каким образом на него наткнулся, но чувство недоумения от этого эффекта до сих пор живо в моей памяти.

2 комментария

Неблокирующий режим файлов в PHP

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

Смотрел недавно исходники языка ПХП, хотел изучить подробнее как реализованы там стримы. Пока читал, случайно натолкнулся на недокументированную возможность — у функции fopen, как оказалось, есть режим открытия в неблокирующем режиме (буква n).

#if defined(O_NONBLOCK)
        if (strchr(mode, 'n')) {
                flags |= O_NONBLOCK;
        }
#endif

Доступно не на всех системах, только там, где есть константа O_NONBLOCK в fcntl.h, то есть в Линуксах, например.

Для меня это довольно полезное знание, так как я давно ищу возможность проверить из ПХП не зависла ли подмонтированная через NFS система хранения. Из-за таких зависаний виснет драйвер в ядре и все системные вызовы, которые к нему обращаются. В конечном счёте виснет процесс, который пытается работать с такой файловой системой, да так, что его не получается убить через SIGKILL.

В общем, надо будет попробовать при следующем таком инциденте — не спасает ли открытие в неблокирующем режиме. Код, кажется, должен выглядеть примерно так:

function tryToOpen(string $path, float $timeout): ?bool
{
	$dio_fd = dio_open($path, O_RDONLY | O_NONBLOCK);
	if ($dio_fd === false) {
		return null;
	}

	foreach (glob('/proc/self/fd/*') as $file) {
	    if (is_link($file) && readlink($file) === $path) {
		$fd = basename($file);
		$read = [fopen("php://fd/$fd", 'rn')];
		$write = $except = null;

		$timeout_us = (int) ($timeout * 1e6);
		return stream_select($read, $write, $except, 0, $timeout_us) === 1;
             }
	}
	return null;
}

Тут используется модуль ПХП Direct IO, чтобы обратиться к системной функции open мимо ПХП. Приходится так делать, потому что функция fopen, которая есть в языке, сначала попытается проверить существование файла и в моей задаче сразу зависнет.

После этого я, пользуясь /proc, ищу среди своих файловых дескрипторов тот, который отвечает за только что открытый файл и переоткрываю его через fopen (в этом месте делается дубликат файлового дескриптора, а прежний закрывается — вызывается dup), так как мы тут работаем с номером файлового дескриптора, то проверка на существование файла не используется.

Переоткрытие файла нужно, так как в вызов stream_select, который я использую для ожидания данных из файла с таймаутом, можно передать результат fopen, но не dio_open.

Надеюсь случай испытать этот код представится нескоро.

2 комментария

PHPFuck для PHP8

Немного про программирование.

Один из читателей написал, что программа PHPFuck (это способ писать программы без алфавитно-цифровых символов), которую я писал несколько лет назад, не работает с восьмой версией интерпретатора ПХП. Он попробовал починить её самостоятельно, но не преуспел.

В самом деле, восьмая версия стала куда строже к программисту, из-за чего починить написанное очень тяжело, проще написать всё заново с учётом новых ограничений.

<?=[[$_=@([].[]),$___=-!![],$$_=![]+![],$$_=$$_*$$_,$__=$$_+![],$$__=$$_+$__,
$____=$_[-![]-![]],$$___="{$$__}"^$____,$$___=($$___^"$___"). ($$___^"{$$__}"^"$___"^"{$$_}").
($$$_=$$___^"{$$_}"). $$$_.($_____="{$$__}"^"{$$_}"^++$____).("$___"&$____).
([$$___=$_[+![]],"{$$_}"|++$$___][+![]]).$_____.$_[+![]].$$$_. ("$__"^$_[-![]-![]]^"$___")
],$$___][+![]];

Выше новая версия.

Кстати, она компактнее, но при этом выводит больше текста — «hello world», вместо «Hello!» предыдущего варианта. Ниже я немного расскажу откуда взялась экономия и с чем ещё пришлось столкнуться.

Принцип формирования текста остался, в общих чертах, прежним — корнем всего является комбинация @([].[]), которая даёт слово ArrayArray из-за неявного преобразования массива в текст — это такая особенность ПХП (символ @ нужен тут, чтобы подавить предупреждение, которое выдаёт интерпретатор).

Из этой строки потом рождаются все остальные символы. Дело в том, что в ПХП над символами можно производить некоторые бинарные и математические операции, что иногда позволяет получить другие символы. Например, первая буква — h в слове hello получается из сочетания '9' ^ 'a' ^ '0' (где ^ — бинарная операция «исключающего или»).

При этом цифры я получаю довольно простым способом — 0 как +!![] (преобразованием false в число), «девятку»,  — набирая нужные цифры по единицам (из +![]), а a — как предпоследнюю букву из полученной ранее строки ArrayArray, то есть @([].[])[-2] (аналогично, -2 получается тут из двух замаскированных единиц со знаком «минус»).

По сравнению с предыдущим вариантом в новом коде сделаны следующие изменения.

Во-первых, вместо того, чтобы составлять из получаемых букв printf (функцию, которую я использовал в прошлый раз для вывода текста), тут я пользуюсь выводом через короткую конструкцию <?= — в этом основная экономия. Я выбрал этот вариант, так как конструкция <?, которую я использовал ранее, больше не работает, а заменившая её <?php содержит буквы.

В этой связи весь код теперь — большое выражение, все переменные в нём лежат внутри массива. Если давать им нормальные имена, то основной принцип выглядит как-то так: <?=[ [ $v1 = …, $v2 = …, $result = $v1 . $v2 . … ], $result ][1]; То есть они вычисляются внутри массива, последний элемент которого, содержащий вычисленное выражение, выводится.

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

Кроме того, мне надоело подбирать буквы вручную, вместо этого я написал небольшую программу, которая делает это за меня. Я не стал придумывать алгоритм, который бы анализировал какие биты нужно изменить, а решил задачу полным перебором.

Ниже приведен листинг этой программы, вдруг кому-то интересно:

<?php
declare(strict_types=1);

// Доступный алфавит
$abc = str_split(count_chars("Array0123456789", 3));

// буквы, которые надо получить
$need = str_split(count_chars("hello world", 3));

function gen(array $chrs): Generator
{
    // чем операции длиннее, тем ближе к концу
    // это нужно, чтобы более короткие конструкции выигрывали
    foreach ($chrs as $ch) {
        yield $ch => [$ch, $ch];
    }

    // возврат: из чего делаем, [как выглядит, значение]
    foreach ($chrs as $ch) {
        yield $ch => ["~$ch", ~$ch];
    }

    // для символа инкремент работает, а декремент — нет (особенность ПХП)
    foreach ($chrs as $ch) {
        $value = $ch;
        yield $ch => ["++$ch", (string)++$value];
    }
}

function eval_op(string $op, string $a, string $b): string
{
    return ([
        '|' => fn($a, $b) => $a | $b,
        '&' => fn($a, $b) => $a & $b,
        '^' => fn($a, $b) => $a ^ $b,
    ][$op])($a, $b);
}

function generate(array $set): Generator
{
    foreach (gen($set) as $src1 => [$p1, $ch1]) {
        foreach (gen($set) as $src2 => [$p2, $ch2]) {
            foreach (['|', '&', '^'] as $op) {
                $result = eval_op($op, $ch1, $ch2);

                if ($result !== $ch1 && $result !== $ch2) {
                    yield [
                        $src1,
                        $src2,
                        "$p1 $op $p2",
                        $result,
                    ];
                }
            }
        }
    }
}

function out(string $str): string
{
    return preg_replace_callback(
        '/[\x00-\x1F\x80-]/s',
        fn($m) => sprintf("\\x%02X", ord($m[0])),
        $str
    );
}

$set = $abc;

do {
    foreach (generate($set) as [$src1, $src2, $str, $res]) {
        $idx = array_search($res, $need, true);

        if (!isset($rules[$res])) {
            $rules[$res] = $str;
        }

        if ($idx !== false) {
            if (!in_array($src1, $abc, true)) {
                echo out("{$rules[$src1]} = '$src1'"), "\n";
            }

            if (!in_array($src2, $abc, true)) {
                echo out("{$rules[$src2]} = '$src2'"), "\n";
            }

            echo out("$str = '$res'"), "\n\n";

            unset($need[$idx]);
        }

        $set[] = $res;
    }

    $set = array_unique($set);
} while ($need);

Это ни в коем случае не инструмент для оптимизации программ в такой технике, а набросок, позволяющий ускорить подбор букв.

Для более глубоких оптимизаций нужно научить эту программу дробить выражения, которые уже вычислялись, на куски и эффективно использовать их, а так же подбирать частоиспользуемым кускам имена переменных меньшей длины.

У меня дробления нет, я использую предыдущие выражения целиком, а имена переменным моя программа вообще не подбирает.

2 комментария

Длинные имена и PHP

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

Оказалось, что наша версия ПХП использует устаревший вызов readdir_r. У него много недостатков, но среди прочих — на некоторых системах он не может читать длинные имена. Проблему исправили в версии 7.4, но у нас всё ещё есть проекты на 7.3.

До конца перехода будем использовать системную утилиту /bin/ls, у неё таких трудностей не возникает.

Оставить комментарий

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

12 комментариев

← 1 2 3 4 5 6 12 →