Files
preland/bin/check-runtime-sets.php
2026-10-25 11:33:56 +03:00

117 lines
6.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* preland#22: проверка runtime indpic без сравнения с Git.
* Контракт имён/размеров: api 591a67cc8302 components/indpic/IndpicSetStorage.php.
* HTTP проверяет все обязательные файлы; только локальный режим может исключить лишние.
*/
declare(strict_types=1);
function runtimeSetDimensions(): array
{
$files = [];
for ($example = 1; $example <= 5; $example++) {
foreach (['desk' => [[1696, 920], [200, 200]], 'mob' => [[400, 217], [60, 60]]] as $device => [$full, $thumb]) {
foreach (['1-source' => $full, '2-result' => $full, '3-thumb' => $thumb] as $part => $dimensions) {
foreach (['avif', 'webp', 'jpg'] as $extension) $files[sprintf('%02d_%s_%s.%s', $example, $device, $part, $extension)] = $dimensions;
}
}
}
return $files;
}
function runtimeSetNumbers(array $config): array
{
$settings = $config['utm']['indpic'] ?? ['default' => '1', 'weights' => ['1' => 1]];
if (!is_array($settings)) throw new RuntimeException('Некорректный config utm.indpic');
$number = static function ($value): int {
if ((!is_string($value) && !is_int($value)) || !preg_match('/^[1-9]$/D', (string) $value)) throw new RuntimeException('Номер indpic должен быть 1–9');
return (int) $value;
};
$sets = [$number(array_key_exists('default', $settings) ? $settings['default'] : '1')];
$weights = $settings['weights'] ?? [];
if (!is_array($weights)) throw new RuntimeException('Некорректные веса indpic');
foreach ($weights as $variant => $weight) {
if ((!is_int($weight) && !is_float($weight)) || !is_finite((float) $weight) || $weight < 0) throw new RuntimeException('Некорректный вес indpic');
// Даже weight=0 доступен по явной ссылке indpic=abcdN и участвует в legacy random.
$sets[] = $number($variant);
}
$sets = array_values(array_unique($sets)); sort($sets); return $sets;
}
function runtimeLocalReader(string $root): Closure
{
return static function (string $path) use ($root): string {
$bytes = @file_get_contents($root . '/' . $path);
if ($bytes === false) throw new RuntimeException('Не прочитан runtime ресурс ' . $path);
return $bytes;
};
}
function runtimeHttpReader(string $origin): Closure
{
if (!preg_match('~^https://[A-Za-z0-9.-]+$~D', $origin)) throw new RuntimeException('Требуется HTTPS origin');
return static function (string $path) use ($origin): string {
$context = stream_context_create(['http' => ['timeout' => 30, 'follow_location' => 0, 'header' => "Cache-Control: no-cache\r\n"], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);
$bytes = @file_get_contents($origin . '/' . $path . '?runtime_check=' . bin2hex(random_bytes(8)), false, $context, 0, 20971521);
if ($bytes === false || strlen($bytes) > 20971520 || !preg_match('~^HTTP/\S+ 200(?: |$)~', $http_response_header[0] ?? '')) throw new RuntimeException('Не прочитан runtime ресурс ' . $path);
return $bytes;
};
}
function checkRuntimeSets(array $config, callable $read, ?string $localRoot = null): array
{
if (!class_exists(Imagick::class)) throw new RuntimeException('Для полной проверки indpic требуется Imagick');
$sets = runtimeSetNumbers($config);
$expected = runtimeSetDimensions();
foreach ($sets as $set) {
if ($localRoot !== null) {
$directory = $localRoot . '/' . $set;
if (is_link($directory) || !is_dir($directory)) throw new RuntimeException('Не найден каталог indpic ' . $set);
$actual = [];
foreach (new FilesystemIterator($directory, FilesystemIterator::SKIP_DOTS) as $file) {
if ($file->isLink() || !$file->isFile()) throw new RuntimeException('Лишний элемент indpic ' . $set);
$actual[] = $file->getFilename();
}
$names = array_keys($expected); sort($names); sort($actual);
if ($actual !== $names) throw new RuntimeException('Набор indpic ' . $set . ' должен содержать ровно 90 обязательных файлов');
}
foreach ($expected as $name => [$width, $height]) {
$path = $set . '/' . $name;
$image = new Imagick();
try {
$image->readImageBlob($read($path));
$format = strtolower($image->getImageFormat());
$extension = pathinfo($name, PATHINFO_EXTENSION);
$formats = ['jpg' => ['jpeg', 'jpg'], 'webp' => ['webp'], 'avif' => ['avif', 'heic']];
if (!in_array($format, $formats[$extension], true) || $image->getNumberImages() !== 1 || $image->getImageWidth() !== $width || $image->getImageHeight() !== $height) throw new RuntimeException('Неверный формат/размер изображения');
} catch (Throwable $error) {
throw new RuntimeException('Невалидный runtime indpic ' . $path . ': ' . $error->getMessage(), 0, $error);
} finally { $image->clear(); }
}
}
return $sets;
}
function verifyRemoteRuntimeSets(string $origin): void
{
$read = runtimeHttpReader($origin);
$bytes = $read('config.json');
$config = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($config)) throw new RuntimeException('Некорректный runtime config');
$sets = checkRuntimeSets($config, $read);
if ($read('config.json') !== $bytes) throw new RuntimeException('Runtime config изменился во время проверки; повторите проверку');
fwrite(STDERR, 'OK: runtime indpic ' . implode(',', $sets) . '; все обязательные изображения декодированы, inventory лишних файлов по HTTP недоступен' . "\n");
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
try {
if (($argv[1] ?? '') === '--remote') verifyRemoteRuntimeSets($argv[2] ?? '');
else {
$root = $argv[1] ?? dirname(__DIR__);
$config = json_decode(runtimeLocalReader($root)('config.json'), true, 512, JSON_THROW_ON_ERROR);
checkRuntimeSets($config, runtimeLocalReader($root), $root);
echo "OK: локальный runtime indpic, ровно 90 файлов в каждом используемом наборе\n";
}
} catch (Throwable $error) { fwrite(STDERR, $error->getMessage() . "\n"); exit(1); }
}