[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

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

   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace MaxMind\Db\Reader;
   6  
   7  // @codingStandardsIgnoreLine
   8  
   9  class Decoder
  10  {
  11      /**
  12       * @var resource
  13       */
  14      private $fileStream;
  15  
  16      /**
  17       * @var int
  18       */
  19      private $pointerBase;
  20  
  21      /**
  22       * This is only used for unit testing.
  23       *
  24       * @var bool
  25       */
  26      private $pointerTestHack;
  27  
  28      /**
  29       * @var bool
  30       */
  31      private $switchByteOrder;
  32  
  33      private const _EXTENDED = 0;
  34      private const _POINTER = 1;
  35      private const _UTF8_STRING = 2;
  36      private const _DOUBLE = 3;
  37      private const _BYTES = 4;
  38      private const _UINT16 = 5;
  39      private const _UINT32 = 6;
  40      private const _MAP = 7;
  41      private const _INT32 = 8;
  42      private const _UINT64 = 9;
  43      private const _UINT128 = 10;
  44      private const _ARRAY = 11;
  45      // 12 is the container type
  46      // 13 is the end marker type
  47      private const _BOOLEAN = 14;
  48      private const _FLOAT = 15;
  49  
  50      /**
  51       * @param resource $fileStream
  52       */
  53      public function __construct(
  54          $fileStream,
  55          int $pointerBase = 0,
  56          bool $pointerTestHack = false
  57      ) {
  58          $this->fileStream = $fileStream;
  59          $this->pointerBase = $pointerBase;
  60  
  61          $this->pointerTestHack = $pointerTestHack;
  62  
  63          $this->switchByteOrder = $this->isPlatformLittleEndian();
  64      }
  65  
  66      /**
  67       * @return array<mixed>
  68       */
  69      public function decode(int $offset): array
  70      {
  71          $ctrlByte = \ord(Util::read($this->fileStream, $offset, 1));
  72          ++$offset;
  73  
  74          $type = $ctrlByte >> 5;
  75  
  76          // Pointers are a special case, we don't read the next $size bytes, we
  77          // use the size to determine the length of the pointer and then follow
  78          // it.
  79          if ($type === self::_POINTER) {
  80              [$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);
  81  
  82              // for unit testing
  83              if ($this->pointerTestHack) {
  84                  return [$pointer];
  85              }
  86  
  87              [$result] = $this->decode($pointer);
  88  
  89              return [$result, $offset];
  90          }
  91  
  92          if ($type === self::_EXTENDED) {
  93              $nextByte = \ord(Util::read($this->fileStream, $offset, 1));
  94  
  95              $type = $nextByte + 7;
  96  
  97              if ($type < 8) {
  98                  throw new InvalidDatabaseException(
  99                      'Something went horribly wrong in the decoder. An extended type '
 100                      . 'resolved to a type number < 8 ('
 101                      . $type
 102                      . ')'
 103                  );
 104              }
 105  
 106              ++$offset;
 107          }
 108  
 109          [$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);
 110  
 111          return $this->decodeByType($type, $offset, $size);
 112      }
 113  
 114      /**
 115       * @param int<0, max> $size
 116       *
 117       * @return array{0:mixed, 1:int}
 118       */
 119      private function decodeByType(int $type, int $offset, int $size): array
 120      {
 121          switch ($type) {
 122              case self::_MAP:
 123                  return $this->decodeMap($size, $offset);
 124  
 125              case self::_ARRAY:
 126                  return $this->decodeArray($size, $offset);
 127  
 128              case self::_BOOLEAN:
 129                  return [$this->decodeBoolean($size), $offset];
 130          }
 131  
 132          $newOffset = $offset + $size;
 133          $bytes = Util::read($this->fileStream, $offset, $size);
 134  
 135          switch ($type) {
 136              case self::_BYTES:
 137              case self::_UTF8_STRING:
 138                  return [$bytes, $newOffset];
 139  
 140              case self::_DOUBLE:
 141                  $this->verifySize(8, $size);
 142  
 143                  return [$this->decodeDouble($bytes), $newOffset];
 144  
 145              case self::_FLOAT:
 146                  $this->verifySize(4, $size);
 147  
 148                  return [$this->decodeFloat($bytes), $newOffset];
 149  
 150              case self::_INT32:
 151                  return [$this->decodeInt32($bytes, $size), $newOffset];
 152  
 153              case self::_UINT16:
 154              case self::_UINT32:
 155              case self::_UINT64:
 156              case self::_UINT128:
 157                  return [$this->decodeUint($bytes, $size), $newOffset];
 158  
 159              default:
 160                  throw new InvalidDatabaseException(
 161                      'Unknown or unexpected type: ' . $type
 162                  );
 163          }
 164      }
 165  
 166      private function verifySize(int $expected, int $actual): void
 167      {
 168          if ($expected !== $actual) {
 169              throw new InvalidDatabaseException(
 170                  "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
 171              );
 172          }
 173      }
 174  
 175      /**
 176       * @return array{0:array<mixed>, 1:int}
 177       */
 178      private function decodeArray(int $size, int $offset): array
 179      {
 180          $array = [];
 181  
 182          for ($i = 0; $i < $size; ++$i) {
 183              [$value, $offset] = $this->decode($offset);
 184              $array[] = $value;
 185          }
 186  
 187          return [$array, $offset];
 188      }
 189  
 190      private function decodeBoolean(int $size): bool
 191      {
 192          return $size !== 0;
 193      }
 194  
 195      private function decodeDouble(string $bytes): float
 196      {
 197          // This assumes IEEE 754 doubles, but most (all?) modern platforms
 198          // use them.
 199          $rc = unpack('E', $bytes);
 200          if ($rc === false) {
 201              throw new InvalidDatabaseException(
 202                  'Could not unpack a double value from the given bytes.'
 203              );
 204          }
 205          [, $double] = $rc;
 206  
 207          return $double;
 208      }
 209  
 210      private function decodeFloat(string $bytes): float
 211      {
 212          // This assumes IEEE 754 floats, but most (all?) modern platforms
 213          // use them.
 214          $rc = unpack('G', $bytes);
 215          if ($rc === false) {
 216              throw new InvalidDatabaseException(
 217                  'Could not unpack a float value from the given bytes.'
 218              );
 219          }
 220          [, $float] = $rc;
 221  
 222          return $float;
 223      }
 224  
 225      private function decodeInt32(string $bytes, int $size): int
 226      {
 227          switch ($size) {
 228              case 0:
 229                  return 0;
 230  
 231              case 1:
 232              case 2:
 233              case 3:
 234                  $bytes = str_pad($bytes, 4, "\x00", \STR_PAD_LEFT);
 235  
 236                  break;
 237  
 238              case 4:
 239                  break;
 240  
 241              default:
 242                  throw new InvalidDatabaseException(
 243                      "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
 244                  );
 245          }
 246  
 247          $rc = unpack('l', $this->maybeSwitchByteOrder($bytes));
 248          if ($rc === false) {
 249              throw new InvalidDatabaseException(
 250                  'Could not unpack a 32bit integer value from the given bytes.'
 251              );
 252          }
 253          [, $int] = $rc;
 254  
 255          return $int;
 256      }
 257  
 258      /**
 259       * @return array{0:array<string, mixed>, 1:int}
 260       */
 261      private function decodeMap(int $size, int $offset): array
 262      {
 263          $map = [];
 264  
 265          for ($i = 0; $i < $size; ++$i) {
 266              [$key, $offset] = $this->decode($offset);
 267              [$value, $offset] = $this->decode($offset);
 268              $map[$key] = $value;
 269          }
 270  
 271          return [$map, $offset];
 272      }
 273  
 274      /**
 275       * @return array{0:int, 1:int}
 276       */
 277      private function decodePointer(int $ctrlByte, int $offset): array
 278      {
 279          $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
 280  
 281          $buffer = Util::read($this->fileStream, $offset, $pointerSize);
 282          $offset += $pointerSize;
 283  
 284          switch ($pointerSize) {
 285              case 1:
 286                  $packed = \chr($ctrlByte & 0x7) . $buffer;
 287                  $rc = unpack('n', $packed);
 288                  if ($rc === false) {
 289                      throw new InvalidDatabaseException(
 290                          'Could not unpack an unsigned short value from the given bytes (pointerSize is 1).'
 291                      );
 292                  }
 293                  [, $pointer] = $rc;
 294                  $pointer += $this->pointerBase;
 295  
 296                  break;
 297  
 298              case 2:
 299                  $packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
 300                  $rc = unpack('N', $packed);
 301                  if ($rc === false) {
 302                      throw new InvalidDatabaseException(
 303                          'Could not unpack an unsigned long value from the given bytes (pointerSize is 2).'
 304                      );
 305                  }
 306                  [, $pointer] = $rc;
 307                  $pointer += $this->pointerBase + 2048;
 308  
 309                  break;
 310  
 311              case 3:
 312                  $packed = \chr($ctrlByte & 0x7) . $buffer;
 313  
 314                  // It is safe to use 'N' here, even on 32 bit machines as the
 315                  // first bit is 0.
 316                  $rc = unpack('N', $packed);
 317                  if ($rc === false) {
 318                      throw new InvalidDatabaseException(
 319                          'Could not unpack an unsigned long value from the given bytes (pointerSize is 3).'
 320                      );
 321                  }
 322                  [, $pointer] = $rc;
 323                  $pointer += $this->pointerBase + 526336;
 324  
 325                  break;
 326  
 327              case 4:
 328                  // We cannot use unpack here as we might overflow on 32 bit
 329                  // machines
 330                  $pointerOffset = $this->decodeUint($buffer, $pointerSize);
 331  
 332                  $pointerBase = $this->pointerBase;
 333  
 334                  if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) {
 335                      $pointer = $pointerOffset + $pointerBase;
 336                  } else {
 337                      throw new \RuntimeException(
 338                          'The database offset is too large to be represented on your platform.'
 339                      );
 340                  }
 341  
 342                  break;
 343  
 344              default:
 345                  throw new InvalidDatabaseException(
 346                      'Unexpected pointer size ' . $pointerSize
 347                  );
 348          }
 349  
 350          return [$pointer, $offset];
 351      }
 352  
 353      // @phpstan-ignore-next-line
 354      private function decodeUint(string $bytes, int $byteLength)
 355      {
 356          if ($byteLength === 0) {
 357              return 0;
 358          }
 359  
 360          // PHP integers are signed. PHP_INT_SIZE - 1 is the number of
 361          // complete bytes that can be converted to an integer. However,
 362          // we can convert another byte if the leading bit is zero.
 363          $useRealInts = $byteLength <= \PHP_INT_SIZE - 1
 364              || ($byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0);
 365  
 366          if ($useRealInts) {
 367              $integer = 0;
 368              for ($i = 0; $i < $byteLength; ++$i) {
 369                  $part = \ord($bytes[$i]);
 370                  $integer = ($integer << 8) + $part;
 371              }
 372  
 373              return $integer;
 374          }
 375  
 376          // We only use gmp or bcmath if the final value is too big
 377          $integerAsString = '0';
 378          for ($i = 0; $i < $byteLength; ++$i) {
 379              $part = \ord($bytes[$i]);
 380  
 381              if (\extension_loaded('gmp')) {
 382                  $integerAsString = gmp_strval(gmp_add(gmp_mul($integerAsString, '256'), $part));
 383              } elseif (\extension_loaded('bcmath')) {
 384                  $integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part);
 385              } else {
 386                  throw new \RuntimeException(
 387                      'The gmp or bcmath extension must be installed to read this database.'
 388                  );
 389              }
 390          }
 391  
 392          return $integerAsString;
 393      }
 394  
 395      /**
 396       * @return array{0:int, 1:int}
 397       */
 398      private function sizeFromCtrlByte(int $ctrlByte, int $offset): array
 399      {
 400          $size = $ctrlByte & 0x1F;
 401  
 402          if ($size < 29) {
 403              return [$size, $offset];
 404          }
 405  
 406          $bytesToRead = $size - 28;
 407          $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
 408  
 409          if ($size === 29) {
 410              $size = 29 + \ord($bytes);
 411          } elseif ($size === 30) {
 412              $rc = unpack('n', $bytes);
 413              if ($rc === false) {
 414                  throw new InvalidDatabaseException(
 415                      'Could not unpack an unsigned short value from the given bytes.'
 416                  );
 417              }
 418              [, $adjust] = $rc;
 419              $size = 285 + $adjust;
 420          } else {
 421              $rc = unpack('N', "\x00" . $bytes);
 422              if ($rc === false) {
 423                  throw new InvalidDatabaseException(
 424                      'Could not unpack an unsigned long value from the given bytes.'
 425                  );
 426              }
 427              [, $adjust] = $rc;
 428              $size = $adjust + 65821;
 429          }
 430  
 431          return [$size, $offset + $bytesToRead];
 432      }
 433  
 434      private function maybeSwitchByteOrder(string $bytes): string
 435      {
 436          return $this->switchByteOrder ? strrev($bytes) : $bytes;
 437      }
 438  
 439      private function isPlatformLittleEndian(): bool
 440      {
 441          $testint = 0x00FF;
 442          $packed = pack('S', $testint);
 443          $rc = unpack('v', $packed);
 444          if ($rc === false) {
 445              throw new InvalidDatabaseException(
 446                  'Could not unpack an unsigned short value from the given bytes.'
 447              );
 448          }
 449  
 450          return $testint === current($rc);
 451      }
 452  }


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