PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / extra / maxmind-db / reader / src / MaxMind / Db / Reader / Decoder.php

Decoder.php in Defender Security – Malware Scanner, Login Security & Firewall 5.5.1, at src/extra/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php

357 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare( strict_types=1 );
4
5 namespace MaxMind\Db\Reader;
6
7 // @codingStandardsIgnoreLine
8
9 class Decoder {
10 /**
11 * @var resource
12 */
13 private $fileStream;
14
15 /**
16 * @var int
17 */
18 private $pointerBase;
19
20 /**
21 * This is only used for unit testing.
22 *
23 * @var bool
24 */
25 private $pointerTestHack;
26
27 /**
28 * @var bool
29 */
30 private $switchByteOrder;
31
32 private const _EXTENDED = 0;
33 private const _POINTER = 1;
34 private const _UTF8_STRING = 2;
35 private const _DOUBLE = 3;
36 private const _BYTES = 4;
37 private const _UINT16 = 5;
38 private const _UINT32 = 6;
39 private const _MAP = 7;
40 private const _INT32 = 8;
41 private const _UINT64 = 9;
42 private const _UINT128 = 10;
43 private const _ARRAY = 11;
44 // 12 is the container type
45 // 13 is the end marker type
46 private const _BOOLEAN = 14;
47 private const _FLOAT = 15;
48
49 /**
50 * @param resource $fileStream
51 */
52 public function __construct(
53 $fileStream,
54 int $pointerBase = 0,
55 bool $pointerTestHack = false
56 ) {
57 $this->fileStream = $fileStream;
58 $this->pointerBase = $pointerBase;
59
60 $this->pointerTestHack = $pointerTestHack;
61
62 $this->switchByteOrder = $this->isPlatformLittleEndian();
63 }
64
65 public function decode( int $offset ): array {
66 $ctrlByte = \ord( Util::read( $this->fileStream, $offset, 1 ) );
67 ++ $offset;
68
69 $type = $ctrlByte >> 5;
70
71 // Pointers are a special case, we don't read the next $size bytes, we
72 // use the size to determine the length of the pointer and then follow
73 // it.
74 if ( $type === self::_POINTER ) {
75 [ $pointer, $offset ] = $this->decodePointer( $ctrlByte, $offset );
76
77 // for unit testing
78 if ( $this->pointerTestHack ) {
79 return [ $pointer ];
80 }
81
82 [ $result ] = $this->decode( $pointer );
83
84 return [ $result, $offset ];
85 }
86
87 if ( $type === self::_EXTENDED ) {
88 $nextByte = \ord( Util::read( $this->fileStream, $offset, 1 ) );
89
90 $type = $nextByte + 7;
91
92 if ( $type < 8 ) {
93 throw new InvalidDatabaseException(
94 'Something went horribly wrong in the decoder. An extended type '
95 . 'resolved to a type number < 8 ('
96 . $type
97 . ')'
98 );
99 }
100
101 ++ $offset;
102 }
103
104 [ $size, $offset ] = $this->sizeFromCtrlByte( $ctrlByte, $offset );
105
106 return $this->decodeByType( $type, $offset, $size );
107 }
108
109 private function decodeByType( int $type, int $offset, int $size ): array {
110 switch ( $type ) {
111 case self::_MAP:
112 return $this->decodeMap( $size, $offset );
113
114 case self::_ARRAY:
115 return $this->decodeArray( $size, $offset );
116
117 case self::_BOOLEAN:
118 return [ $this->decodeBoolean( $size ), $offset ];
119 }
120
121 $newOffset = $offset + $size;
122 $bytes = Util::read( $this->fileStream, $offset, $size );
123
124 switch ( $type ) {
125 case self::_BYTES:
126 case self::_UTF8_STRING:
127 return [ $bytes, $newOffset ];
128
129 case self::_DOUBLE:
130 $this->verifySize( 8, $size );
131
132 return [ $this->decodeDouble( $bytes ), $newOffset ];
133
134 case self::_FLOAT:
135 $this->verifySize( 4, $size );
136
137 return [ $this->decodeFloat( $bytes ), $newOffset ];
138
139 case self::_INT32:
140 return [ $this->decodeInt32( $bytes, $size ), $newOffset ];
141
142 case self::_UINT16:
143 case self::_UINT32:
144 case self::_UINT64:
145 case self::_UINT128:
146 return [ $this->decodeUint( $bytes, $size ), $newOffset ];
147
148 default:
149 throw new InvalidDatabaseException(
150 'Unknown or unexpected type: ' . $type
151 );
152 }
153 }
154
155 private function verifySize( int $expected, int $actual ): void {
156 if ( $expected !== $actual ) {
157 throw new InvalidDatabaseException(
158 "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
159 );
160 }
161 }
162
163 private function decodeArray( int $size, int $offset ): array {
164 $array = [];
165
166 for ( $i = 0; $i < $size; ++ $i ) {
167 [ $value, $offset ] = $this->decode( $offset );
168 $array[] = $value;
169 }
170
171 return [ $array, $offset ];
172 }
173
174 private function decodeBoolean( int $size ): bool {
175 return $size !== 0;
176 }
177
178 private function decodeDouble( string $bytes ): float {
179 // This assumes IEEE 754 doubles, but most (all?) modern platforms
180 // use them.
181 [ , $double ] = unpack( 'E', $bytes );
182
183 return $double;
184 }
185
186 private function decodeFloat( string $bytes ): float {
187 // This assumes IEEE 754 floats, but most (all?) modern platforms
188 // use them.
189 [ , $float ] = unpack( 'G', $bytes );
190
191 return $float;
192 }
193
194 private function decodeInt32( string $bytes, int $size ): int {
195 switch ( $size ) {
196 case 0:
197 return 0;
198
199 case 1:
200 case 2:
201 case 3:
202 $bytes = str_pad( $bytes, 4, "\x00", \STR_PAD_LEFT );
203
204 break;
205
206 case 4:
207 break;
208
209 default:
210 throw new InvalidDatabaseException(
211 "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
212 );
213 }
214
215 [ , $int ] = unpack( 'l', $this->maybeSwitchByteOrder( $bytes ) );
216
217 return $int;
218 }
219
220 private function decodeMap( int $size, int $offset ): array {
221 $map = [];
222
223 for ( $i = 0; $i < $size; ++ $i ) {
224 [ $key, $offset ] = $this->decode( $offset );
225 [ $value, $offset ] = $this->decode( $offset );
226 $map[ $key ] = $value;
227 }
228
229 return [ $map, $offset ];
230 }
231
232 private function decodePointer( int $ctrlByte, int $offset ): array {
233 $pointerSize = ( ( $ctrlByte >> 3 ) & 0x3 ) + 1;
234
235 $buffer = Util::read( $this->fileStream, $offset, $pointerSize );
236 $offset = $offset + $pointerSize;
237
238 switch ( $pointerSize ) {
239 case 1:
240 $packed = \chr( $ctrlByte & 0x7 ) . $buffer;
241 [ , $pointer ] = unpack( 'n', $packed );
242 $pointer += $this->pointerBase;
243
244 break;
245
246 case 2:
247 $packed = "\x00" . \chr( $ctrlByte & 0x7 ) . $buffer;
248 [ , $pointer ] = unpack( 'N', $packed );
249 $pointer += $this->pointerBase + 2048;
250
251 break;
252
253 case 3:
254 $packed = \chr( $ctrlByte & 0x7 ) . $buffer;
255
256 // It is safe to use 'N' here, even on 32 bit machines as the
257 // first bit is 0.
258 [ , $pointer ] = unpack( 'N', $packed );
259 $pointer += $this->pointerBase + 526336;
260
261 break;
262
263 case 4:
264 // We cannot use unpack here as we might overflow on 32 bit
265 // machines
266 $pointerOffset = $this->decodeUint( $buffer, $pointerSize );
267
268 $pointerBase = $this->pointerBase;
269
270 if ( \PHP_INT_MAX - $pointerBase >= $pointerOffset ) {
271 $pointer = $pointerOffset + $pointerBase;
272 } else {
273 throw new \RuntimeException(
274 'The database offset is too large to be represented on your platform.'
275 );
276 }
277
278 break;
279
280 default:
281 throw new InvalidDatabaseException(
282 'Unexpected pointer size ' . $pointerSize
283 );
284 }
285
286 return [ $pointer, $offset ];
287 }
288
289 // @phpstan-ignore-next-line
290 private function decodeUint( string $bytes, int $byteLength ) {
291 if ( $byteLength === 0 ) {
292 return 0;
293 }
294
295 $integer = 0;
296
297 // PHP integers are signed. PHP_INT_SIZE - 1 is the number of
298 // complete bytes that can be converted to an integer. However,
299 // we can convert another byte if the leading bit is zero.
300 $useRealInts = $byteLength <= \PHP_INT_SIZE - 1
301 || ( $byteLength === \PHP_INT_SIZE && ( \ord( $bytes[0] ) & 0x80 ) === 0 );
302
303 for ( $i = 0; $i < $byteLength; ++ $i ) {
304 $part = \ord( $bytes[ $i ] );
305
306 // We only use gmp or bcmath if the final value is too big
307 if ( $useRealInts ) {
308 $integer = ( $integer << 8 ) + $part;
309 } elseif ( \extension_loaded( 'gmp' ) ) {
310 $integer = gmp_strval( gmp_add( gmp_mul( (string) $integer, '256' ), $part ) );
311 } elseif ( \extension_loaded( 'bcmath' ) ) {
312 $integer = bcadd( bcmul( (string) $integer, '256' ), (string) $part );
313 } else {
314 throw new \RuntimeException(
315 'The gmp or bcmath extension must be installed to read this database.'
316 );
317 }
318 }
319
320 return $integer;
321 }
322
323 private function sizeFromCtrlByte( int $ctrlByte, int $offset ): array {
324 $size = $ctrlByte & 0x1F;
325
326 if ( $size < 29 ) {
327 return [ $size, $offset ];
328 }
329
330 $bytesToRead = $size - 28;
331 $bytes = Util::read( $this->fileStream, $offset, $bytesToRead );
332
333 if ( $size === 29 ) {
334 $size = 29 + \ord( $bytes );
335 } elseif ( $size === 30 ) {
336 [ , $adjust ] = unpack( 'n', $bytes );
337 $size = 285 + $adjust;
338 } else {
339 [ , $adjust ] = unpack( 'N', "\x00" . $bytes );
340 $size = $adjust + 65821;
341 }
342
343 return [ $size, $offset + $bytesToRead ];
344 }
345
346 private function maybeSwitchByteOrder( string $bytes ): string {
347 return $this->switchByteOrder ? strrev( $bytes ) : $bytes;
348 }
349
350 private function isPlatformLittleEndian(): bool {
351 $testint = 0x00FF;
352 $packed = pack( 'S', $testint );
353
354 return $testint === current( unpack( 'v', $packed ) );
355 }
356 }
357