Completed
Push — add/gdpr-ads-compliance ( 47ea51...6d1e7f )
by
unknown
09:08
created

Decoder::decodeBigUint()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 6
nop 2
dl 0
loc 35
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace MaxMind\Db\Reader;
4
5
class Decoder
6
{
7
    private $fileStream;
8
    private $pointerBase;
9
    // This is only used for unit testing
10
    private $pointerTestHack;
11
    private $switchByteOrder;
12
13
    private $types = [
14
        0 => 'extended',
15
        1 => 'pointer',
16
        2 => 'utf8_string',
17
        3 => 'double',
18
        4 => 'bytes',
19
        5 => 'uint16',
20
        6 => 'uint32',
21
        7 => 'map',
22
        8 => 'int32',
23
        9 => 'uint64',
24
        10 => 'uint128',
25
        11 => 'array',
26
        12 => 'container',
27
        13 => 'end_marker',
28
        14 => 'boolean',
29
        15 => 'float',
30
    ];
31
32
    public function __construct(
33
        $fileStream,
34
        $pointerBase = 0,
35
        $pointerTestHack = false
36
    ) {
37
        $this->fileStream = $fileStream;
38
        $this->pointerBase = $pointerBase;
39
        $this->pointerTestHack = $pointerTestHack;
40
41
        $this->switchByteOrder = $this->isPlatformLittleEndian();
42
    }
43
44
    public function decode($offset)
45
    {
46
        list(, $ctrlByte) = unpack(
47
            'C',
48
            Util::read($this->fileStream, $offset, 1)
49
        );
50
        $offset++;
51
52
        $type = $this->types[$ctrlByte >> 5];
53
54
        // Pointers are a special case, we don't read the next $size bytes, we
55
        // use the size to determine the length of the pointer and then follow
56
        // it.
57
        if ($type === 'pointer') {
58
            list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
59
60
            // for unit testing
61
            if ($this->pointerTestHack) {
62
                return [$pointer];
63
            }
64
65
            list($result) = $this->decode($pointer);
66
67
            return [$result, $offset];
68
        }
69
70
        if ($type === 'extended') {
71
            list(, $nextByte) = unpack(
72
                'C',
73
                Util::read($this->fileStream, $offset, 1)
74
            );
75
76
            $typeNum = $nextByte + 7;
77
78
            if ($typeNum < 8) {
79
                throw new InvalidDatabaseException(
80
                    'Something went horribly wrong in the decoder. An extended type '
81
                    . 'resolved to a type number < 8 ('
82
                    . $this->types[$typeNum]
83
                    . ')'
84
                );
85
            }
86
87
            $type = $this->types[$typeNum];
88
            $offset++;
89
        }
90
91
        list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset);
92
93
        return $this->decodeByType($type, $offset, $size);
94
    }
95
96
    private function decodeByType($type, $offset, $size)
97
    {
98
        switch ($type) {
99
            case 'map':
100
                return $this->decodeMap($size, $offset);
101
            case 'array':
102
                return $this->decodeArray($size, $offset);
103
            case 'boolean':
104
                return [$this->decodeBoolean($size), $offset];
105
        }
106
107
        $newOffset = $offset + $size;
108
        $bytes = Util::read($this->fileStream, $offset, $size);
109
        switch ($type) {
110
            case 'utf8_string':
111
                return [$this->decodeString($bytes), $newOffset];
112
            case 'double':
113
                $this->verifySize(8, $size);
114
115
                return [$this->decodeDouble($bytes), $newOffset];
116
            case 'float':
117
                $this->verifySize(4, $size);
118
119
                return [$this->decodeFloat($bytes), $newOffset];
120
            case 'bytes':
121
                return [$bytes, $newOffset];
122
            case 'uint16':
123
            case 'uint32':
124
                return [$this->decodeUint($bytes), $newOffset];
125
            case 'int32':
126
                return [$this->decodeInt32($bytes), $newOffset];
127
            case 'uint64':
128
            case 'uint128':
129
                return [$this->decodeBigUint($bytes, $size), $newOffset];
130
            default:
131
                throw new InvalidDatabaseException(
132
                    'Unknown or unexpected type: ' . $type
133
                );
134
        }
135
    }
136
137
    private function verifySize($expected, $actual)
138
    {
139
        if ($expected !== $actual) {
140
            throw new InvalidDatabaseException(
141
                "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
142
            );
143
        }
144
    }
145
146
    private function decodeArray($size, $offset)
147
    {
148
        $array = [];
149
150
        for ($i = 0; $i < $size; $i++) {
151
            list($value, $offset) = $this->decode($offset);
152
            array_push($array, $value);
153
        }
154
155
        return [$array, $offset];
156
    }
