/home/edulekha/www/wp-content/plugins/wp-slimstat/src/Utils/MaxMindDecoder.php
<?php

namespace SlimStat\Utils;

class MaxMindDecoder
{
    private $fileStream;

    private $pointerBase;

    // This is only used for unit testing
    private $pointerTestHack;

    private $switchByteOrder;

    private $types = [
        0  => 'extended',
        1  => 'pointer',
        2  => 'utf8_string',
        3  => 'double',
        4  => 'bytes',
        5  => 'uint16',
        6  => 'uint32',
        7  => 'map',
        8  => 'int32',
        9  => 'uint64',
        10 => 'uint128',
        11 => 'array',
        12 => 'container',
        13 => 'end_marker',
        14 => 'boolean',
        15 => 'float',
    ];

    public function __construct($fileStream, $pointerBase = 0, $pointerTestHack = false)
    {
        $this->fileStream      = $fileStream;
        $this->pointerBase     = $pointerBase;
        $this->pointerTestHack = $pointerTestHack;

        $this->switchByteOrder = $this->isPlatformLittleEndian();
    }

    /**
     * @throws InvalidDatabaseException
     */
    public function decode($offset)
    {
        [, $ctrlByte] = unpack(
            'C',
            MaxMindUtil::read($this->fileStream, $offset, 1)
        );
        $offset++;

        $type = $this->types[$ctrlByte >> 5];

        // Pointers are a special case, we don't read the next $size bytes, we
        // use the size to determine the length of the pointer and then follow
        // it.
        if ('pointer' == $type) {
            [$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);

            // for unit testing
            if ($this->pointerTestHack) {
                return [$pointer];
            }

            [$result] = $this->decode($pointer);

            return [$result, $offset];
        }

        if ('extended' == $type) {
            [, $nextByte] = unpack(
                'C',
                MaxMindUtil::read($this->fileStream, $offset, 1)
            );

            $typeNum = $nextByte + 7;

            if ($typeNum < 8) {
                throw new InvalidDatabaseException(
                    'Something went horribly wrong in the decoder. An extended type '
                    . 'resolved to a type number < 8 ('
                    . $this->types[$typeNum]
                    . ')'
                );
            }

            $type = $this->types[$typeNum];
            $offset++;
        }

        [$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);

        return $this->decodeByType($type, $offset, $size);
    }

    /**
     * @throws InvalidDatabaseException
     */
    private function decodeByType($type, $offset, $size): array
    {
        switch ($type) {
            case 'map':
                return $this->decodeMap($size, $offset);
            case 'array':
                return $this->decodeArray($size, $offset);
            case 'boolean':
                return [$this->decodeBoolean($size), $offset];
        }

        $newOffset = $offset + $size;
        $bytes     = MaxMindUtil::read($this->fileStream, $offset, $size);
        switch ($type) {
            case 'utf8_string':
                return [$this->decodeString($bytes), $newOffset];
            case 'double':
                $this->verifySize(8, $size);
                return [$this->decodeDouble($bytes), $newOffset];
            case 'float':
                $this->verifySize(4, $size);
                return [$this->decodeFloat($bytes), $newOffset];
            case 'bytes':
                return [$bytes, $newOffset];
            case 'uint16':
            case 'uint32':
                return [$this->decodeUint($bytes), $newOffset];
            case 'int32':
                return [$this->decodeInt32($bytes), $newOffset];
            case 'uint64':
            case 'uint128':
                return [$this->decodeBigUint($bytes, $size), $newOffset];
            default:
                throw new InvalidDatabaseException(
                    'Unknown or unexpected type: ' . $type
                );
        }
    }

    /**
     * @throws InvalidDatabaseException
     */
    private function verifySize($expected, $actual)
    {
        if ($expected != $actual) {
            throw new InvalidDatabaseException(
                "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
            );
        }
    }

    /**
     * @throws InvalidDatabaseException
     */
    private function decodeArray($size, $offset): array
    {
        $array = [];

        for ($i = 0; $i < $size; $i++) {
            [$value, $offset] = $this->decode($offset);
            $array[]          = $value;
        }

        return [$array, $offset];
    }

    private function decodeBoolean($size): bool
    {
        return 0 != $size;
    }

