1: <?php
2:
3: namespace MaxMind\Db\Reader;
4:
5: class Decoder
6: {
7: private $fileStream;
8: private $pointerBase;
9:
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:
55:
56:
57: if ($type === 'pointer') {
58: list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
59:
60:
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:
166: list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
167:
168: return $double;
169: }
170:
171: private function decodeFloat($bits)
172: {
173:
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:
248: $twoTo32 = '4294967296';
249:
250: foreach ($unpacked as $part) {
251:
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:
271:
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: