- Move src/, tests/, bin/, generated/, proto/, composer.json, composer.lock, phpunit.xml from php/ to repo root - Move ref/ to doc/reference_samples/ for better organization - Remove vendor/ from git tracking (now properly gitignored) - Update all test file paths (dirname adjustments and ref/ -> doc/reference_samples/) - Update all documentation paths (AGENTS.md, doc/*.md) - Remove php.bak/ directory - All 252 tests pass
199 lines
5.6 KiB
PHP
199 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ProPresenter\Parser\Tests;
|
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ProPresenter\Parser\Zip64Fixer;
|
|
use RuntimeException;
|
|
use ZipArchive;
|
|
|
|
class Zip64FixerTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function fixReturnsValidZipForProPresenterExport(): void
|
|
{
|
|
$data = $this->readFixture('doc/reference_samples/TestPlaylist.proplaylist');
|
|
$fixed = Zip64Fixer::fix($data);
|
|
|
|
$zip = $this->openZipFromBytes($fixed);
|
|
$this->assertGreaterThan(0, $zip->numFiles);
|
|
$zip->close();
|
|
}
|
|
|
|
#[Test]
|
|
public function fixReturnsValidZipForLargePlaylist(): void
|
|
{
|
|
$data = $this->readFixture('doc/reference_samples/ExamplePlaylists/Gottesdienst.proplaylist');
|
|
$fixed = Zip64Fixer::fix($data);
|
|
|
|
$zip = $this->openZipFromBytes($fixed);
|
|
$this->assertGreaterThan(0, $zip->numFiles);
|
|
$zip->close();
|
|
}
|
|
|
|
#[Test]
|
|
public function fixReturnsValidZipForAllRequiredGottesdienstFixtures(): void
|
|
{
|
|
$files = [
|
|
'doc/reference_samples/ExamplePlaylists/Gottesdienst.proplaylist',
|
|
'doc/reference_samples/ExamplePlaylists/Gottesdienst 2.proplaylist',
|
|
'doc/reference_samples/ExamplePlaylists/Gottesdienst 3.proplaylist',
|
|
];
|
|
|
|
foreach ($files as $fixture) {
|
|
$fixed = Zip64Fixer::fix($this->readFixture($fixture));
|
|
$zip = $this->openZipFromBytes($fixed);
|
|
$this->assertGreaterThan(0, $zip->numFiles, $fixture);
|
|
$zip->close();
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function fixThrowsOnNonZipData(): void
|
|
{
|
|
$this->expectException(RuntimeException::class);
|
|
Zip64Fixer::fix(random_bytes(256));
|
|
}
|
|
|
|
#[Test]
|
|
public function fixThrowsOnTooSmallData(): void
|
|
{
|
|
$this->expectException(RuntimeException::class);
|
|
Zip64Fixer::fix(str_repeat('x', 10));
|
|
}
|
|
|
|
#[Test]
|
|
public function fixPreservesAllEntries(): void
|
|
{
|
|
$raw = $this->readFixture('doc/reference_samples/TestPlaylist.proplaylist');
|
|
$fixed = Zip64Fixer::fix($raw);
|
|
|
|
$expectedEntries = $this->listEntriesWithUnzip($fixed);
|
|
$actualEntries = $this->listEntriesWithZipArchive($fixed);
|
|
|
|
$expectedEntries = array_map([$this, 'canonicalizeEntryName'], $expectedEntries);
|
|
$actualEntries = array_map([$this, 'canonicalizeEntryName'], $actualEntries);
|
|
|
|
sort($expectedEntries, SORT_STRING);
|
|
sort($actualEntries, SORT_STRING);
|
|
|
|
$this->assertSame($expectedEntries, $actualEntries);
|
|
}
|
|
|
|
#[Test]
|
|
public function fixIdempotent(): void
|
|
{
|
|
$raw = $this->readFixture('doc/reference_samples/TestPlaylist.proplaylist');
|
|
$once = Zip64Fixer::fix($raw);
|
|
$twice = Zip64Fixer::fix($once);
|
|
|
|
$this->assertSame($once, $twice);
|
|
}
|
|
|
|
private function readFixture(string $relativePath): string
|
|
{
|
|
$path = dirname(__DIR__) . '/' . $relativePath;
|
|
$data = file_get_contents($path);
|
|
$this->assertNotFalse($data, sprintf('Failed to read fixture: %s', $relativePath));
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function listEntriesWithUnzip(string $zipData): array
|
|
{
|
|
$path = $this->writeTempZip($zipData);
|
|
|
|
try {
|
|
$output = shell_exec(sprintf('unzip -l %s 2>&1', escapeshellarg($path)));
|
|
$this->assertNotNull($output, 'Failed to execute unzip');
|
|
|
|
$entries = [];
|
|
foreach (explode("\n", $output) as $line) {
|
|
$trimmed = trim($line);
|
|
if ($trimmed === '') {
|
|
continue;
|
|
}
|
|
|
|
$parts = preg_split('/\s+/', $trimmed, 4);
|
|
if ($parts === false || count($parts) !== 4) {
|
|
continue;
|
|
}
|
|
|
|
if (!ctype_digit($parts[0])) {
|
|
continue;
|
|
}
|
|
|
|
if (!preg_match('/^\d{2}-\d{2}-\d{4}$/', $parts[1])) {
|
|
continue;
|
|
}
|
|
|
|
if (!preg_match('/^\d{2}:\d{2}$/', $parts[2])) {
|
|
continue;
|
|
}
|
|
|
|
$entries[] = $parts[3];
|
|
}
|
|
|
|
return array_values($entries);
|
|
} finally {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
|
|
private function listEntriesWithZipArchive(string $zipData): array
|
|
{
|
|
$zip = $this->openZipFromBytes($zipData);
|
|
$entries = [];
|
|
|
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
|
$name = $zip->getNameIndex($i);
|
|
if ($name !== false) {
|
|
$entries[] = $name;
|
|
}
|
|
}
|
|
|
|
$zip->close();
|
|
|
|
return $entries;
|
|
}
|
|
|
|
private function openZipFromBytes(string $zipData): ZipArchive
|
|
{
|
|
$path = $this->writeTempZip($zipData);
|
|
$zip = new ZipArchive();
|
|
$status = $zip->open($path);
|
|
@unlink($path);
|
|
|
|
$this->assertTrue($status === true, sprintf('ZipArchive open failed with status: %s', (string) $status));
|
|
|
|
return $zip;
|
|
}
|
|
|
|
private function writeTempZip(string $zipData): string
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'zip64-fixer-');
|
|
$this->assertNotFalse($path);
|
|
|
|
$result = file_put_contents($path, $zipData);
|
|
$this->assertNotFalse($result);
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function canonicalizeEntryName(string $entry): string
|
|
{
|
|
$ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $entry);
|
|
if ($ascii === false) {
|
|
$ascii = preg_replace('/[^\x20-\x7E]/', '', $entry);
|
|
if ($ascii === null) {
|
|
return $entry;
|
|
}
|
|
}
|
|
|
|
return str_replace('?', '', $ascii);
|
|
}
|
|
}
|