propresenter-php/tests/Zip64FixerTest.php
Thorsten Bus 22ba4aff7d refactor: make repo Composer-compatible by moving php/ to root and ref/ to doc/reference_samples
- 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
2026-03-30 13:26:29 +02:00

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