    private function decodeDouble($bits)
    {
        // XXX - Assumes IEEE 754 double on platform
        [, $double] = unpack('d', $this->maybeSwitchByteOrder($bits));
        return $double;
    }

    private function decodeFloat($bits)
    {
        // XXX - Assumes IEEE 754 floats on platform
        [, $float] = unpack('f', $this->maybeSwitchByteOrder($bits));
        return $float;
    }

    private function decodeInt32($bytes)
    {
        $bytes   = $this->zeroPadLeft($bytes, 4);
        [, $int] = unpack('l', $this->maybeSwitchByteOrder($bytes));
        return $int;
    }

    private function decodeMap($size, $offset): array
    {

        $map = [];

        for ($i = 0; $i < $size; $i++) {
            [$key, $offset]   = $this->decode($offset);
            [$value, $offset] = $this->decode($offset);
            $map[$key]        = $value;
        }

        return [$map, $offset];
    }

    private $pointerValueOffset = [
        1 => 0,
        2 => 2048,
        3 => 526336,
        4 => 0,
    ];

    /**
     * @throws InvalidDatabaseException
     */
    private function decodePointer($ctrlByte, $offset): array
    {
        $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;

        $buffer = MaxMindUtil::read($this->fileStream, $offset, $pointerSize);
        $offset += $pointerSize;

        $packed = 4 == $pointerSize
            ? $buffer
            : (pack('C', $ctrlByte & 0x7)) . $buffer;

        $unpacked = $this->decodeUint($packed);
        $pointer  = $unpacked + $this->pointerBase
            + $this->pointerValueOffset[$pointerSize];

        return [$pointer, $offset];
    }

    private function decodeUint($bytes)
    {
        [, $int] = unpack('N', $this->zeroPadLeft($bytes, 4));
        return $int;
    }

    private function decodeBigUint($bytes, $byteLength)
    {
        $maxUintBytes = log(PHP_INT_MAX, 2) / 8;

        if (0 == $byteLength) {
            return 0;
        }

        $numberOfLongs = ceil($byteLength / 4);
        $paddedLength  = $numberOfLongs * 4;
        $paddedBytes   = $this->zeroPadLeft($bytes, $paddedLength);
        $unpacked      = array_merge(unpack('N' . $numberOfLongs, $paddedBytes));

        $integer = 0;

        // 2^32
        $twoTo32 = '4294967296';

        foreach ($unpacked as $part) {
            // We only use gmp or bcmath if the final value is too big
            if ($byteLength <= $maxUintBytes) {
                $integer = ($integer << 32) + $part;
            } elseif (extension_loaded('gmp')) {
                $integer = gmp_strval(gmp_add(gmp_mul($integer, $twoTo32), $part));
            } elseif (extension_loaded('bcmath')) {
                $integer = bcadd(bcmul($integer, $twoTo32), $part);
            } else {
                throw new \RuntimeException(
                    'The gmp or bcmath extension must be installed to read this database.'
                );
            }
        }

        return $integer;
    }

    private function decodeString($bytes)
    {
        // XXX - NOOP. As far as I know, the end user has to explicitly set the
        // encoding in PHP. Strings are just bytes.
        return $bytes;
    }

    private function sizeFromCtrlByte($ctrlByte, $offset): array
    {
        $size        = $ctrlByte & 0x1f;
        $bytesToRead = $size < 29 ? 0 : $size - 28;
        $bytes       = MaxMindUtil::read($this->fileStream, $offset, $bytesToRead);
        $decoded     = $this->decodeUint($bytes);

        if (29 == $size) {
            $size = 29 + $decoded;
        } elseif (30 == $size) {
            $size = 285 + $decoded;
        } elseif ($size > 30) {
            $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
                + 65821;
        }

        return [$size, $offset + $bytesToRead];
    }

    private function zeroPadLeft($content, $desiredLength): string
    {
        return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT);
    }

    private function maybeSwitchByteOrder($bytes)
    {
        return $this->switchByteOrder ? strrev($bytes) : $bytes;
    }

    private function isPlatformLittleEndian(): bool
    {
        $testint = 0x00FF;
        $packed  = pack('S', $testint);
        return $testint === current(unpack('v', $packed));
    }
}