diff --git a/CHANGELOG.md b/CHANGELOG.md index 9982f6b..6e47a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,36 @@ CHANGELOG 1.15.0 ------------------- +* The pure PHP reader now rejects non-string map keys with + `InvalidDatabaseException` instead of a `TypeError`, a warning, or an + implicit type conversion. +* The pure PHP reader now rejects malformed metadata with + `InvalidDatabaseException` instead of a `TypeError`, a warning, or an + implicit type conversion. Missing optional `languages` and `description` + fields default to empty arrays. +* Missing gmp/bcmath support and offsets that exceed the platform limit now + throw `MaxMind\Db\Reader\UnsupportedPlatformException`. It extends + `RuntimeException`, so existing catches continue to work. +* The pure PHP reader also throws `UnsupportedPlatformException` when the + metadata node count or the start of the data section exceeds the platform's + integer limit. These cases no longer produce `InvalidDatabaseException` or + overflow into a `TypeError`. * `phpinfo()` and `php --ri maxminddb` now show whether the extension was built with the bundled libmaxminddb or links a system library. The `libmaxminddb library version` row reads, for example, `1.14.0 (bundled)` or `1.9.1 (system)`. Pull request by Remi Collet. GitHub #289. +* The PHPDoc of the pure PHP reader now lists the exceptions that its methods + can throw: + * The `MaxMind\Db\Reader` constructor, `get()`, and `getWithPrefixLen()` + declare `UnsupportedPlatformException`. The reader throws it when an + integer needs gmp or bcmath and neither is installed, or a data offset + exceeds the platform limit. + * The constructor declares `UnexpectedValueException`. + * `close()` declares `BadMethodCallException` in place of `Exception`. + `metadata()` no longer declares `InvalidArgumentException`, which it + cannot throw. + * `MaxMind\Db\Reader\Decoder::decode()` and `MaxMind\Db\Reader\Util::read()` + declare their exceptions. 1.14.0 (2026-09-10) ------------------- diff --git a/composer.json b/composer.json index 4ce807d..e235bca 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "friendsofphp/php-cs-fixer": "3.*", "phpunit/phpunit": ">=8.0.0,<10.0.0", "squizlabs/php_codesniffer": "4.*", - "phpstan/phpstan": "*", + "phpstan/phpstan": "^1.12 || ^2.2", "symfony/polyfill-php80": "^1.33" }, "autoload": { diff --git a/phpstan.neon b/phpstan.neon index 63aadba..7e1d22b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,16 @@ parameters: paths: - src - tests + exceptions: + # These classes signal programmer errors, so callers need not declare them. + uncheckedExceptionClasses: + - Error + - BadMethodCallException + check: + missingCheckedExceptionInThrows: true + ignoreErrors: + # PHPUnit handles exceptions from test methods and their helpers. + - + identifier: missingType.checkedException + path: tests/* + reportUnmatched: false diff --git a/src/MaxMind/Db/Reader.php b/src/MaxMind/Db/Reader.php index 923dc02..04ed2c0 100644 --- a/src/MaxMind/Db/Reader.php +++ b/src/MaxMind/Db/Reader.php @@ -7,11 +7,15 @@ use MaxMind\Db\Reader\Decoder; use MaxMind\Db\Reader\InvalidDatabaseException; use MaxMind\Db\Reader\Metadata; +use MaxMind\Db\Reader\UnsupportedPlatformException; use MaxMind\Db\Reader\Util; /** * Instances of this class provide a reader for the MaxMind DB format. IP * addresses can be looked up using the get method. + * + * The declared exceptions describe the pure PHP reader. The C extension may + * differ. */ class Reader { @@ -71,10 +75,18 @@ class Reader * * @param string $database the MaxMind DB file to use * - * @throws \InvalidArgumentException for invalid database path or unknown arguments + * @throws \InvalidArgumentException if the database file does not exist or + * is not readable * @throws InvalidDatabaseException - * if the database is invalid or there is an error reading - * from it + * if the database is invalid or there is an error reading + * from it + * @throws \UnexpectedValueException if the size of the database file + * cannot be determined + * @throws UnsupportedPlatformException if the metadata contains an integer + * that needs the gmp or bcmath extension + * and neither is installed, or a data + * offset that is too large for the + * platform */ public function __construct(string $database) { @@ -110,6 +122,9 @@ public function __construct(string $database) $start = $this->findMetadataStart($database); $metadataDecoder = new Decoder($this->fileHandle, $start); [$metadataArray] = $metadataDecoder->decode($start); + if (!\is_array($metadataArray)) { + throw new InvalidDatabaseException('The database metadata must be a map.'); + } $this->metadata = new Metadata($metadataArray); $this->decoder = new Decoder( $this->fileHandle, @@ -123,11 +138,18 @@ public function __construct(string $database) * * @param string $ipAddress the IP address to look up * - * @throws \BadMethodCallException if the database is closed or another lookup is in progress - * @throws \InvalidArgumentException if something other than a single IP address is passed to the method + * @throws \BadMethodCallException if the database is closed or another lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only * @throws InvalidDatabaseException - * if the database is invalid or there is an error reading - * from it + * if the database is invalid or there is an error reading + * from it + * @throws UnsupportedPlatformException if the record contains an integer + * that needs the gmp or bcmath extension + * and neither is installed, or a data + * offset that is too large for the + * platform * * @return mixed the record for the IP address */ @@ -148,11 +170,18 @@ public function get(string $ipAddress) * * @param string $ipAddress the IP address to look up * - * @throws \BadMethodCallException if the database is closed or another lookup is in progress - * @throws \InvalidArgumentException if something other than a single IP address is passed to the method + * @throws \BadMethodCallException if the database is closed or another lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only * @throws InvalidDatabaseException - * if the database is invalid or there is an error reading - * from it + * if the database is invalid or there is an error reading + * from it + * @throws UnsupportedPlatformException if the record contains an integer + * that needs the gmp or bcmath extension + * and neither is installed, or a data + * offset that is too large for the + * platform * * @return array{0:mixed, 1:int} an array where the first element is the record and the * second the network prefix length for the record @@ -193,6 +222,9 @@ public function getWithPrefixLen(string $ipAddress): array } /** + * @throws \InvalidArgumentException + * @throws InvalidDatabaseException + * * @return array{0:int, 1:int} */ private function findAddressInTree(string $ipAddress): array @@ -254,6 +286,9 @@ private function findAddressInTree(string $ipAddress): array ); } + /** + * @throws InvalidDatabaseException + */ private function ipV4StartNode(): int { // If we have an IPv4 database, the start node is the first node @@ -270,6 +305,9 @@ private function ipV4StartNode(): int return $node; } + /** + * @throws InvalidDatabaseException + */ private function readNode(int $nodeNumber, int $index): int { $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; @@ -325,6 +363,9 @@ private function readNode(int $nodeNumber, int $index): int } /** + * @throws InvalidDatabaseException + * @throws UnsupportedPlatformException + * * @return mixed */ private function resolveDataPointer(int $pointer) @@ -342,10 +383,12 @@ private function resolveDataPointer(int $pointer) return $data; } - /* + /** * This is an extremely naive but reasonably readable implementation. There * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever * an issue, but I suspect it won't be. + * + * @throws InvalidDatabaseException */ private function findMetadataStart(string $filename): int { @@ -374,8 +417,10 @@ private function findMetadataStart(string $filename): int } /** - * @throws \InvalidArgumentException if arguments are passed to the method - * @throws \BadMethodCallException if the database has been closed + * The C extension can also throw InvalidDatabaseException if it cannot + * decode the metadata. The pure PHP reader decodes it during construction. + * + * @throws \BadMethodCallException if the database has been closed * * @return Metadata object for the database */ @@ -401,8 +446,7 @@ public function metadata(): Metadata /** * Closes the MaxMind DB and returns resources to the system. * - * @throws \Exception - * if an I/O error occurs + * @throws \BadMethodCallException if the database has already been closed */ public function close(): void { diff --git a/src/MaxMind/Db/Reader/Decoder.php b/src/MaxMind/Db/Reader/Decoder.php index 02d9a10..eeb3d1c 100644 --- a/src/MaxMind/Db/Reader/Decoder.php +++ b/src/MaxMind/Db/Reader/Decoder.php @@ -111,6 +111,13 @@ public function __construct( } /** + * @throws InvalidDatabaseException if the data is invalid or there is an + * error reading it + * @throws UnsupportedPlatformException if the data contains an integer that + * needs the gmp or bcmath extension and + * neither is installed, or a data offset + * that is too large for the platform + * * @return array */ public function decode(int $offset): array @@ -129,6 +136,9 @@ public function decode(int $offset): array } /** + * @throws InvalidDatabaseException + * @throws UnsupportedPlatformException + * * @return array */ private function decodeWithBudget(int $offset, int $depth, bool $allowPointer = true): array @@ -194,6 +204,9 @@ private function decodeWithBudget(int $offset, int $depth, bool $allowPointer = /** * @param int<0, max> $size * + * @throws InvalidDatabaseException + * @throws UnsupportedPlatformException + * * @return array{0:mixed, 1:int} */ private function decodeByType(int $type, int $offset, int $size, int $depth): array @@ -275,6 +288,8 @@ private function decodeByType(int $type, int $offset, int $size, int $depth): ar * the previous one ended. * * @param int<0, max> $numberOfBytes + * + * @throws InvalidDatabaseException */ private function read(int $offset, int $numberOfBytes): string { @@ -300,6 +315,9 @@ private function read(int $offset, int $numberOfBytes): string return $value; } + /** + * @throws InvalidDatabaseException + */ private function verifySize(int $expected, int $actual): void { if ($expected !== $actual) { @@ -313,6 +331,8 @@ private function verifySize(int $expected, int $actual): void * Charges declared children before decoding them. An oversized container * fails before any child is read. Each visit to a shared container charges * its children again, which bounds pointer fan-out. + * + * @throws InvalidDatabaseException */ private function enterContainer( int $size, @@ -333,6 +353,9 @@ private function enterContainer( } /** + * @throws InvalidDatabaseException + * @throws UnsupportedPlatformException + * * @return array{0:array, 1:int} */ private function decodeArray(int $size, int $offset, int $depth): array @@ -354,6 +377,9 @@ private function decodeBoolean(int $size): bool return $size !== 0; } + /** + * @throws InvalidDatabaseException + */ private function decodeDouble(string $bytes): float { // This assumes IEEE 754 doubles, but most (all?) modern platforms @@ -369,6 +395,9 @@ private function decodeDouble(string $bytes): float return $double; } + /** + * @throws InvalidDatabaseException + */ private function decodeFloat(string $bytes): float { // This assumes IEEE 754 floats, but most (all?) modern platforms @@ -384,6 +413,9 @@ private function decodeFloat(string $bytes): float return $float; } + /** + * @throws InvalidDatabaseException + */ private function decodeInt32(string $bytes, int $size): int { switch ($size) { @@ -418,6 +450,9 @@ private function decodeInt32(string $bytes, int $size): int } /** + * @throws InvalidDatabaseException + * @throws UnsupportedPlatformException + * * @return array{0:array, 1:int} */ private function decodeMap(int $size, int $offset, int $depth): array @@ -429,6 +464,9 @@ private function decodeMap(int $size, int $offset, int $depth): array for ($i = 0; $i < $size; ++$i) { [$key, $offset] = $this->decodeWithBudget($offset, $depth + 1); + if (!\is_string($key)) { + throw new InvalidDatabaseException('A map key must be a string.'); + } [$value, $offset] = $this->decodeWithBudget($offset, $depth + 1); $map[$key] = $value; } @@ -437,6 +475,9 @@ private function decodeMap(int $size, int $offset, int $depth): array } /** + * @throws InvalidDatabaseException + * @throws UnsupportedPlatformException + * * @return array{0:int, 1:int} */ private function decodePointer(int $ctrlByte, int $offset): array @@ -499,7 +540,7 @@ private function decodePointer(int $ctrlByte, int $offset): array if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) { $pointer = $pointerOffset + $pointerBase; } else { - throw new \RuntimeException( + throw new UnsupportedPlatformException( 'The database offset is too large to be represented on your platform.' ); } @@ -515,7 +556,11 @@ private function decodePointer(int $ctrlByte, int $offset): array return [$pointer, $offset]; } - // @phpstan-ignore-next-line + /** + * @throws UnsupportedPlatformException + * + * @return int|string + */ private function decodeUint(string $bytes, int $byteLength) { if ($byteLength === 0) { @@ -548,7 +593,7 @@ private function decodeUint(string $bytes, int $byteLength) } elseif (\extension_loaded('bcmath')) { $integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part); } else { - throw new \RuntimeException( + throw new UnsupportedPlatformException( 'The gmp or bcmath extension must be installed to read this database.' ); } @@ -558,6 +603,8 @@ private function decodeUint(string $bytes, int $byteLength) } /** + * @throws InvalidDatabaseException + * * @return array{0:int, 1:int} */ private function sizeFromCtrlByte(int $ctrlByte, int $offset): array @@ -607,7 +654,7 @@ private function isPlatformLittleEndian(): bool $packed = pack('S', $testint); $rc = unpack('v', $packed); if ($rc === false) { - throw new InvalidDatabaseException( + throw new \Error( 'Could not unpack an unsigned short value from the given bytes.' ); } diff --git a/src/MaxMind/Db/Reader/Metadata.php b/src/MaxMind/Db/Reader/Metadata.php index 6cb6332..62ecba9 100644 --- a/src/MaxMind/Db/Reader/Metadata.php +++ b/src/MaxMind/Db/Reader/Metadata.php @@ -29,7 +29,7 @@ class Metadata * This is an unsigned 64-bit integer that contains the database build * timestamp as a Unix epoch value. * - * @var int + * @var int|string */ public $buildEpoch; @@ -97,6 +97,9 @@ class Metadata /** * @param array $metadata + * + * @throws InvalidDatabaseException if a metadata field is missing or invalid + * @throws UnsupportedPlatformException if the search tree exceeds the platform limit */ public function __construct(array $metadata) { @@ -106,6 +109,62 @@ public function __construct(array $metadata) ); } + $nodeCount = $metadata['node_count'] ?? null; + if (\is_string($nodeCount) && preg_match('/\A(?:0|[1-9][0-9]*)\z/', $nodeCount)) { + $maxInteger = (string) \PHP_INT_MAX; + if (\strlen($nodeCount) > \strlen($maxInteger) + || (\strlen($nodeCount) === \strlen($maxInteger) && strcmp($nodeCount, $maxInteger) > 0) + ) { + throw new UnsupportedPlatformException('The database node count exceeds the platform limit.'); + } + } + + foreach ([ + 'binary_format_major_version', + 'binary_format_minor_version', + 'ip_version', + 'node_count', + 'record_size', + ] as $key) { + if (!isset($metadata[$key]) || !\is_int($metadata[$key]) || $metadata[$key] < 0) { + throw new InvalidDatabaseException("Metadata field $key must be an unsigned integer."); + } + } + $buildEpoch = $metadata['build_epoch'] ?? null; + if ((!\is_int($buildEpoch) || $buildEpoch < 0) + && (!\is_string($buildEpoch) || !preg_match('/\A[0-9]+\z/', $buildEpoch)) + ) { + throw new InvalidDatabaseException('Metadata build_epoch must be an unsigned integer.'); + } + if (!\in_array($metadata['record_size'], [24, 28, 32], true)) { + throw new InvalidDatabaseException('Metadata record_size must be 24, 28, or 32.'); + } + if (!\in_array($metadata['ip_version'], [4, 6], true)) { + throw new InvalidDatabaseException('Metadata ip_version must be 4 or 6.'); + } + if (!isset($metadata['database_type']) || !\is_string($metadata['database_type'])) { + throw new InvalidDatabaseException('Metadata database_type must be a string.'); + } + foreach (['languages', 'description'] as $key) { + if (!\array_key_exists($key, $metadata)) { + $metadata[$key] = []; + } + if (!\is_array($metadata[$key])) { + throw new InvalidDatabaseException("Metadata field $key must be an array of strings."); + } + foreach ($metadata[$key] as $value) { + if (!\is_string($value)) { + throw new InvalidDatabaseException("Metadata field $key must contain only strings."); + } + } + } + + $nodeByteSize = intdiv($metadata['record_size'], 4); + // The data section starts after the search tree and its 16-byte separator. + if ($metadata['node_count'] > intdiv(\PHP_INT_MAX - 16, $nodeByteSize)) { + throw new UnsupportedPlatformException('The database search tree exceeds the platform limit.'); + } + $this->binaryFormatMajorVersion = $metadata['binary_format_major_version']; $this->binaryFormatMinorVersion @@ -117,7 +176,7 @@ public function __construct(array $metadata) $this->ipVersion = $metadata['ip_version']; $this->nodeCount = $metadata['node_count']; $this->recordSize = $metadata['record_size']; - $this->nodeByteSize = $this->recordSize / 4; + $this->nodeByteSize = $nodeByteSize; $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; } } diff --git a/src/MaxMind/Db/Reader/UnsupportedPlatformException.php b/src/MaxMind/Db/Reader/UnsupportedPlatformException.php new file mode 100644 index 0000000..eaa831c --- /dev/null +++ b/src/MaxMind/Db/Reader/UnsupportedPlatformException.php @@ -0,0 +1,11 @@ + $numberOfBytes + * + * @throws InvalidDatabaseException if the bytes cannot be read */ public static function read($stream, int $offset, int $numberOfBytes): string { diff --git a/tests/MaxMind/Db/Test/Reader/DecoderTest.php b/tests/MaxMind/Db/Test/Reader/DecoderTest.php index b8e465b..dddb19a 100644 --- a/tests/MaxMind/Db/Test/Reader/DecoderTest.php +++ b/tests/MaxMind/Db/Test/Reader/DecoderTest.php @@ -6,6 +6,7 @@ use MaxMind\Db\Reader\Decoder; use MaxMind\Db\Reader\InvalidDatabaseException; +use MaxMind\Db\Reader\UnsupportedPlatformException; use PHPUnit\Framework\TestCase; /** @@ -365,6 +366,56 @@ public function testMaps(): void $this->validateTypeDecodingList('map', $this->maps); } + /** + * @dataProvider invalidMapKeys + */ + public function testInvalidMapKey(string $key): void + { + $handle = fopen('php://memory', 'rwb'); + fwrite($handle, "\xe1" . $key . "\xa0"); + $decoder = new Decoder($handle); + + try { + $this->expectException(InvalidDatabaseException::class); + $this->expectExceptionMessage('A map key must be a string.'); + $decoder->decode(0); + } finally { + fclose($handle); + } + } + + /** + * @return array + */ + public static function invalidMapKeys(): array + { + return [ + 'map' => ["\xe0"], + 'array' => ["\x00\x04"], + 'uint16' => ["\xa1\x01"], + 'double' => ["\x68" . pack('E', 1.5)], + 'boolean' => ["\x01\x07"], + ]; + } + + public function testLargeIntegerWithoutExtensions(): void + { + if (\extension_loaded('gmp') || \extension_loaded('bcmath')) { + $this->markTestSkipped('This test requires both gmp and bcmath to be disabled.'); + } + $handle = fopen('php://memory', 'rwb'); + // uint64 with the high bit set cannot fit in a signed PHP integer. + fwrite($handle, "\x08\x02\x80\x00\x00\x00\x00\x00\x00\x00"); + $decoder = new Decoder($handle); + + try { + $this->expectException(UnsupportedPlatformException::class); + $decoder->decode(0); + } finally { + fclose($handle); + } + } + public function testPointers(): void { $this->validateTypeDecodingList('pointers', $this->pointers()); diff --git a/tests/MaxMind/Db/Test/Reader/MetadataTest.php b/tests/MaxMind/Db/Test/Reader/MetadataTest.php index bfddca0..c5391ad 100644 --- a/tests/MaxMind/Db/Test/Reader/MetadataTest.php +++ b/tests/MaxMind/Db/Test/Reader/MetadataTest.php @@ -4,7 +4,9 @@ namespace MaxMind\Db\Test\Reader; +use MaxMind\Db\Reader\InvalidDatabaseException; use MaxMind\Db\Reader\Metadata; +use MaxMind\Db\Reader\UnsupportedPlatformException; use PHPUnit\Framework\TestCase; /** @@ -42,6 +44,126 @@ public function testConstructor(): void $this->assertSame($metadata->searchTreeSize, 6 * 665037); } + /** + * @dataProvider invalidMetadata + * + * @param mixed $value + */ + public function testInvalidMetadata(string $key, $value): void + { + if (\extension_loaded('maxminddb')) { + $this->markTestSkipped('This test covers the pure PHP metadata constructor.'); + } + + $metadata = [ + 'node_count' => 1, + 'record_size' => 24, + 'ip_version' => 6, + 'binary_format_major_version' => 2, + 'binary_format_minor_version' => 0, + 'build_epoch' => 1594066370, + 'database_type' => 'Test', + 'languages' => ['en'], + 'description' => ['en' => 'Test'], + ]; + $metadata[$key] = $value; + + $this->expectException(InvalidDatabaseException::class); + new Metadata($metadata); + } + + /** + * @return array + */ + public static function invalidMetadata(): array + { + return [ + 'string record size' => ['record_size', 'bad'], + 'map record size' => ['record_size', ['bad' => 1]], + 'null record size' => ['record_size', null], + 'unsupported record size' => ['record_size', 16], + 'string node count' => ['node_count', '1'], + 'negative node count' => ['node_count', -1], + 'unsupported IP version' => ['ip_version', 5], + 'invalid database type' => ['database_type', []], + 'invalid languages' => ['languages', 'en'], + 'invalid language' => ['languages', [1]], + 'invalid description' => ['description', ['en' => []]], + 'invalid build epoch' => ['build_epoch', 'bad'], + ]; + } + + public function testOptionalMetadata(): void + { + if (\extension_loaded('maxminddb')) { + $this->markTestSkipped('This test covers the pure PHP metadata constructor.'); + } + + $metadata = new Metadata([ + 'node_count' => 1, + 'record_size' => 24, + 'ip_version' => 6, + 'binary_format_major_version' => 2, + 'binary_format_minor_version' => 0, + 'build_epoch' => '2147483648', + 'database_type' => 'Test', + ]); + $this->assertSame([], $metadata->languages); + $this->assertSame([], $metadata->description); + $this->assertSame('2147483648', $metadata->buildEpoch); + } + + public function testMissingMetadata(): void + { + if (\extension_loaded('maxminddb')) { + $this->markTestSkipped('This test covers the pure PHP metadata constructor.'); + } + + $this->expectException(InvalidDatabaseException::class); + new Metadata([]); + } + + /** + * @dataProvider unsupportedNodeCounts + * + * @param int|string $nodeCount + */ + public function testUnsupportedNodeCount($nodeCount): void + { + if (\extension_loaded('maxminddb')) { + $this->markTestSkipped('This test covers the pure PHP metadata constructor.'); + } + + $this->expectException(UnsupportedPlatformException::class); + new Metadata([ + 'node_count' => $nodeCount, + 'record_size' => 32, + 'ip_version' => 6, + 'binary_format_major_version' => 2, + 'binary_format_minor_version' => 0, + 'build_epoch' => 1594066370, + 'database_type' => 'Test', + ]); + } + + /** + * @return array + */ + public static function unsupportedNodeCounts(): array + { + $firstUnsupportedInteger = '9223372036854775808'; + if (\PHP_INT_SIZE === 4) { + $firstUnsupportedInteger = '2147483648'; + } + + return [ + 'decoded integer just beyond platform limit' => [$firstUnsupportedInteger], + 'decoded integer beyond platform limit' => [(string) \PHP_INT_MAX . '0'], + 'search tree multiplication overflow' => [\PHP_INT_MAX], + 'data section separator overflow' => [intdiv(\PHP_INT_MAX, 8)], + ]; + } + public function testTooManyConstructorArgs(): void { $this->expectException(\ArgumentCountError::class); diff --git a/tests/MaxMind/Db/Test/ReaderTest.php b/tests/MaxMind/Db/Test/ReaderTest.php index 399fa0d..1430d51 100644 --- a/tests/MaxMind/Db/Test/ReaderTest.php +++ b/tests/MaxMind/Db/Test/ReaderTest.php @@ -129,6 +129,35 @@ public function testMax(): void $this->assertSame('340282366920938463463374607431768211455', $uint128); } + public function testInvalidMetadataType(): void + { + $path = tempnam(sys_get_temp_dir(), 'mmdb-metadata-'); + file_put_contents($path, "\xab\xcd\xefMaxMind.com\xa0"); + + try { + $this->expectException(InvalidDatabaseException::class); + new Reader($path); + } finally { + unlink($path); + } + } + + public function testInvalidMetadataRecordSize(): void + { + $database = file_get_contents('tests/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'); + $database = str_replace("\x4brecord_size\xa1\x18", "\x4brecord_size\x41x", $database, $count); + $this->assertSame(1, $count); + $path = tempnam(sys_get_temp_dir(), 'mmdb-metadata-'); + file_put_contents($path, $database); + + try { + $this->expectException(InvalidDatabaseException::class); + new Reader($path); + } finally { + unlink($path); + } + } + public function testMetadataPointers(): void { $reader = new Reader(