Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

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