Completed
Push — add/gdpr-ads-compliance ( 47ea51...6d1e7f )
by
unknown
09:08
created

Reader   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 296
Duplicated Lines 3.38 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 10
loc 296
rs 8.2769
c 0
b 0
f 0
wmc 41
lcom 1
cbo 4

10 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 35 5
C get() 0 33 7
B findAddressInTree() 0 29 5
A startNode() 0 11 3
B ipV4StartNode() 0 20 5
B readNode() 10 35 5
A resolveDataPointer() 0 14 2
B findMetadataStart() 0 26 4
A metadata() 0 18 3
A close() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Reader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Reader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MaxMind\Db;
4
5
use MaxMind\Db\Reader\Decoder;
6
use MaxMind\Db\Reader\InvalidDatabaseException;
7
use MaxMind\Db\Reader\Metadata;
8
use 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
 */
14
class 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;
19
    private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB
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
32
     *                         the MaxMind DB file to use
33
     *
34
     * @throws \InvalidArgumentException                   for invalid database path or unknown arguments
35
     * @throws \MaxMind\Db\Reader\InvalidDatabaseException
36
     *                                                     if the database is invalid or there is an error reading
37
     *                                                     from it
38
     */
39
    public function __construct($database)
40
    {
41
        if (func_num_args() !== 1) {
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
0 ignored issues
show
Documentation introduced by
The property $searchTreeSize is declared private in MaxMind\Db\Reader\Metadata. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
72
        );
73
    }
74
75
    /**
76
     * Looks up the <code>address</code> in the MaxMind DB.
77
     *
78
     * @param string $ipAddress
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
83
     * @throws InvalidDatabaseException
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
88
     */
89
    public function get($ipAddress)
90
    {
91
        if (func_num_args() !== 1) {
92
            throw new \InvalidArgumentException(
93
                'Method takes exactly one argument.'
94
            );
95
        }
96
97
        if (!is_resource($this->fileHandle)) {
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
109
        if ($this->metadata->ipVersion === 4 && strrpos($ipAddress, ':')) {
110
            throw new \InvalidArgumentException(
111
                "Error looking up $ipAddress. You attempted to look up an"
112
                . ' IPv6 address in an IPv4-only database.'
113
            );
114
        }
115
        $pointer = $this->findAddressInTree($ipAddress);
116
        if ($pointer === 0) {
117
            return null;
118
        }
119
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
128
        $bitCount = count($rawAddress) * 8;
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
134
        for ($i = 0; $i < $bitCount; $i++) {
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
        }
143
        if ($node === $this->metadata->nodeCount) {
144
            // Record is empty
145
            return 0;
146
        } elseif ($node > $this->metadata->nodeCount) {
147
            // Record is a data pointer
148
            return $node;
149
        }
150
        throw new InvalidDatabaseException('Something bad happened');
151
    }
152
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.
157
        if ($this->metadata->ipVersion === 6 && $length === 32) {
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.
169
        if ($this->metadata->ipVersion === 4) {
170
            return 0;
171
        }
172
173
        if ($this->ipV4Start) {
174
            return $this->ipV4Start;
175
        }
176
        $node = 0;
177
178
        for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) {
179
            $node = $this->readNode($node, 0);
180
        }
181
        $this->ipV4Start = $node;
182
183
        return $node;
184
    }
185
186
    private function readNode($nodeNumber, $index)
187
    {
188
        $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
0 ignored issues
show
Documentation introduced by
The property $nodeByteSize is declared private in MaxMind\Db\Reader\Metadata. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
189
190
        // XXX - probably could condense this.
191
        switch ($this->metadata->recordSize) {
192 View Code Duplication
            case 24:
193
                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
194
                list(, $node) = unpack('N', "\x00" . $bytes);
195
196
                return $node;
197
            case 28:
198
                $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
199
                list(, $middle) = unpack('C', $middleByte);
200
                if ($index === 0) {
201
                    $middle = (0xF0 & $middle) >> 4;
202
                } else {
203
                    $middle = 0x0F & $middle;
204
                }
205
                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
206
                list(, $node) = unpack('N', chr($middle) . $bytes);
207
208
                return $node;
209 View Code Duplication
            case 32:
210
                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
211
                list(, $node) = unpack('N', $bytes);
212
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;
0 ignored issues
show
Documentation introduced by
The property $searchTreeSize is declared private in MaxMind\Db\Reader\Metadata. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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);
233
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;
249
        $metadataMaxLengthExcludingMarker
250
            = min(self::$METADATA_MAX_SIZE, $fileSize) - $markerLength;
251
252
        for ($i = 0; $i <= $metadataMaxLengthExcludingMarker; $i++) {
253
            for ($j = 0; $j < $markerLength; $j++) {
254
                fseek($handle, $fileSize - $i - $j - 1);
255
                $matchBit = fgetc($handle);
256
                if ($matchBit !== $marker[$markerLength - $j - 1]) {
257
                    continue 2;
258
                }
259
            }
260
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
    /**
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
274
     */
275
    public function metadata()
276
    {
277
        if (func_num_args()) {
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.
285
        if (!is_resource($this->fileHandle)) {
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
298
     *                    if an I/O error occurs
299
     */
300
    public function close()
301
    {
302
        if (!is_resource($this->fileHandle)) {
303
            throw new \BadMethodCallException(
304
                'Attempt to close a closed MaxMind DB.'
305
            );
306
        }
307
        fclose($this->fileHandle);
308
    }
309
}
310