From 75160b12821f7f4299cce7f0b69c83c1502ae071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Luka=20=C5=A0ijanec?= Date: Mon, 27 May 2024 13:08:29 +0200 Subject: 2024-02-19 upstream --- vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php | 218 ++++++++++++++------- .../reader/src/MaxMind/Db/Reader/Decoder.php | 134 +++++++++---- .../MaxMind/Db/Reader/InvalidDatabaseException.php | 9 +- .../reader/src/MaxMind/Db/Reader/Metadata.php | 156 +++++++++------ .../reader/src/MaxMind/Db/Reader/Util.php | 11 +- 5 files changed, 348 insertions(+), 180 deletions(-) (limited to 'vendor/maxmind-db/reader/src/MaxMind') diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php index 85457c5..807fe62 100644 --- a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php @@ -1,15 +1,13 @@ + */ private static $METADATA_START_MARKER_LENGTH = 14; - private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB + /** + * @var int + */ + private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB + + /** + * @var Decoder + */ private $decoder; + + /** + * @var resource + */ private $fileHandle; + + /** + * @var int + */ private $fileSize; + + /** + * @var int + */ private $ipV4Start; + + /** + * @var Metadata + */ private $metadata; /** @@ -35,40 +67,38 @@ class Reader * @param string $database * the MaxMind DB file to use * - * @throws InvalidArgumentException for invalid database path or unknown arguments - * @throws \MaxMind\Db\Reader\InvalidDatabaseException - * if the database is invalid or there is an error reading - * from it + * @throws \InvalidArgumentException for invalid database path or unknown arguments + * @throws InvalidDatabaseException + * if the database is invalid or there is an error reading + * from it */ - public function __construct($database) + public function __construct(string $database) { if (\func_num_args() !== 1) { - throw new InvalidArgumentException( - 'The constructor takes exactly one argument.' + throw new \ArgumentCountError( + sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) ); } - if (!is_readable($database)) { - throw new InvalidArgumentException( + $fileHandle = @fopen($database, 'rb'); + if ($fileHandle === false) { + throw new \InvalidArgumentException( "The file \"$database\" does not exist or is not readable." ); } - $this->fileHandle = @fopen($database, 'rb'); - if ($this->fileHandle === false) { - throw new InvalidArgumentException( - "Error opening \"$database\"." - ); - } - $this->fileSize = @filesize($database); - if ($this->fileSize === false) { - throw new UnexpectedValueException( + $this->fileHandle = $fileHandle; + + $fileSize = @filesize($database); + if ($fileSize === false) { + throw new \UnexpectedValueException( "Error determining the size of \"$database\"." ); } + $this->fileSize = $fileSize; $start = $this->findMetadataStart($database); $metadataDecoder = new Decoder($this->fileHandle, $start); - list($metadataArray) = $metadataDecoder->decode($start); + [$metadataArray] = $metadataDecoder->decode($start); $this->metadata = new Metadata($metadataArray); $this->decoder = new Decoder( $this->fileHandle, @@ -83,22 +113,22 @@ class Reader * @param string $ipAddress * the IP address to look up * - * @throws BadMethodCallException if this method is called on a closed database - * @throws InvalidArgumentException if something other than a single IP address is passed to the method + * @throws \BadMethodCallException if this method is called on a closed database + * @throws \InvalidArgumentException if something other than a single IP address is passed to the method * @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 * * @return mixed the record for the IP address */ - public function get($ipAddress) + public function get(string $ipAddress) { if (\func_num_args() !== 1) { - throw new InvalidArgumentException( - 'Method takes exactly one argument.' + throw new \ArgumentCountError( + sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) ); } - list($record) = $this->getWithPrefixLen($ipAddress); + [$record] = $this->getWithPrefixLen($ipAddress); return $record; } @@ -109,36 +139,30 @@ class Reader * @param string $ipAddress * the IP address to look up * - * @throws BadMethodCallException if this method is called on a closed database - * @throws InvalidArgumentException if something other than a single IP address is passed to the method + * @throws \BadMethodCallException if this method is called on a closed database + * @throws \InvalidArgumentException if something other than a single IP address is passed to the method * @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 * * @return array an array where the first element is the record and the * second the network prefix length for the record */ - public function getWithPrefixLen($ipAddress) + public function getWithPrefixLen(string $ipAddress): array { if (\func_num_args() !== 1) { - throw new InvalidArgumentException( - 'Method takes exactly one argument.' + throw new \ArgumentCountError( + sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) ); } if (!\is_resource($this->fileHandle)) { - throw new BadMethodCallException( + throw new \BadMethodCallException( 'Attempt to read from a closed MaxMind DB.' ); } - if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) { - throw new InvalidArgumentException( - "The value \"$ipAddress\" is not a valid IP address." - ); - } - - list($pointer, $prefixLen) = $this->findAddressInTree($ipAddress); + [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress); if ($pointer === 0) { return [null, $prefixLen]; } @@ -146,9 +170,21 @@ class Reader return [$this->resolveDataPointer($pointer), $prefixLen]; } - private function findAddressInTree($ipAddress) + private function findAddressInTree(string $ipAddress): array { - $rawAddress = unpack('C*', inet_pton($ipAddress)); + $packedAddr = @inet_pton($ipAddress); + if ($packedAddr === false) { + throw new \InvalidArgumentException( + "The value \"$ipAddress\" is not a valid IP address." + ); + } + + $rawAddress = unpack('C*', $packedAddr); + if ($rawAddress === false) { + throw new InvalidDatabaseException( + 'Could not unpack the unsigned char of the packed in_addr representation.' + ); + } $bitCount = \count($rawAddress) * 8; @@ -165,7 +201,7 @@ class Reader $node = $this->ipV4Start; } } elseif ($metadata->ipVersion === 4 && $bitCount === 128) { - throw new InvalidArgumentException( + throw new \InvalidArgumentException( "Error looking up $ipAddress. You attempted to look up an" . ' IPv6 address in an IPv4-only database.' ); @@ -182,14 +218,18 @@ class Reader if ($node === $nodeCount) { // Record is empty return [0, $i]; - } elseif ($node > $nodeCount) { + } + if ($node > $nodeCount) { // Record is a data pointer return [$node, $i]; } - throw new InvalidDatabaseException('Something bad happened'); + + throw new InvalidDatabaseException( + 'Invalid or corrupt database. Maximum search depth reached without finding a leaf node' + ); } - private function ipV4StartNode() + private function ipV4StartNode(): int { // If we have an IPv4 database, the start node is the first node if ($this->metadata->ipVersion === 4) { @@ -205,16 +245,23 @@ class Reader return $node; } - private function readNode($nodeNumber, $index) + private function readNode(int $nodeNumber, int $index): int { $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; switch ($this->metadata->recordSize) { case 24: $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3); - list(, $node) = unpack('N', "\x00" . $bytes); + $rc = unpack('N', "\x00" . $bytes); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack the unsigned long of the node.' + ); + } + [, $node] = $rc; return $node; + case 28: $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4); if ($index === 0) { @@ -222,14 +269,28 @@ class Reader } else { $middle = 0x0F & \ord($bytes[0]); } - list(, $node) = unpack('N', \chr($middle) . substr($bytes, $index, 3)); + $rc = unpack('N', \chr($middle) . substr($bytes, $index, 3)); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack the unsigned long of the node.' + ); + } + [, $node] = $rc; return $node; + case 32: $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4); - list(, $node) = unpack('N', $bytes); + $rc = unpack('N', $bytes); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack the unsigned long of the node.' + ); + } + [, $node] = $rc; return $node; + default: throw new InvalidDatabaseException( 'Unknown record size: ' @@ -238,7 +299,10 @@ class Reader } } - private function resolveDataPointer($pointer) + /** + * @return mixed + */ + private function resolveDataPointer(int $pointer) { $resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize; @@ -248,7 +312,7 @@ class Reader ); } - list($data) = $this->decoder->decode($resolved); + [$data] = $this->decoder->decode($resolved); return $data; } @@ -258,10 +322,15 @@ class Reader * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever * an issue, but I suspect it won't be. */ - private function findMetadataStart($filename) + private function findMetadataStart(string $filename): int { $handle = $this->fileHandle; $fstat = fstat($handle); + if ($fstat === false) { + throw new InvalidDatabaseException( + "Error getting file information ($filename)." + ); + } $fileSize = $fstat['size']; $marker = self::$METADATA_START_MARKER; $markerLength = self::$METADATA_START_MARKER_LENGTH; @@ -278,6 +347,7 @@ class Reader return $offset + $markerLength; } } + throw new InvalidDatabaseException( "Error opening database file ($filename). " . 'Is this a valid MaxMind DB file?' @@ -285,40 +355,46 @@ class Reader } /** - * @throws InvalidArgumentException if arguments are passed to the method - * @throws BadMethodCallException if the database has been closed + * @throws \InvalidArgumentException if arguments are passed to the method + * @throws \BadMethodCallException if the database has been closed * * @return Metadata object for the database */ - public function metadata() + public function metadata(): Metadata { if (\func_num_args()) { - throw new InvalidArgumentException( - 'Method takes no arguments.' + throw new \ArgumentCountError( + sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()) ); } // Not technically required, but this makes it consistent with // C extension and it allows us to change our implementation later. if (!\is_resource($this->fileHandle)) { - throw new BadMethodCallException( + throw new \BadMethodCallException( 'Attempt to read from a closed MaxMind DB.' ); } - return $this->metadata; + return clone $this->metadata; } /** * Closes the MaxMind DB and returns resources to the system. * - * @throws Exception - * if an I/O error occurs + * @throws \Exception + * if an I/O error occurs */ - public function close() + public function close(): void { + if (\func_num_args()) { + throw new \ArgumentCountError( + sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()) + ); + } + if (!\is_resource($this->fileHandle)) { - throw new BadMethodCallException( + throw new \BadMethodCallException( 'Attempt to close a closed MaxMind DB.' ); } diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php index 8f451b8..8786a01 100644 --- a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php @@ -5,14 +5,6 @@ declare(strict_types=1); namespace MaxMind\Db\Reader; // @codingStandardsIgnoreLine -use RuntimeException; - -/* - * @ignore - * - * We subtract 1 from the log to protect against precision loss. - */ -\define(__NAMESPACE__ . '\_MM_MAX_INT_BYTES', (int) ((log(\PHP_INT_MAX, 2) - 1) / 8)); class Decoder { @@ -20,20 +12,19 @@ class Decoder * @var resource */ private $fileStream; + /** * @var int */ private $pointerBase; - /** - * @var float - */ - private $pointerBaseByteSize; + /** * This is only used for unit testing. * * @var bool */ private $pointerTestHack; + /** * @var bool */ @@ -51,8 +42,8 @@ class Decoder private const _UINT64 = 9; private const _UINT128 = 10; private const _ARRAY = 11; - private const _CONTAINER = 12; - private const _END_MARKER = 13; + // 12 is the container type + // 13 is the end marker type private const _BOOLEAN = 14; private const _FLOAT = 15; @@ -67,7 +58,6 @@ class Decoder $this->fileStream = $fileStream; $this->pointerBase = $pointerBase; - $this->pointerBaseByteSize = $pointerBase > 0 ? log($pointerBase, 2) / 8 : 0; $this->pointerTestHack = $pointerTestHack; $this->switchByteOrder = $this->isPlatformLittleEndian(); @@ -118,6 +108,9 @@ class Decoder return $this->decodeByType($type, $offset, $size); } + /** + * @param int<0, max> $size + */ private function decodeByType(int $type, int $offset, int $size): array { switch ($type) { @@ -195,7 +188,13 @@ class Decoder { // This assumes IEEE 754 doubles, but most (all?) modern platforms // use them. - [, $double] = unpack('E', $bytes); + $rc = unpack('E', $bytes); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack a double value from the given bytes.' + ); + } + [, $double] = $rc; return $double; } @@ -204,7 +203,13 @@ class Decoder { // This assumes IEEE 754 floats, but most (all?) modern platforms // use them. - [, $float] = unpack('G', $bytes); + $rc = unpack('G', $bytes); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack a float value from the given bytes.' + ); + } + [, $float] = $rc; return $float; } @@ -231,7 +236,13 @@ class Decoder ); } - [, $int] = unpack('l', $this->maybeSwitchByteOrder($bytes)); + $rc = unpack('l', $this->maybeSwitchByteOrder($bytes)); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack a 32bit integer value from the given bytes.' + ); + } + [, $int] = $rc; return $int; } @@ -254,19 +265,31 @@ class Decoder $pointerSize = (($ctrlByte >> 3) & 0x3) + 1; $buffer = Util::read($this->fileStream, $offset, $pointerSize); - $offset = $offset + $pointerSize; + $offset += $pointerSize; switch ($pointerSize) { case 1: $packed = \chr($ctrlByte & 0x7) . $buffer; - [, $pointer] = unpack('n', $packed); + $rc = unpack('n', $packed); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack an unsigned short value from the given bytes (pointerSize is 1).' + ); + } + [, $pointer] = $rc; $pointer += $this->pointerBase; break; case 2: $packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer; - [, $pointer] = unpack('N', $packed); + $rc = unpack('N', $packed); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack an unsigned long value from the given bytes (pointerSize is 2).' + ); + } + [, $pointer] = $rc; $pointer += $this->pointerBase + 2048; break; @@ -276,7 +299,13 @@ class Decoder // It is safe to use 'N' here, even on 32 bit machines as the // first bit is 0. - [, $pointer] = unpack('N', $packed); + $rc = unpack('N', $packed); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack an unsigned long value from the given bytes (pointerSize is 3).' + ); + } + [, $pointer] = $rc; $pointer += $this->pointerBase + 526336; break; @@ -291,7 +320,7 @@ class Decoder if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) { $pointer = $pointerOffset + $pointerBase; } else { - throw new RuntimeException( + throw new \RuntimeException( 'The database offset is too large to be represented on your platform.' ); } @@ -314,37 +343,44 @@ class Decoder return 0; } - $integer = 0; - - // PHP integers are signed. _MM_MAX_INT_BYTES is the number of + // PHP integers are signed. PHP_INT_SIZE - 1 is the number of // complete bytes that can be converted to an integer. However, // we can convert another byte if the leading bit is zero. - $useRealInts = $byteLength <= _MM_MAX_INT_BYTES - || ($byteLength === _MM_MAX_INT_BYTES + 1 && (\ord($bytes[0]) & 0x80) === 0); + $useRealInts = $byteLength <= \PHP_INT_SIZE - 1 + || ($byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0); + + if ($useRealInts) { + $integer = 0; + for ($i = 0; $i < $byteLength; ++$i) { + $part = \ord($bytes[$i]); + $integer = ($integer << 8) + $part; + } + return $integer; + } + + // We only use gmp or bcmath if the final value is too big + $integerAsString = '0'; for ($i = 0; $i < $byteLength; ++$i) { $part = \ord($bytes[$i]); - // We only use gmp or bcmath if the final value is too big - if ($useRealInts) { - $integer = ($integer << 8) + $part; - } elseif (\extension_loaded('gmp')) { - $integer = gmp_strval(gmp_add(gmp_mul((string) $integer, '256'), $part)); + if (\extension_loaded('gmp')) { + $integerAsString = gmp_strval(gmp_add(gmp_mul($integerAsString, '256'), $part)); } elseif (\extension_loaded('bcmath')) { - $integer = bcadd(bcmul((string) $integer, '256'), (string) $part); + $integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part); } else { - throw new RuntimeException( + throw new \RuntimeException( 'The gmp or bcmath extension must be installed to read this database.' ); } } - return $integer; + return $integerAsString; } private function sizeFromCtrlByte(int $ctrlByte, int $offset): array { - $size = $ctrlByte & 0x1f; + $size = $ctrlByte & 0x1F; if ($size < 29) { return [$size, $offset]; @@ -356,10 +392,22 @@ class Decoder if ($size === 29) { $size = 29 + \ord($bytes); } elseif ($size === 30) { - [, $adjust] = unpack('n', $bytes); + $rc = unpack('n', $bytes); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack an unsigned short value from the given bytes.' + ); + } + [, $adjust] = $rc; $size = 285 + $adjust; } else { - [, $adjust] = unpack('N', "\x00" . $bytes); + $rc = unpack('N', "\x00" . $bytes); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack an unsigned long value from the given bytes.' + ); + } + [, $adjust] = $rc; $size = $adjust + 65821; } @@ -375,7 +423,13 @@ class Decoder { $testint = 0x00FF; $packed = pack('S', $testint); + $rc = unpack('v', $packed); + if ($rc === false) { + throw new InvalidDatabaseException( + 'Could not unpack an unsigned short value from the given bytes.' + ); + } - return $testint === current(unpack('v', $packed)); + return $testint === current($rc); } } diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php index 543fde4..028e63f 100644 --- a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php @@ -1,12 +1,11 @@ binaryFormatMajorVersion = $metadata['binary_format_major_version']; $this->binaryFormatMinorVersion = @@ -80,9 +117,4 @@ class Metadata $this->nodeByteSize = $this->recordSize / 4; $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; } - - public function __get($var) - { - return $this->$var; - } } diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php index 149a5c4..b8c461e 100644 --- a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php @@ -1,10 +1,16 @@ $numberOfBytes + */ + public static function read($stream, int $offset, int $numberOfBytes): string { if ($numberOfBytes === 0) { return ''; @@ -15,10 +21,11 @@ class Util // We check that the number of bytes read is equal to the number // asked for. We use ftell as getting the length of $value is // much slower. - if (ftell($stream) - $offset === $numberOfBytes) { + if ($value !== false && ftell($stream) - $offset === $numberOfBytes) { return $value; } } + throw new InvalidDatabaseException( 'The MaxMind DB file contains bad data' ); -- cgit v1.2.3