propresenter-php/php/src/Zip64Fixer.php
Thorsten Bus 2c1b8e3370 feat(playlist): add proto field 5, Zip64Fixer, and format spec
- Add arrangement_name field 5 to PlaylistItem.Presentation proto
- Regenerate PHP proto classes with new field
- Implement Zip64Fixer utility to patch ProPresenter's broken ZIP headers
- Add comprehensive test suite for Zip64Fixer (7 tests, 37 assertions)
- Create pp_playlist_spec.md documenting .proplaylist file format

Wave 1 of proplaylist-module plan complete (Tasks 1-3)
2026-03-01 20:50:14 +01:00

138 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class Zip64Fixer
{
private const EOCD_SIGNATURE = "\x50\x4b\x05\x06";
private const ZIP64_EOCD_SIGNATURE = "\x50\x4b\x06\x06";
private const ZIP64_LOCATOR_SIGNATURE = "\x50\x4b\x06\x07";
public static function fix(string $zipData): string
{
if ($zipData === '') {
throw new InvalidArgumentException('ZIP data must not be empty.');
}
$length = strlen($zipData);
if ($length < 22) {
throw new RuntimeException('ZIP data is too small to contain EOCD.');
}
$eocdPosition = self::findLastSignature($zipData, self::EOCD_SIGNATURE);
if ($eocdPosition < 0) {
throw new RuntimeException('EOCD signature not found in ZIP data.');
}
if ($eocdPosition + 22 > $length) {
throw new RuntimeException('EOCD record is truncated.');
}
$locatorPosition = self::findLastSignatureBefore($zipData, self::ZIP64_LOCATOR_SIGNATURE, $eocdPosition);
if ($locatorPosition < 0) {
$cdOffset = self::readUInt32LE($zipData, $eocdPosition + 16);
if ($cdOffset === 0xFFFFFFFF) {
throw new RuntimeException('ZIP64 EOCD locator not found.');
}
return $zipData;
}
if ($locatorPosition + 20 > $length) {
throw new RuntimeException('ZIP64 EOCD locator is truncated.');
}
$zip64EocdPosition = self::readUInt64LE($zipData, $locatorPosition + 8);
if ($zip64EocdPosition < 0 || $zip64EocdPosition + 56 > $length) {
throw new RuntimeException('ZIP64 EOCD position is out of bounds.');
}
if (substr($zipData, $zip64EocdPosition, 4) !== self::ZIP64_EOCD_SIGNATURE) {
throw new RuntimeException('ZIP64 EOCD signature not found at locator position.');
}
$zip64CdOffset = self::readUInt64LE($zipData, $zip64EocdPosition + 48);
$correctCdSize = $zip64EocdPosition - $zip64CdOffset;
if ($correctCdSize < 0) {
throw new RuntimeException('Computed central directory size is invalid.');
}
$zipData = substr_replace($zipData, self::writeUInt64LE($correctCdSize), $zip64EocdPosition + 40, 8);
$regularCdSize = $correctCdSize > 0xFFFFFFFF ? 0xFFFFFFFF : (int) $correctCdSize;
return substr_replace($zipData, pack('V', $regularCdSize), $eocdPosition + 12, 4);
}
private static function findLastSignature(string $data, string $signature): int
{
$position = strrpos($data, $signature);
if ($position === false) {
return -1;
}
return $position;
}
private static function findLastSignatureBefore(string $data, string $signature, int $before): int
{
if ($before <= 0) {
return -1;
}
$slice = substr($data, 0, $before);
$position = strrpos($slice, $signature);
if ($position === false) {
return -1;
}
return $position;
}
private static function readUInt32LE(string $data, int $offset): int
{
$chunk = substr($data, $offset, 4);
if (strlen($chunk) !== 4) {
throw new RuntimeException('Unable to read 32-bit little-endian integer.');
}
$value = unpack('V', $chunk);
if ($value === false) {
throw new RuntimeException('Unable to unpack 32-bit little-endian integer.');
}
return (int) $value[1];
}
private static function readUInt64LE(string $data, int $offset): int
{
$chunk = substr($data, $offset, 8);
if (strlen($chunk) !== 8) {
throw new RuntimeException('Unable to read 64-bit little-endian integer.');
}
$parts = unpack('Vlow/Vhigh', $chunk);
if ($parts === false) {
throw new RuntimeException('Unable to unpack 64-bit little-endian integer.');
}
return (int) ($parts['high'] * 4294967296 + $parts['low']);
}
private static function writeUInt64LE(int $value): string
{
if ($value < 0) {
throw new RuntimeException('Unable to encode negative 64-bit value.');
}
$low = $value & 0xFFFFFFFF;
$high = intdiv($value, 4294967296);
return pack('V2', $low, $high);
}
}