157
158
    private function decodeBoolean($size)
159
    {
160
        return $size === 0 ? false : true;
161
    }
162
163
    private function decodeDouble($bits)
164
    {
165
        // XXX - Assumes IEEE 754 double on platform
166
        list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
167
168
        return $double;
169
    }
170
171
    private function decodeFloat($bits)
172
    {
173
        // XXX - Assumes IEEE 754 floats on platform
174
        list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits));
175
176
        return $float;
177
    }
178
179
    private function decodeInt32($bytes)
180
    {
181
        $bytes = $this->zeroPadLeft($bytes, 4);
182
        list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes));
183
184
        return $int;
185
    }
186
187
    private function decodeMap($size, $offset)
188
    {
189
        $map = [];
190
191
        for ($i = 0; $i < $size; $i++) {
192
            list($key, $offset) = $this->decode($offset);
193
            list($value, $offset) = $this->decode($offset);
194
            $map[$key] = $value;
195
        }
196
197
        return [$map, $offset];
198
    }
199
200
    private $pointerValueOffset = [
201
        1 => 0,
202
        2 => 2048,
203
        3 => 526336,
204
        4 => 0,
205
    ];
206
207
    private function decodePointer($ctrlByte, $offset)
208
    {
209
        $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
210
211
        $buffer = Util::read($this->fileStream, $offset, $pointerSize);
212
        $offset = $offset + $pointerSize;
213
214
        $packed = $pointerSize === 4
215
            ? $buffer
216
            : (pack('C', $ctrlByte & 0x7)) . $buffer;
217
218
        $unpacked = $this->decodeUint($packed);
219
        $pointer = $unpacked + $this->pointerBase
220
            + $this->pointerValueOffset[$pointerSize];
221
222
        return [$pointer, $offset];
223
    }
224
225
    private function decodeUint($bytes)
226
    {
227
        list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4));
228
229
        return $int;
230
    }
231
232
    private function decodeBigUint($bytes, $byteLength)
233
    {
234
        $maxUintBytes = log(PHP_INT_MAX, 2) / 8;
235
236
        if ($byteLength === 0) {
237
            return 0;
238
        }
239
240
        $numberOfLongs = ceil($byteLength / 4);
241
        $paddedLength = $numberOfLongs * 4;
242
        $paddedBytes = $this->zeroPadLeft($bytes, $paddedLength);
243
        $unpacked = array_merge(unpack("N$numberOfLongs", $paddedBytes));
244
245
        $integer = 0;
246
247
        // 2^32
248
        $twoTo32 = '4294967296';
249
250
        foreach ($unpacked as $part) {
251
            // We only use gmp or bcmath if the final value is too big
252
            if ($byteLength <= $maxUintBytes) {
253
                $integer = ($integer << 32) + $part;
254
            } elseif (extension_loaded('gmp')) {
255
                $integer = gmp_strval(gmp_add(gmp_mul($integer, $twoTo32), $part));
256
            } elseif (extension_loaded('bcmath')) {
257
                $integer = bcadd(bcmul($integer, $twoTo32), $part);
258
            } else {
259
                throw new \RuntimeException(
260
                    'The gmp or bcmath extension must be installed to read this database.'
261
                );
262
            }
263
        }
264
265
        return $integer;
266
    }
267
268
    private function decodeString($bytes)
269
    {
270
        // XXX - NOOP. As far as I know, the end user has to explicitly set the
271
        // encoding in PHP. Strings are just bytes.
272
        return $bytes;
273
    }
274
275
    private function sizeFromCtrlByte($ctrlByte, $offset)
276
    {
277
        $size = $ctrlByte & 0x1f;
278
        $bytesToRead = $size < 29 ? 0 : $size - 28;
279
        $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
280
        $decoded = $this->decodeUint($bytes);
281
282
        if ($size === 29) {
283
            $size = 29 + $decoded;
284
        } elseif ($size === 30) {
285
            $size = 285 + $decoded;
286
        } elseif ($size > 30) {
287
            $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
288
                + 65821;
289
        }
290
291
        return [$size, $offset + $bytesToRead];
292
    }
293
294
    private function zeroPadLeft($content, $desiredLength)
295
    {
296
        return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT);
297
    }
298
299
    private function maybeSwitchByteOrder($bytes)
300
    {
301
        return $this->switchByteOrder ? strrev($bytes) : $bytes;
302
    }
303
304
    private function isPlatformLittleEndian()
305
    {
306
        $testint = 0x00FF;
307
        $packed = pack('S', $testint);
308
309
        return $testint === current(unpack('v', $packed));
310
    }
311
}
312