MDL-65763 lib: Upgrade MixMind DB reader lib to 1.4.1
[moodle.git] / lib / maxmind / MaxMind / Db / Reader.php
CommitLineData
eacc36a2
DP
1<?php
2
3namespace MaxMind\Db;
4
5use MaxMind\Db\Reader\Decoder;
6use MaxMind\Db\Reader\InvalidDatabaseException;
7use MaxMind\Db\Reader\Metadata;
8use MaxMind\Db\Reader\Util;
9
10/**
11 * Instances of this class provide a reader for the MaxMind DB format. IP
12 * addresses can be looked up using the <code>get</code> method.
13 */
14class Reader
15{
16 private static $DATA_SECTION_SEPARATOR_SIZE = 16;
17 private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
18 private static $METADATA_START_MARKER_LENGTH = 14;
4e23b6b2 19 private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB
eacc36a2
DP
20
21 private $decoder;
22 private $fileHandle;
23 private $fileSize;
24 private $ipV4Start;
25 private $metadata;
26
27 /**
28 * Constructs a Reader for the MaxMind DB format. The file passed to it must
29 * be a valid MaxMind DB file such as a GeoIp2 database file.
30 *
31 * @param string $database
b55260fc
MG
32 * the MaxMind DB file to use
33 *
34 * @throws \InvalidArgumentException for invalid database path or unknown arguments
eacc36a2 35 * @throws \MaxMind\Db\Reader\InvalidDatabaseException
b55260fc
MG
36 * if the database is invalid or there is an error reading
37 * from it
eacc36a2
DP
38 */
39 public function __construct($database)
40 {
b55260fc 41 if (\func_num_args() !== 1) {
eacc36a2
DP
42 throw new \InvalidArgumentException(
43 'The constructor takes exactly one argument.'
44 );
45 }
46
47 if (!is_readable($database)) {
48 throw new \InvalidArgumentException(
49 "The file \"$database\" does not exist or is not readable."
50 );
51 }
52 $this->fileHandle = @fopen($database, 'rb');
53 if ($this->fileHandle === false) {
54 throw new \InvalidArgumentException(
55 "Error opening \"$database\"."
56 );
57 }
58 $this->fileSize = @filesize($database);
59 if ($this->fileSize === false) {
60 throw new \UnexpectedValueException(
61 "Error determining the size of \"$database\"."
62 );
63 }
64
65 $start = $this->findMetadataStart($database);
66 $metadataDecoder = new Decoder($this->fileHandle, $start);
67 list($metadataArray) = $metadataDecoder->decode($start);
68 $this->metadata = new Metadata($metadataArray);
69 $this->decoder = new Decoder(
70 $this->fileHandle,
71 $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
72 );
73 }
74
75 /**
76 * Looks up the <code>address</code> in the MaxMind DB.
77 *
78 * @param string $ipAddress
b55260fc
MG
79 * the IP address to look up
80 *
81 * @throws \BadMethodCallException if this method is called on a closed database
82 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
eacc36a2 83 * @throws InvalidDatabaseException
b55260fc
MG
84 * if the database is invalid or there is an error reading
85 * from it
86 *
87 * @return array the record for the IP address
eacc36a2
DP
88 */
89 public function get($ipAddress)
90 {
b55260fc 91 if (\func_num_args() !== 1) {
eacc36a2
DP
92 throw new \InvalidArgumentException(
93 'Method takes exactly one argument.'
94 );
95 }
96
b55260fc 97 if (!\is_resource($this->fileHandle)) {
eacc36a2
DP
98 throw new \BadMethodCallException(
99 'Attempt to read from a closed MaxMind DB.'
100 );
101 }
102
103 if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
104 throw new \InvalidArgumentException(
105 "The value \"$ipAddress\" is not a valid IP address."
106 );
107 }
108
b55260fc 109 if ($this->metadata->ipVersion === 4 && strrpos($ipAddress, ':')) {
eacc36a2
DP
110 throw new \InvalidArgumentException(
111 "Error looking up $ipAddress. You attempted to look up an"
b55260fc 112 . ' IPv6 address in an IPv4-only database.'
eacc36a2
DP
113 );
114 }
115 $pointer = $this->findAddressInTree($ipAddress);
b55260fc 116 if ($pointer === 0) {
eacc36a2
DP
117 return null;
118 }
b55260fc 119
eacc36a2
DP
120 return $this->resolveDataPointer($pointer);
121 }
122
123 private function findAddressInTree($ipAddress)
124 {
125 // XXX - could simplify. Done as a byte array to ease porting
126 $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
127
b55260fc 128 $bitCount = \count($rawAddress) * 8;
eacc36a2
DP
129
130 // The first node of the tree is always node 0, at the beginning of the
131 // value
132 $node = $this->startNode($bitCount);
133
b55260fc 134 for ($i = 0; $i < $bitCount; ++$i) {
eacc36a2
DP
135 if ($node >= $this->metadata->nodeCount) {
136 break;
137 }
138 $tempBit = 0xFF & $rawAddress[$i >> 3];
139 $bit = 1 & ($tempBit >> 7 - ($i % 8));
140
141 $node = $this->readNode($node, $bit);
142 }
b55260fc 143 if ($node === $this->metadata->nodeCount) {
eacc36a2
DP
144 // Record is empty
145 return 0;
146 } elseif ($node > $this->metadata->nodeCount) {
147 // Record is a data pointer
148 return $node;
149 }
b55260fc 150 throw new InvalidDatabaseException('Something bad happened');
eacc36a2
DP
151 }
152
eacc36a2
DP
153 private function startNode($length)
154 {
155 // Check if we are looking up an IPv4 address in an IPv6 tree. If this
156 // is the case, we can skip over the first 96 nodes.
b55260fc 157 if ($this->metadata->ipVersion === 6 && $length === 32) {
eacc36a2
DP
158 return $this->ipV4StartNode();
159 }
160 // The first node of the tree is always node 0, at the beginning of the
161 // value
162 return 0;
163 }
164
165 private function ipV4StartNode()
166 {
167 // This is a defensive check. There is no reason to call this when you
168 // have an IPv4 tree.
b55260fc 169 if ($this->metadata->ipVersion === 4) {
eacc36a2
DP
170 return 0;
171 }
172
b55260fc 173 if ($this->ipV4Start) {
eacc36a2
DP
174 return $this->ipV4Start;
175 }
176 $node = 0;
177
b55260fc 178 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
eacc36a2
DP
179 $node = $this->readNode($node, 0);
180 }
181 $this->ipV4Start = $node;
b55260fc 182
eacc36a2
DP
183 return $node;
184 }
185
186 private function readNode($nodeNumber, $index)
187 {
188 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
189
190 // XXX - probably could condense this.
191 switch ($this->metadata->recordSize) {
192 case 24:
193 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
194 list(, $node) = unpack('N', "\x00" . $bytes);
b55260fc 195
eacc36a2
DP
196 return $node;
197 case 28:
198 $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
199 list(, $middle) = unpack('C', $middleByte);
b55260fc 200 if ($index === 0) {
eacc36a2
DP
201 $middle = (0xF0 & $middle) >> 4;
202 } else {
203 $middle = 0x0F & $middle;
204 }
205 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
b55260fc
MG
206 list(, $node) = unpack('N', \chr($middle) . $bytes);
207
eacc36a2
DP
208 return $node;
209 case 32:
210 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
211 list(, $node) = unpack('N', $bytes);
b55260fc 212
eacc36a2
DP
213 return $node;
214 default:
215 throw new InvalidDatabaseException(
216 'Unknown record size: '
217 . $this->metadata->recordSize
218 );
219 }
220 }
221
222 private function resolveDataPointer($pointer)
223 {
224 $resolved = $pointer - $this->metadata->nodeCount
225 + $this->metadata->searchTreeSize;
226 if ($resolved > $this->fileSize) {
227 throw new InvalidDatabaseException(
228 "The MaxMind DB file's search tree is corrupt"
229 );
230 }
231
232 list($data) = $this->decoder->decode($resolved);
b55260fc 233
eacc36a2
DP
234 return $data;
235 }
236
237 /*
238 * This is an extremely naive but reasonably readable implementation. There
239 * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
240 * an issue, but I suspect it won't be.
241 */
242 private function findMetadataStart($filename)
243 {
244 $handle = $this->fileHandle;
245 $fstat = fstat($handle);
246 $fileSize = $fstat['size'];
247 $marker = self::$METADATA_START_MARKER;
248 $markerLength = self::$METADATA_START_MARKER_LENGTH;
4e23b6b2
AA
249 $metadataMaxLengthExcludingMarker
250 = min(self::$METADATA_MAX_SIZE, $fileSize) - $markerLength;
eacc36a2 251
b55260fc
MG
252 for ($i = 0; $i <= $metadataMaxLengthExcludingMarker; ++$i) {
253 for ($j = 0; $j < $markerLength; ++$j) {
eacc36a2
DP
254 fseek($handle, $fileSize - $i - $j - 1);
255 $matchBit = fgetc($handle);
b55260fc 256 if ($matchBit !== $marker[$markerLength - $j - 1]) {
eacc36a2
DP
257 continue 2;
258 }
259 }
b55260fc 260
eacc36a2
DP
261 return $fileSize - $i;
262 }
263 throw new InvalidDatabaseException(
264 "Error opening database file ($filename). " .
265 'Is this a valid MaxMind DB file?'
266 );
267 }
268
269 /**
b55260fc
MG
270 * @throws \InvalidArgumentException if arguments are passed to the method
271 * @throws \BadMethodCallException if the database has been closed
272 *
273 * @return Metadata object for the database
eacc36a2
DP
274 */
275 public function metadata()
276 {
b55260fc 277 if (\func_num_args()) {
eacc36a2
DP
278 throw new \InvalidArgumentException(
279 'Method takes no arguments.'
280 );
281 }
282
283 // Not technically required, but this makes it consistent with
284 // C extension and it allows us to change our implementation later.
b55260fc 285 if (!\is_resource($this->fileHandle)) {
eacc36a2
DP
286 throw new \BadMethodCallException(
287 'Attempt to read from a closed MaxMind DB.'
288 );
289 }
290
291 return $this->metadata;
292 }
293
294 /**
295 * Closes the MaxMind DB and returns resources to the system.
296 *
297 * @throws \Exception
b55260fc 298 * if an I/O error occurs
eacc36a2
DP
299 */
300 public function close()
301 {
b55260fc 302 if (!\is_resource($this->fileHandle)) {
eacc36a2
DP
303 throw new \BadMethodCallException(
304 'Attempt to close a closed MaxMind DB.'
305 );
306 }
307 fclose($this->fileHandle);
308 }
309}