| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Enrichment;
- use App\Infrastructure\Enrichment\Downloaders\DownloaderException;
- use App\Infrastructure\Enrichment\Downloaders\MaxMindDownloader;
- use PharData;
- use PHPUnit\Framework\TestCase;
- /**
- * SEC_REVIEW F48 — `MaxMindDownloader::assertSizeBudget` walks the
- * tarball before extraction and rejects per-entry / total-size limit
- * breaches so a small `.tar.gz` cannot decompress into multi-GB and
- * exhaust disk before MmdbVerifier sees the file.
- */
- final class MaxMindDownloaderTest extends TestCase
- {
- private string $tmpDir = '';
- protected function setUp(): void
- {
- $this->tmpDir = sys_get_temp_dir() . '/irdb-maxmind-' . bin2hex(random_bytes(6));
- mkdir($this->tmpDir);
- }
- protected function tearDown(): void
- {
- // Clean up the tar file and any generated companions.
- if ($this->tmpDir !== '' && is_dir($this->tmpDir)) {
- foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
- @unlink($f);
- }
- @rmdir($this->tmpDir);
- }
- }
- public function testNormalArchivePasses(): void
- {
- $phar = $this->buildTar([
- 'GeoLite2-Country.mmdb' => 'pretend-mmdb-bytes',
- 'COPYRIGHT.txt' => 'CC BY-SA 4.0',
- ]);
- // Default caps (200 MiB / 400 MiB) — small fixtures pass cleanly.
- MaxMindDownloader::assertSizeBudget($phar, 'country');
- $this->expectNotToPerformAssertions();
- }
- public function testEntryOverPerEntryCapIsRejected(): void
- {
- $phar = $this->buildTar([
- 'big.bin' => str_repeat('A', 4096),
- ]);
- $this->expectException(DownloaderException::class);
- $this->expectExceptionMessage('uncompressed size 4096 > cap 1024');
- // Force the per-entry cap below the entry size.
- MaxMindDownloader::assertSizeBudget($phar, 'country', maxEntryBytes: 1024);
- }
- public function testTotalOverArchiveCapIsRejected(): void
- {
- // Three 1 KiB entries individually pass per-entry; the total
- // breaches the archive cap at the third entry.
- $phar = $this->buildTar([
- 'a.bin' => str_repeat('a', 1024),
- 'b.bin' => str_repeat('b', 1024),
- 'c.bin' => str_repeat('c', 1024),
- ]);
- $this->expectException(DownloaderException::class);
- $this->expectExceptionMessage('archive uncompressed size > cap 2048');
- MaxMindDownloader::assertSizeBudget(
- $phar,
- 'country',
- maxEntryBytes: 4096,
- maxTotalBytes: 2048,
- );
- }
- public function testNestedEntriesAreCounted(): void
- {
- // The real MaxMind tarball nests one directory like
- // `GeoLite2-Country_20260427/GeoLite2-Country.mmdb`. Recursive
- // iteration must walk into the directory and count nested
- // entries against both caps.
- $phar = $this->buildTar([
- 'GeoLite2-Country_20260427/GeoLite2-Country.mmdb' => str_repeat('m', 2048),
- 'GeoLite2-Country_20260427/COPYRIGHT.txt' => 'CC',
- ]);
- $this->expectException(DownloaderException::class);
- $this->expectExceptionMessage('GeoLite2-Country.mmdb uncompressed size 2048 > cap 1024');
- MaxMindDownloader::assertSizeBudget(
- $phar,
- 'country',
- maxEntryBytes: 1024,
- );
- }
- /**
- * @param array<string, string> $entries map of entry path → contents
- */
- private function buildTar(array $entries): PharData
- {
- $tarPath = $this->tmpDir . '/' . bin2hex(random_bytes(4)) . '.tar';
- $writer = new PharData($tarPath);
- foreach ($entries as $name => $contents) {
- $writer->addFromString($name, $contents);
- }
- // PharData iteration on the *write* handle returns nothing until
- // the file is closed and re-opened. Production builds the
- // PharData from an already-on-disk tarball, so this matches the
- // production path.
- unset($writer);
- return new PharData($tarPath);
- }
- }
|