Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
}