Issues (10)

src/Readers/Reader.php (6 issues)

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
The condition $this->fileSize === false is always false.
Loading history...
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 ignore-type  annotation

119
        $metaLength = unpack('N', /** @scrutinizer ignore-type */ $this->read(0, 4))[1];
Loading history...
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 ignore-type  annotation

122
        $this->meta = json_decode(/** @scrutinizer ignore-type */ $text, true);
Loading history...
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
Consider adding a comment why this CATCH block is empty.
Loading history...
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 ignore-type  annotation

310
        if (strlen(/** @scrutinizer ignore-type */ $value) === $length) {
Loading history...
311 4
            return $value;
0 ignored issues
show
Bug Best Practice introduced by
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.

Loading history...
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