1
0

MaxMindDownloaderTest.php 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Enrichment;
  4. use App\Infrastructure\Enrichment\Downloaders\DownloaderException;
  5. use App\Infrastructure\Enrichment\Downloaders\MaxMindDownloader;
  6. use PharData;
  7. use PHPUnit\Framework\TestCase;
  8. /**
  9. * SEC_REVIEW F48 — `MaxMindDownloader::assertSizeBudget` walks the
  10. * tarball before extraction and rejects per-entry / total-size limit
  11. * breaches so a small `.tar.gz` cannot decompress into multi-GB and
  12. * exhaust disk before MmdbVerifier sees the file.
  13. */
  14. final class MaxMindDownloaderTest extends TestCase
  15. {
  16. private string $tmpDir = '';
  17. protected function setUp(): void
  18. {
  19. $this->tmpDir = sys_get_temp_dir() . '/irdb-maxmind-' . bin2hex(random_bytes(6));
  20. mkdir($this->tmpDir);
  21. }
  22. protected function tearDown(): void
  23. {
  24. // Clean up the tar file and any generated companions.
  25. if ($this->tmpDir !== '' && is_dir($this->tmpDir)) {
  26. foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
  27. @unlink($f);
  28. }
  29. @rmdir($this->tmpDir);
  30. }
  31. }
  32. public function testNormalArchivePasses(): void
  33. {
  34. $phar = $this->buildTar([
  35. 'GeoLite2-Country.mmdb' => 'pretend-mmdb-bytes',
  36. 'COPYRIGHT.txt' => 'CC BY-SA 4.0',
  37. ]);
  38. // Default caps (200 MiB / 400 MiB) — small fixtures pass cleanly.
  39. MaxMindDownloader::assertSizeBudget($phar, 'country');
  40. $this->expectNotToPerformAssertions();
  41. }
  42. public function testEntryOverPerEntryCapIsRejected(): void
  43. {
  44. $phar = $this->buildTar([
  45. 'big.bin' => str_repeat('A', 4096),
  46. ]);
  47. $this->expectException(DownloaderException::class);
  48. $this->expectExceptionMessage('uncompressed size 4096 > cap 1024');
  49. // Force the per-entry cap below the entry size.
  50. MaxMindDownloader::assertSizeBudget($phar, 'country', maxEntryBytes: 1024);
  51. }
  52. public function testTotalOverArchiveCapIsRejected(): void
  53. {
  54. // Three 1 KiB entries individually pass per-entry; the total
  55. // breaches the archive cap at the third entry.
  56. $phar = $this->buildTar([
  57. 'a.bin' => str_repeat('a', 1024),
  58. 'b.bin' => str_repeat('b', 1024),
  59. 'c.bin' => str_repeat('c', 1024),
  60. ]);
  61. $this->expectException(DownloaderException::class);
  62. $this->expectExceptionMessage('archive uncompressed size > cap 2048');
  63. MaxMindDownloader::assertSizeBudget(
  64. $phar,
  65. 'country',
  66. maxEntryBytes: 4096,
  67. maxTotalBytes: 2048,
  68. );
  69. }
  70. public function testNestedEntriesAreCounted(): void
  71. {
  72. // The real MaxMind tarball nests one directory like
  73. // `GeoLite2-Country_20260427/GeoLite2-Country.mmdb`. Recursive
  74. // iteration must walk into the directory and count nested
  75. // entries against both caps.
  76. $phar = $this->buildTar([
  77. 'GeoLite2-Country_20260427/GeoLite2-Country.mmdb' => str_repeat('m', 2048),
  78. 'GeoLite2-Country_20260427/COPYRIGHT.txt' => 'CC',
  79. ]);
  80. $this->expectException(DownloaderException::class);
  81. $this->expectExceptionMessage('GeoLite2-Country.mmdb uncompressed size 2048 > cap 1024');
  82. MaxMindDownloader::assertSizeBudget(
  83. $phar,
  84. 'country',
  85. maxEntryBytes: 1024,
  86. );
  87. }
  88. /**
  89. * @param array<string, string> $entries map of entry path → contents
  90. */
  91. private function buildTar(array $entries): PharData
  92. {
  93. $tarPath = $this->tmpDir . '/' . bin2hex(random_bytes(4)) . '.tar';
  94. $writer = new PharData($tarPath);
  95. foreach ($entries as $name => $contents) {
  96. $writer->addFromString($name, $contents);
  97. }
  98. // PharData iteration on the *write* handle returns nothing until
  99. // the file is closed and re-opened. Production builds the
  100. // PharData from an already-on-disk tarball, so this matches the
  101. // production path.
  102. unset($writer);
  103. return new PharData($tarPath);
  104. }
  105. }