Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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', (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              case self::_ARRAY:
 127                  return $this->decodeArray($size, $offset);
 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          switch ($type) {
 135              case self::_BYTES:
 136              case self::_UTF8_STRING:
 137                  return [$bytes, $newOffset];
 138              case self::_DOUBLE:
 139                  $this->verifySize(8, $size);
 140  
 141                  return [$this->decodeDouble($bytes), $newOffset];
 142              case self::_FLOAT:
 143                  $this->verifySize(4, $size);
 144  
 145                  return [$this->decodeFloat($bytes), $newOffset];
 146              case self::_INT32:
 147                  return [$this->decodeInt32($bytes, $size), $newOffset];
 148              case self::_UINT16:
 149              case self::_UINT32:
 150              case self::_UINT64:
 151              case self::_UINT128:
 152                  return [$this->decodeUint($bytes, $size), $newOffset];
 153              default:
 154                  throw new InvalidDatabaseException(
 155                      'Unknown or unexpected type: ' . $type
 156                  );
 157          }
 158      }
 159  
 160      private function verifySize(int $expected, int $actual): void
 161      {
 162          if ($expected !== $actual) {
 163              throw new InvalidDatabaseException(
 164                  "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
 165              );
 166          }
 167      }
 168  
 169      private function decodeArray(int $size, int $offset): array
 170      {
 171          $array = [];
 172  
 173          for ($i = 0; $i < $size; ++$i) {
 174              [$value, $offset] = $this->decode($offset);
 175              $array[] = $value;
 176          }
 177  
 178          return [$array, $offset];
 179      }
 180  
 181      private function decodeBoolean(int $size): bool
 182      {
 183          return $size !== 0;
 184      }
 185  
 186      private function decodeDouble(string $bytes): float
 187      {
 188          // This assumes IEEE 754 doubles, but most (all?) modern platforms
 189          // use them.
 190          [, $double] = unpack('E', $bytes);
 191  
 192          return $double;
 193      }
 194  
 195      private function decodeFloat(string $bytes): float
 196      {
 197          // This assumes IEEE 754 floats, but most (all?) modern platforms
 198          // use them.
 199          [, $float] = unpack('G', $bytes);
 200  
 201          return $float;
 202      }
 203  
 204      private function decodeInt32(string $bytes, int $size): int
 205      {
 206          switch ($size) {
 207              case 0:
 208                  return 0;
 209              case 1:
 210              case 2:
 211              case 3:
 212                  $bytes = str_pad($bytes, 4, "\x00", STR_PAD_LEFT);
 213                  break;
 214              case 4:
 215                  break;
 216              default:
 217                  throw new InvalidDatabaseException(
 218                      "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
 219                  );
 220          }
 221  
 222          [, $int] = unpack('l', $this->maybeSwitchByteOrder($bytes));
 223  
 224          return $int;
 225      }
 226  
 227      private function decodeMap(int $size, int $offset): array
 228      {
 229          $map = [];
 230  
 231          for ($i = 0; $i < $size; ++$i) {
 232              [$key, $offset] = $this->decode($offset);
 233              [$value, $offset] = $this->decode($offset);
 234              $map[$key] = $value;
 235          }
 236  
 237          return [$map, $offset];
 238      }
 239  
 240      private function decodePointer(int $ctrlByte, int $offset): array
 241      {
 242          $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
 243  
 244          $buffer = Util::read($this->fileStream, $offset, $pointerSize);
 245          $offset = $offset + $pointerSize;
 246  
 247          switch ($pointerSize) {
 248              case 1:
 249                  $packed = \chr($ctrlByte & 0x7) . $buffer;
 250                  [, $pointer] = unpack('n', $packed);
 251                  $pointer += $this->pointerBase;
 252                  break;
 253              case 2:
 254                  $packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
 255                  [, $pointer] = unpack('N', $packed);
 256                  $pointer += $this->pointerBase + 2048;
 257                  break;
 258              case 3:
 259                  $packed = \chr($ctrlByte & 0x7) . $buffer;
 260  
 261                  // It is safe to use 'N' here, even on 32 bit machines as the
 262                  // first bit is 0.
 263                  [, $pointer] = unpack('N', $packed);
 264                  $pointer += $this->pointerBase + 526336;
 265                  break;
 266              case 4:
 267                  // We cannot use unpack here as we might overflow on 32 bit
 268                  // machines
 269                  $pointerOffset = $this->decodeUint($buffer, $pointerSize);
 270  
 271                  $byteLength = $pointerSize + $this->pointerBaseByteSize;
 272  
 273                  if ($byteLength <= _MM_MAX_INT_BYTES) {
 274                      $pointer = $pointerOffset + $this->pointerBase;
 275                  } elseif (\extension_loaded('gmp')) {
 276                      $pointer = gmp_strval(gmp_add($pointerOffset, $this->pointerBase));
 277                  } elseif (\extension_loaded('bcmath')) {
 278                      $pointer = bcadd($pointerOffset, (string) $this->pointerBase);
 279                  } else {
 280                      throw new RuntimeException(
 281                          'The gmp or bcmath extension must be installed to read this database.'
 282                      );
 283                  }
 284                  break;
 285              default:
 286                  throw new InvalidDatabaseException(
 287                      'Unexpected pointer size ' . $pointerSize
 288                  );
 289          }
 290  
 291          return [$pointer, $offset];
 292      }
 293  
 294      private function decodeUint(string $bytes, int $byteLength)
 295      {
 296          if ($byteLength === 0) {
 297              return 0;
 298          }
 299  
 300          $integer = 0;
 301  
 302          for ($i = 0; $i < $byteLength; ++$i) {
 303              $part = \ord($bytes[$i]);
 304  
 305              // We only use gmp or bcmath if the final value is too big
 306              if ($byteLength <= _MM_MAX_INT_BYTES) {
 307                  $integer = ($integer << 8) + $part;
 308              } elseif (\extension_loaded('gmp')) {
 309                  $integer = gmp_strval(gmp_add(gmp_mul((string) $integer, '256'), $part));
 310              } elseif (\extension_loaded('bcmath')) {
 311                  $integer = bcadd(bcmul((string) $integer, '256'), (string) $part);
 312              } else {
 313                  throw new RuntimeException(
 314                      'The gmp or bcmath extension must be installed to read this database.'
 315                  );
 316              }
 317          }
 318  
 319          return $integer;
 320      }
 321  
 322      private function sizeFromCtrlByte(int $ctrlByte, int $offset): array
 323      {
 324          $size = $ctrlByte & 0x1f;
 325  
 326          if ($size < 29) {
 327              return [$size, $offset];
 328          }
 329  
 330          $bytesToRead = $size - 28;
 331          $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
 332  
 333          if ($size === 29) {
 334              $size = 29 + \ord($bytes);
 335          } elseif ($size === 30) {
 336              [, $adjust] = unpack('n', $bytes);
 337              $size = 285 + $adjust;
 338          } else {
 339              [, $adjust] = unpack('N', "\x00" . $bytes);
 340              $size = $adjust + 65821;
 341          }
 342  
 343          return [$size, $offset + $bytesToRead];
 344      }
 345  
 346      private function maybeSwitchByteOrder(string $bytes): string
 347      {
 348          return $this->switchByteOrder ? strrev($bytes) : $bytes;
 349      }
 350  
 351      private function isPlatformLittleEndian(): bool
 352      {
 353          $testint = 0x00FF;
 354          $packed = pack('S', $testint);
 355  
 356          return $testint === current(unpack('v', $packed));
 357      }
 358  }