MDL-48766 lib: Import MaxMind GeoIP2 PHP API
[moodle.git] / lib / maxmind / MaxMind / Db / Reader / Decoder.php
1 <?php
3 namespace MaxMind\Db\Reader;
5 use MaxMind\Db\Reader\InvalidDatabaseException;
6 use MaxMind\Db\Reader\Util;
8 class Decoder
9 {
11     private $fileStream;
12     private $pointerBase;
13     // This is only used for unit testing
14     private $pointerTestHack;
15     private $switchByteOrder;
17     private $types = array(
18         0 => 'extended',
19         1 => 'pointer',
20         2 => 'utf8_string',
21         3 => 'double',
22         4 => 'bytes',
23         5 => 'uint16',
24         6 => 'uint32',
25         7 => 'map',
26         8 => 'int32',
27         9 => 'uint64',
28         10 => 'uint128',
29         11 => 'array',
30         12 => 'container',
31         13 => 'end_marker',
32         14 => 'boolean',
33         15 => 'float',
34     );
36     public function __construct(
37         $fileStream,
38         $pointerBase = 0,
39         $pointerTestHack = false
40     ) {
41         $this->fileStream = $fileStream;
42         $this->pointerBase = $pointerBase;
43         $this->pointerTestHack = $pointerTestHack;
45         $this->switchByteOrder = $this->isPlatformLittleEndian();
46     }
49     public function decode($offset)
50     {
51         list(, $ctrlByte) = unpack(
52             'C',
53             Util::read($this->fileStream, $offset, 1)
54         );
55         $offset++;
57         $type = $this->types[$ctrlByte >> 5];
59         // Pointers are a special case, we don't read the next $size bytes, we
60         // use the size to determine the length of the pointer and then follow
61         // it.
62         if ($type == 'pointer') {
63             list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
65             // for unit testing
66             if ($this->pointerTestHack) {
67                 return array($pointer);
68             }
70             list($result) = $this->decode($pointer);
72             return array($result, $offset);
73         }
75         if ($type == 'extended') {
76             list(, $nextByte) = unpack(
77                 'C',
78                 Util::read($this->fileStream, $offset, 1)
79             );
81             $typeNum = $nextByte + 7;
83             if ($typeNum < 8) {
84                 throw new InvalidDatabaseException(
85                     "Something went horribly wrong in the decoder. An extended type "
86                     . "resolved to a type number < 8 ("
87                     . $this->types[$typeNum]
88                     . ")"
89                 );
90             }
92             $type = $this->types[$typeNum];
93             $offset++;
94         }
96         list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset);
98         return $this->decodeByType($type, $offset, $size);
99     }
101     private function decodeByType($type, $offset, $size)
102     {
103         switch ($type) {
104             case 'map':
105                 return $this->decodeMap($size, $offset);
106             case 'array':
107                 return $this->decodeArray($size, $offset);
108             case 'boolean':
109                 return array($this->decodeBoolean($size), $offset);
110         }
112         $newOffset = $offset + $size;
113         $bytes = Util::read($this->fileStream, $offset, $size);
114         switch ($type) {
115             case 'utf8_string':
116                 return array($this->decodeString($bytes), $newOffset);
117             case 'double':
118                 $this->verifySize(8, $size);
119                 return array($this->decodeDouble($bytes), $newOffset);
120             case 'float':
121                 $this->verifySize(4, $size);
122                 return array($this->decodeFloat($bytes), $newOffset);
123             case 'bytes':
124                 return array($bytes, $newOffset);
125             case 'uint16':
126             case 'uint32':
127                 return array($this->decodeUint($bytes), $newOffset);
128             case 'int32':
129                 return array($this->decodeInt32($bytes), $newOffset);
130             case 'uint64':
131             case 'uint128':
132                 return array($this->decodeBigUint($bytes, $size), $newOffset);
133             default:
134                 throw new InvalidDatabaseException(
135                     "Unknown or unexpected type: " . $type
136                 );
137         }
138     }
140     private function verifySize($expected, $actual)
141     {
142         if ($expected != $actual) {
143             throw new InvalidDatabaseException(
144                 "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
145             );
146         }
147     }
149     private function decodeArray($size, $offset)
150     {
151         $array = array();
153         for ($i = 0; $i < $size; $i++) {
154             list($value, $offset) = $this->decode($offset);
155             array_push($array, $value);
156         }
158         return array($array, $offset);
159     }
161     private function decodeBoolean($size)
162     {
163         return $size == 0 ? false : true;
164     }
166     private function decodeDouble($bits)
167     {
168         // XXX - Assumes IEEE 754 double on platform
169         list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
170         return $double;
171     }
173     private function decodeFloat($bits)
174     {
175         // XXX - Assumes IEEE 754 floats on platform
176         list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits));
177         return $float;
178     }
180     private function decodeInt32($bytes)
181     {
182         $bytes = $this->zeroPadLeft($bytes, 4);
183         list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes));
184         return $int;
185     }
187     private function decodeMap($size, $offset)
188     {
190         $map = array();
192         for ($i = 0; $i < $size; $i++) {
193             list($key, $offset) = $this->decode($offset);
194             list($value, $offset) = $this->decode($offset);
195             $map[$key] = $value;
196         }
198         return array($map, $offset);
199     }
201     private $pointerValueOffset = array(
202         1 => 0,
203         2 => 2048,
204         3 => 526336,
205         4 => 0,
206     );
208     private function decodePointer($ctrlByte, $offset)
209     {
210         $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
212         $buffer = Util::read($this->fileStream, $offset, $pointerSize);
213         $offset = $offset + $pointerSize;
215         $packed = $pointerSize == 4
216             ? $buffer
217             : (pack('C', $ctrlByte & 0x7)) . $buffer;
219         $unpacked = $this->decodeUint($packed);
220         $pointer = $unpacked + $this->pointerBase
221             + $this->pointerValueOffset[$pointerSize];
223         return array($pointer, $offset);
224     }
226     private function decodeUint($bytes)
227     {
228         list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4));
229         return $int;
230     }
232     private function decodeBigUint($bytes, $byteLength)
233     {
234         $maxUintBytes = log(PHP_INT_MAX, 2) / 8;
236         if ($byteLength == 0) {
237             return 0;
238         }
240         $numberOfLongs = ceil($byteLength / 4);
241         $paddedLength = $numberOfLongs * 4;
242         $paddedBytes = $this->zeroPadLeft($bytes, $paddedLength);
243         $unpacked = array_merge(unpack("N$numberOfLongs", $paddedBytes));
245         $integer = 0;
247         // 2^32
248         $twoTo32 = '4294967296';
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         return $integer;
265     }
267     private function decodeString($bytes)
268     {
269         // XXX - NOOP. As far as I know, the end user has to explicitly set the
270         // encoding in PHP. Strings are just bytes.
271         return $bytes;
272     }
274     private function sizeFromCtrlByte($ctrlByte, $offset)
275     {
276         $size = $ctrlByte & 0x1f;
277         $bytesToRead = $size < 29 ? 0 : $size - 28;
278         $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
279         $decoded = $this->decodeUint($bytes);
281         if ($size == 29) {
282             $size = 29 + $decoded;
283         } elseif ($size == 30) {
284             $size = 285 + $decoded;
285         } elseif ($size > 30) {
286             $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
287                 + 65821;
288         }
290         return array($size, $offset + $bytesToRead);
291     }
293     private function zeroPadLeft($content, $desiredLength)
294     {
295         return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT);
296     }
298     private function maybeSwitchByteOrder($bytes)
299     {
300         return $this->switchByteOrder ? strrev($bytes) : $bytes;
301     }
303     private function isPlatformLittleEndian()
304     {
305         $testint = 0x00FF;
306         $packed = pack('S', $testint);
307         return $testint === current(unpack('v', $packed));
308     }