[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

/includes/vendor/maxmind-db/reader/src/MaxMind/Db/ -> Reader.php (source)

   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace MaxMind\Db;
   6  
   7  use MaxMind\Db\Reader\Decoder;
   8  use MaxMind\Db\Reader\InvalidDatabaseException;
   9  use MaxMind\Db\Reader\Metadata;
  10  use MaxMind\Db\Reader\Util;
  11  
  12  /**
  13   * Instances of this class provide a reader for the MaxMind DB format. IP
  14   * addresses can be looked up using the get method.
  15   */
  16  class Reader
  17  {
  18      /**
  19       * @var int
  20       */
  21      private static $DATA_SECTION_SEPARATOR_SIZE = 16;
  22  
  23      /**
  24       * @var string
  25       */
  26      private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
  27  
  28      /**
  29       * @var int<0, max>
  30       */
  31      private static $METADATA_START_MARKER_LENGTH = 14;
  32  
  33      /**
  34       * @var int
  35       */
  36      private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB
  37  
  38      /**
  39       * @var Decoder
  40       */
  41      private $decoder;
  42  
  43      /**
  44       * @var resource
  45       */
  46      private $fileHandle;
  47  
  48      /**
  49       * @var int
  50       */
  51      private $fileSize;
  52  
  53      /**
  54       * @var int
  55       */
  56      private $ipV4Start;
  57  
  58      /**
  59       * @var Metadata
  60       */
  61      private $metadata;
  62  
  63      /**
  64       * Constructs a Reader for the MaxMind DB format. The file passed to it must
  65       * be a valid MaxMind DB file such as a GeoIp2 database file.
  66       *
  67       * @param string $database the MaxMind DB file to use
  68       *
  69       * @throws \InvalidArgumentException for invalid database path or unknown arguments
  70       * @throws InvalidDatabaseException
  71       *                                   if the database is invalid or there is an error reading
  72       *                                   from it
  73       */
  74      public function __construct(string $database)
  75      {
  76          if (\func_num_args() !== 1) {
  77              throw new \ArgumentCountError(
  78                  \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
  79              );
  80          }
  81  
  82          if (is_dir($database)) {
  83              // This matches the error that the C extension throws.
  84              throw new InvalidDatabaseException(
  85                  "Error opening database file ($database). Is this a valid MaxMind DB file?"
  86              );
  87          }
  88  
  89          $fileHandle = @fopen($database, 'rb');
  90          if ($fileHandle === false) {
  91              throw new \InvalidArgumentException(
  92                  "The file \"$database\" does not exist or is not readable."
  93              );
  94          }
  95          $this->fileHandle = $fileHandle;
  96  
  97          $fileSize = @filesize($database);
  98          if ($fileSize === false) {
  99              throw new \UnexpectedValueException(
 100                  "Error determining the size of \"$database\"."
 101              );
 102          }
 103          $this->fileSize = $fileSize;
 104  
 105          $start = $this->findMetadataStart($database);
 106          $metadataDecoder = new Decoder($this->fileHandle, $start);
 107          [$metadataArray] = $metadataDecoder->decode($start);
 108          $this->metadata = new Metadata($metadataArray);
 109          $this->decoder = new Decoder(
 110              $this->fileHandle,
 111              $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
 112          );
 113          $this->ipV4Start = $this->ipV4StartNode();
 114      }
 115  
 116      /**
 117       * Retrieves the record for the IP address.
 118       *
 119       * @param string $ipAddress the IP address to look up
 120       *
 121       * @throws \BadMethodCallException   if this method is called on a closed database
 122       * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
 123       * @throws InvalidDatabaseException
 124       *                                   if the database is invalid or there is an error reading
 125       *                                   from it
 126       *
 127       * @return mixed the record for the IP address
 128       */
 129      public function get(string $ipAddress)
 130      {
 131          if (\func_num_args() !== 1) {
 132              throw new \ArgumentCountError(
 133                  \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
 134              );
 135          }
 136          [$record] = $this->getWithPrefixLen($ipAddress);
 137  
 138          return $record;
 139      }
 140  
 141      /**
 142       * Retrieves the record for the IP address and its associated network prefix length.
 143       *
 144       * @param string $ipAddress the IP address to look up
 145       *
 146       * @throws \BadMethodCallException   if this method is called on a closed database
 147       * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
 148       * @throws InvalidDatabaseException
 149       *                                   if the database is invalid or there is an error reading
 150       *                                   from it
 151       *
 152       * @return array{0:mixed, 1:int} an array where the first element is the record and the
 153       *                               second the network prefix length for the record
 154       */
 155      public function getWithPrefixLen(string $ipAddress): array
 156      {
 157          if (\func_num_args() !== 1) {
 158              throw new \ArgumentCountError(
 159                  \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
 160              );
 161          }
 162  
 163          if (!\is_resource($this->fileHandle)) {
 164              throw new \BadMethodCallException(
 165                  'Attempt to read from a closed MaxMind DB.'
 166              );
 167          }
 168  
 169          [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
 170          if ($pointer === 0) {
 171              return [null, $prefixLen];
 172          }
 173  
 174          return [$this->resolveDataPointer($pointer), $prefixLen];
 175      }
 176  
 177      /**
 178       * @return array{0:int, 1:int}
 179       */
 180      private function findAddressInTree(string $ipAddress): array
 181      {
 182          $packedAddr = @inet_pton($ipAddress);
 183          if ($packedAddr === false) {
 184              throw new \InvalidArgumentException(
 185                  "The value \"$ipAddress\" is not a valid IP address."
 186              );
 187          }
 188  
 189          $rawAddress = unpack('C*', $packedAddr);
 190          if ($rawAddress === false) {
 191              throw new InvalidDatabaseException(
 192                  'Could not unpack the unsigned char of the packed in_addr representation.'
 193              );
 194          }
 195  
 196          $bitCount = \count($rawAddress) * 8;
 197  
 198          // The first node of the tree is always node 0, at the beginning of the
 199          // value
 200          $node = 0;
 201  
 202          $metadata = $this->metadata;
 203  
 204          // Check if we are looking up an IPv4 address in an IPv6 tree. If this
 205          // is the case, we can skip over the first 96 nodes.
 206          if ($metadata->ipVersion === 6) {
 207              if ($bitCount === 32) {
 208                  $node = $this->ipV4Start;
 209              }
 210          } elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
 211              throw new \InvalidArgumentException(
 212                  "Error looking up $ipAddress. You attempted to look up an"
 213                  . ' IPv6 address in an IPv4-only database.'
 214              );
 215          }
 216  
 217          $nodeCount = $metadata->nodeCount;
 218  
 219          for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
 220              $tempBit = 0xFF & $rawAddress[($i >> 3) + 1];
 221              $bit = 1 & ($tempBit >> 7 - ($i % 8));
 222  
 223              $node = $this->readNode($node, $bit);
 224          }
 225          if ($node === $nodeCount) {
 226              // Record is empty
 227              return [0, $i];
 228          }
 229          if ($node > $nodeCount) {
 230              // Record is a data pointer
 231              return [$node, $i];
 232          }
 233  
 234          throw new InvalidDatabaseException(
 235              'Invalid or corrupt database. Maximum search depth reached without finding a leaf node'
 236          );
 237      }
 238  
 239      private function ipV4StartNode(): int
 240      {
 241          // If we have an IPv4 database, the start node is the first node
 242          if ($this->metadata->ipVersion === 4) {
 243              return 0;
 244          }
 245  
 246          $node = 0;
 247  
 248          for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
 249              $node = $this->readNode($node, 0);
 250          }
 251  
 252          return $node;
 253      }
 254  
 255      private function readNode(int $nodeNumber, int $index): int
 256      {
 257          $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
 258  
 259          switch ($this->metadata->recordSize) {
 260              case 24:
 261                  $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
 262                  $rc = unpack('N', "\x00" . $bytes);
 263                  if ($rc === false) {
 264                      throw new InvalidDatabaseException(
 265                          'Could not unpack the unsigned long of the node.'
 266                      );
 267                  }
 268                  [, $node] = $rc;
 269  
 270                  return $node;
 271  
 272              case 28:
 273                  $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
 274                  if ($index === 0) {
 275                      $middle = (0xF0 & \ord($bytes[3])) >> 4;
 276                  } else {
 277                      $middle = 0x0F & \ord($bytes[0]);
 278                  }
 279                  $rc = unpack('N', \chr($middle) . substr($bytes, $index, 3));
 280                  if ($rc === false) {
 281                      throw new InvalidDatabaseException(
 282                          'Could not unpack the unsigned long of the node.'
 283                      );
 284                  }
 285                  [, $node] = $rc;
 286  
 287                  return $node;
 288  
 289              case 32:
 290                  $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
 291                  $rc = unpack('N', $bytes);
 292                  if ($rc === false) {
 293                      throw new InvalidDatabaseException(
 294                          'Could not unpack the unsigned long of the node.'
 295                      );
 296                  }
 297                  [, $node] = $rc;
 298  
 299                  return $node;
 300  
 301              default:
 302                  throw new InvalidDatabaseException(
 303                      'Unknown record size: '
 304                      . $this->metadata->recordSize
 305                  );
 306          }
 307      }
 308  
 309      /**
 310       * @return mixed
 311       */
 312      private function resolveDataPointer(int $pointer)
 313      {
 314          $resolved = $pointer - $this->metadata->nodeCount
 315              + $this->metadata->searchTreeSize;
 316          if ($resolved >= $this->fileSize) {
 317              throw new InvalidDatabaseException(
 318                  "The MaxMind DB file's search tree is corrupt"
 319              );
 320          }
 321  
 322          [$data] = $this->decoder->decode($resolved);
 323  
 324          return $data;
 325      }
 326  
 327      /*
 328       * This is an extremely naive but reasonably readable implementation. There
 329       * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
 330       * an issue, but I suspect it won't be.
 331       */
 332      private function findMetadataStart(string $filename): int
 333      {
 334          $handle = $this->fileHandle;
 335          $fstat = fstat($handle);
 336          if ($fstat === false) {
 337              throw new InvalidDatabaseException(
 338                  "Error getting file information ($filename)."
 339              );
 340          }
 341          $fileSize = $fstat['size'];
 342          $marker = self::$METADATA_START_MARKER;
 343          $markerLength = self::$METADATA_START_MARKER_LENGTH;
 344  
 345          $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize);
 346  
 347          for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
 348              if (fseek($handle, $offset) !== 0) {
 349                  break;
 350              }
 351  
 352              $value = fread($handle, $markerLength);
 353              if ($value === $marker) {
 354                  return $offset + $markerLength;
 355              }
 356          }
 357  
 358          throw new InvalidDatabaseException(
 359              "Error opening database file ($filename). " .
 360              'Is this a valid MaxMind DB file?'
 361          );
 362      }
 363  
 364      /**
 365       * @throws \InvalidArgumentException if arguments are passed to the method
 366       * @throws \BadMethodCallException   if the database has been closed
 367       *
 368       * @return Metadata object for the database
 369       */
 370      public function metadata(): Metadata
 371      {
 372          if (\func_num_args()) {
 373              throw new \ArgumentCountError(
 374                  \sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
 375              );
 376          }
 377  
 378          // Not technically required, but this makes it consistent with
 379          // C extension and it allows us to change our implementation later.
 380          if (!\is_resource($this->fileHandle)) {
 381              throw new \BadMethodCallException(
 382                  'Attempt to read from a closed MaxMind DB.'
 383              );
 384          }
 385  
 386          return clone $this->metadata;
 387      }
 388  
 389      /**
 390       * Closes the MaxMind DB and returns resources to the system.
 391       *
 392       * @throws \Exception
 393       *                    if an I/O error occurs
 394       */
 395      public function close(): void
 396      {
 397          if (\func_num_args()) {
 398              throw new \ArgumentCountError(
 399                  \sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
 400              );
 401          }
 402  
 403          if (!\is_resource($this->fileHandle)) {
 404              throw new \BadMethodCallException(
 405                  'Attempt to close a closed MaxMind DB.'
 406              );
 407          }
 408          fclose($this->fileHandle);
 409      }
 410  }


Generated: Mon Mar 31 05:10:02 2025 Cross-referenced by PHPXref 0.7.1