tmpDir = sys_get_temp_dir() . '/irdb-dbip-' . bin2hex(random_bytes(6)); mkdir($this->tmpDir); } protected function tearDown(): void { if ($this->tmpDir !== '' && is_dir($this->tmpDir)) { foreach (glob($this->tmpDir . '/*') ?: [] as $f) { @unlink($f); } @rmdir($this->tmpDir); } } public function testNormalGunzipPasses(): void { $gzPath = $this->writeGzip('hello world'); $outPath = $this->tmpDir . '/out.bin'; DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024); self::assertFileExists($outPath); self::assertSame('hello world', file_get_contents($outPath)); // gz file is removed on success. self::assertFileDoesNotExist($gzPath); } public function testOutputOverCapIsRejectedAndCleanedUp(): void { // 4 KiB of plaintext, cap at 1 KiB. $gzPath = $this->writeGzip(str_repeat('A', 4096)); $outPath = $this->tmpDir . '/out.bin'; $threw = false; try { DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024); } catch (DownloaderException $e) { $threw = true; self::assertStringContainsString('exceeds cap 1024 bytes', $e->getMessage()); } self::assertTrue($threw, 'expected DownloaderException for over-cap output'); // Partial output must NOT be left on disk — caller treats // existence of `$outPath` as "verified ready to swap". self::assertFileDoesNotExist($outPath); // gz input was NOT cleaned up (caller decides whether to retry). self::assertFileExists($gzPath); } public function testEmptyGzipIsRejected(): void { // gzip of an empty string produces a valid gzip with 0 bytes // payload — the verifier downstream needs SOMETHING; an empty // file is suspicious enough to bail on. $gzPath = $this->writeGzip(''); $outPath = $this->tmpDir . '/out.bin'; $threw = false; try { DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024); } catch (DownloaderException $e) { $threw = true; self::assertStringContainsString('empty output', $e->getMessage()); } self::assertTrue($threw); self::assertFileDoesNotExist($outPath); } public function testMissingInputIsRejected(): void { $this->expectException(DownloaderException::class); DbipDownloader::gunzipWithCap( $this->tmpDir . '/does-not-exist.gz', $this->tmpDir . '/out.bin', maxBytes: 1024, ); } /** * Streams >chunk-size of plaintext through the gunzip helper to * prove the chunked read loop accumulates correctly. */ public function testLargeInputStreamsCorrectly(): void { // 256 KiB of distinguishable bytes (4 × 64 KiB chunks). $payload = str_repeat('abcd', 256 * 1024 / 4); self::assertSame(256 * 1024, strlen($payload)); $gzPath = $this->writeGzip($payload); $outPath = $this->tmpDir . '/big.bin'; DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 512 * 1024); self::assertFileExists($outPath); self::assertSame($payload, file_get_contents($outPath)); } private function writeGzip(string $plain): string { $path = $this->tmpDir . '/' . bin2hex(random_bytes(4)) . '.gz'; $compressed = gzencode($plain, 6); self::assertNotFalse($compressed); file_put_contents($path, $compressed); return $path; } }