[[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); } }