1 | <?php |
||||
2 | |||||
3 | namespace HughCube\IpDb\Readers; |
||||
4 | |||||
5 | use HughCube\IpDb\Exceptions\InvalidIpException; |
||||
6 | use HughCube\IpDb\Exceptions\NotSupportIpTypeException; |
||||
7 | use HughCube\IpDb\Exceptions\NotSupportLanguageException; |
||||
8 | use HughCube\IpDb\Helpers\Ip; |
||||
9 | |||||
10 | abstract class Reader |
||||
11 | { |
||||
12 | const IPV4 = 1; |
||||
13 | const IPV6 = 2; |
||||
14 | |||||
15 | /** |
||||
16 | * @var int ipDB文件大小 |
||||
17 | */ |
||||
18 | protected $fileSize; |
||||
19 | |||||
20 | protected $nodeCount = 0; |
||||
21 | protected $nodeOffset = 0; |
||||
22 | |||||
23 | /** |
||||
24 | * @var array |
||||
25 | */ |
||||
26 | protected $meta; |
||||
27 | |||||
28 | /** |
||||
29 | * 计算文件大小. |
||||
30 | * |
||||
31 | * @return int |
||||
32 | */ |
||||
33 | abstract protected function computeFileSize(); |
||||
34 | |||||
35 | /** |
||||
36 | * 读取文件内容. |
||||
37 | * |
||||
38 | * @param int $offset 指针偏移 |
||||
39 | * @param int $length 读取长度 |
||||
40 | * |
||||
41 | * @return string|false |
||||
42 | */ |
||||
43 | abstract protected function read($offset, $length); |
||||
44 | |||||
45 | /** |
||||
46 | * 是否支持IP V6. |
||||
47 | * |
||||
48 | * @return bool |
||||
49 | */ |
||||
50 | public function isSupportV6() |
||||
51 | { |
||||
52 | return ($this->meta['ip_version'] & static::IPV6) === static::IPV6; |
||||
53 | } |
||||
54 | |||||
55 | /** |
||||
56 | * 是否支持IP V4. |
||||
57 | * |
||||
58 | * @return bool |
||||
59 | */ |
||||
60 | 4 | public function isSupportV4() |
|||
61 | { |
||||
62 | 4 | return ($this->meta['ip_version'] & static::IPV4) === static::IPV4; |
|||
63 | } |
||||
64 | |||||
65 | /** |
||||
66 | * 是否支持指定语言 |
||||
67 | * |
||||
68 | * @param string $language |
||||
69 | * |
||||
70 | * @return bool |
||||
71 | */ |
||||
72 | 4 | public function isSupportLanguage($language) |
|||
73 | { |
||||
74 | 4 | return in_array($language, $this->getSupportLanguages(), true); |
|||
75 | } |
||||
76 | |||||
77 | /** |
||||
78 | * 支持的语言 |
||||
79 | * |
||||
80 | * @return array |
||||
81 | */ |
||||
82 | 4 | public function getSupportLanguages() |
|||
83 | { |
||||
84 | 4 | if (isset($this->meta['languages']) && is_array($this->meta['languages'])) { |
|||
85 | 4 | return array_keys($this->meta['languages']); |
|||
86 | } |
||||
87 | |||||
88 | return []; |
||||
89 | } |
||||
90 | |||||
91 | /** |
||||
92 | * @return int UTC Timestamp |
||||
93 | */ |
||||
94 | public function getBuildTime() |
||||
95 | { |
||||
96 | return $this->meta['build']; |
||||
97 | } |
||||
98 | |||||
99 | /** |
||||
100 | * 获取mete数据. |
||||
101 | * |
||||
102 | * @return array |
||||
103 | */ |
||||
104 | public function getMeta() |
||||
105 | { |
||||
106 | return $this->meta; |
||||
107 | } |
||||
108 | |||||
109 | /** |
||||
110 | * @throws \Exception |
||||
111 | */ |
||||
112 | 4 | protected function init() |
|||
113 | { |
||||
114 | 4 | $this->fileSize = $this->computeFileSize(); |
|||
115 | 4 | if ($this->fileSize === false) { |
|||
0 ignored issues
–
show
introduced
by
![]() |
|||||
116 | throw new \UnexpectedValueException('Error determining the size of data.'); |
||||
117 | } |
||||
118 | |||||
119 | 4 | $metaLength = unpack('N', $this->read(0, 4))[1]; |
|||
0 ignored issues
–
show
It seems like
$this->read(0, 4) can also be of type false ; however, parameter $data of unpack() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
120 | 4 | $text = $this->read(4, $metaLength); |
|||
121 | |||||
122 | 4 | $this->meta = json_decode($text, true); |
|||
0 ignored issues
–
show
It seems like
$text can also be of type false ; however, parameter $json of json_decode() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
123 | 4 | if (isset($this->meta['fields']) === false || isset($this->meta['languages']) === false) { |
|||
124 | throw new \Exception('IP Database metadata error.'); |
||||
125 | } |
||||
126 | |||||
127 | 4 | $fileSize = 4 + $metaLength + $this->meta['total_size']; |
|||
128 | 4 | if ($fileSize != $this->fileSize) { |
|||
129 | throw new \Exception('IP Database size error.'); |
||||
130 | } |
||||
131 | |||||
132 | 4 | $this->nodeCount = $this->meta['node_count']; |
|||
133 | 4 | $this->nodeOffset = 4 + $metaLength; |
|||
134 | 4 | } |
|||
135 | |||||
136 | /** |
||||
137 | * 查找ip的信息. |
||||
138 | * |
||||
139 | * @param string $ip |
||||
140 | * @param string $language |
||||
141 | * |
||||
142 | * @return array|null |
||||
143 | */ |
||||
144 | 4 | public function find($ip, $language) |
|||
145 | { |
||||
146 | 4 | if (!$this->isSupportLanguage($language)) { |
|||
147 | 4 | throw new NotSupportLanguageException("language : {$language} not support."); |
|||
148 | } |
||||
149 | |||||
150 | 4 | if (!Ip::isIp($ip)) { |
|||
151 | 4 | throw new InvalidIpException("The value \"$ip\" is not a valid IP address."); |
|||
152 | } |
||||
153 | |||||
154 | 4 | if (Ip::isIp4($ip) && !$this->isSupportV4()) { |
|||
155 | throw new NotSupportIpTypeException('The Database not support IPv4 address.'); |
||||
156 | 4 | } elseif (Ip::isIp6($ip) && !$this->isSupportV6()) { |
|||
157 | throw new NotSupportIpTypeException('The Database not support IPv6 address.'); |
||||
158 | } |
||||
159 | |||||
160 | try { |
||||
161 | 4 | $node = $this->findNode($ip); |
|||
162 | |||||
163 | 4 | if ($node > 0) { |
|||
164 | 4 | $data = $this->resolve($node); |
|||
165 | 4 | $values = explode("\t", $data); |
|||
166 | |||||
167 | 4 | return array_slice($values, $this->meta['languages'][$language], count($this->meta['fields'])); |
|||
168 | } |
||||
169 | } catch (\Throwable $exception) { |
||||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||||
170 | } |
||||
171 | } |
||||
172 | |||||
173 | /** |
||||
174 | * 查找ip的信息, 带上字段. |
||||
175 | * |
||||
176 | * @param string $ip |
||||
177 | * @param string $language |
||||
178 | * |
||||
179 | * @return array|false|null |
||||
180 | */ |
||||
181 | 4 | public function findMap($ip, $language) |
|||
182 | { |
||||
183 | 4 | $array = $this->find($ip, $language); |
|||
184 | 4 | if (null === $array) { |
|||
185 | return; |
||||
186 | } |
||||
187 | |||||
188 | 4 | return array_combine($this->meta['fields'], $array); |
|||
189 | } |
||||
190 | |||||
191 | /** |
||||
192 | * @var int |
||||
193 | */ |
||||
194 | private $v4offset = 0; |
||||
195 | |||||
196 | /** |
||||
197 | * @var array |
||||
198 | */ |
||||
199 | private $v6offsetCache = []; |
||||
200 | |||||
201 | /** |
||||
202 | *查找Ip的节点. |
||||
203 | * |
||||
204 | * @param $ip |
||||
205 | * |
||||
206 | * @throws \Exception |
||||
207 | * |
||||
208 | * @return int |
||||
209 | */ |
||||
210 | 4 | private function findNode($ip) |
|||
211 | { |
||||
212 | 4 | $binary = inet_pton($ip); |
|||
213 | 4 | $bitCount = strlen($binary) * 8; // 32 | 128 |
|||
214 | 4 | $key = substr($binary, 0, 2); |
|||
215 | 4 | $node = 0; |
|||
216 | 4 | $index = 0; |
|||
217 | 4 | if ($bitCount === 32) { |
|||
218 | 4 | if ($this->v4offset === 0) { |
|||
219 | 4 | for ($i = 0; $i < 96 && $node < $this->nodeCount; $i++) { |
|||
220 | 4 | $idx = ($i >= 80) ? 1 : 0; |
|||
221 | 4 | $node = $this->readNode($node, $idx); |
|||
222 | 4 | if ($node > $this->nodeCount) { |
|||
223 | return 0; |
||||
224 | } |
||||
225 | } |
||||
226 | 4 | $this->v4offset = $node; |
|||
227 | } else { |
||||
228 | 4 | $node = $this->v4offset; |
|||
229 | } |
||||
230 | } else { |
||||
231 | if (isset($this->v6offsetCache[$key])) { |
||||
232 | $index = 16; |
||||
233 | $node = $this->v6offsetCache[$key]; |
||||
234 | } |
||||
235 | } |
||||
236 | |||||
237 | 4 | for ($i = $index; $i < $bitCount; $i++) { |
|||
238 | 4 | if ($node >= $this->nodeCount) { |
|||
239 | 4 | break; |
|||
240 | } |
||||
241 | |||||
242 | 4 | $node = $this->readNode($node, 1 & ((0xFF & ord($binary[$i >> 3])) >> 7 - ($i % 8))); |
|||
243 | |||||
244 | 4 | if ($i == 15) { |
|||
245 | 4 | $this->v6offsetCache[$key] = $node; |
|||
246 | } |
||||
247 | } |
||||
248 | |||||
249 | 4 | if ($node === $this->nodeCount) { |
|||
250 | return 0; |
||||
251 | 4 | } elseif ($node > $this->nodeCount) { |
|||
252 | 4 | return $node; |
|||
253 | } |
||||
254 | |||||
255 | throw new \Exception('find node failed.'); |
||||
256 | } |
||||
257 | |||||
258 | /** |
||||
259 | * @param $node |
||||
260 | * @param $index |
||||
261 | * |
||||
262 | * @throws \Exception |
||||
263 | * |
||||
264 | * @return mixed |
||||
265 | */ |
||||
266 | 4 | private function readNode($node, $index) |
|||
267 | { |
||||
268 | 4 | return unpack('N', $this->readNodeData(($node * 8 + $index * 4), 4))[1]; |
|||
269 | } |
||||
270 | |||||
271 | /** |
||||
272 | * @param $node |
||||
273 | * |
||||
274 | * @throws \Exception |
||||
275 | * |
||||
276 | * @return mixed |
||||
277 | */ |
||||
278 | 4 | private function resolve($node) |
|||
279 | { |
||||
280 | 4 | $resolved = $node - $this->nodeCount + $this->nodeCount * 8; |
|||
281 | 4 | if ($resolved >= $this->fileSize) { |
|||
282 | return; |
||||
283 | } |
||||
284 | |||||
285 | 4 | $bytes = $this->readNodeData($resolved, 2); |
|||
286 | 4 | $size = unpack('N', str_pad($bytes, 4, "\x00", STR_PAD_LEFT))[1]; |
|||
287 | |||||
288 | 4 | $resolved += 2; |
|||
289 | |||||
290 | 4 | return $this->readNodeData($resolved, $size); |
|||
291 | } |
||||
292 | |||||
293 | /** |
||||
294 | * 读取节点数据. |
||||
295 | * |
||||
296 | * @param int $offset |
||||
297 | * @param int $length |
||||
298 | * |
||||
299 | * @throws \Exception |
||||
300 | * |
||||
301 | * @return string |
||||
302 | */ |
||||
303 | 4 | private function readNodeData($offset, $length) |
|||
304 | { |
||||
305 | 4 | if (0 >= $length) { |
|||
306 | return ''; |
||||
307 | } |
||||
308 | |||||
309 | 4 | $value = $this->read(($offset + $this->nodeOffset), $length); |
|||
310 | 4 | if (strlen($value) === $length) { |
|||
0 ignored issues
–
show
It seems like
$value can also be of type false ; however, parameter $string of strlen() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
311 | 4 | return $value; |
|||
0 ignored issues
–
show
The expression
return $value could also return false which is incompatible with the documented return type string . Did you maybe forget to handle an error condition?
If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled. ![]() |
|||||
312 | } |
||||
313 | |||||
314 | throw new \Exception('The Database file read bad data.'); |
||||
315 | } |
||||
316 | |||||
317 | /** |
||||
318 | * 回收资源. |
||||
319 | * |
||||
320 | * @return bool |
||||
321 | */ |
||||
322 | public function close() |
||||
323 | { |
||||
324 | return true; |
||||
325 | } |
||||
326 | |||||
327 | 4 | public function __destruct() |
|||
328 | { |
||||
329 | 4 | $this->close(); |
|||
330 | 4 | } |
|||
331 | } |
||||
332 |