Completed
Push — master ( 85a727...790e23 )
by Michele
03:16
created

IPv6::toString()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 22
cts 22
cp 1
rs 8.7697
c 0
b 0
f 0
cc 6
nc 5
nop 1
crap 6
1
<?php
2
3
namespace IPLib\Address;
4
5
use IPLib\Range\RangeInterface;
6
use IPLib\Range\Subnet;
7
use IPLib\Range\Type as RangeType;
8
9
/**
10
 * An IPv6 address.
11
 */
12
class IPv6 implements AddressInterface
13
{
14
    /**
15
     * The long string representation of the address.
16
     *
17
     * @var string
18
     *
19
     * @example '0000:0000:0000:0000:0000:0000:0000:0001'
20
     */
21
    protected $longAddress;
22
23
    /**
24
     * The long string representation of the address.
25
     *
26
     * @var string|null
27
     *
28
     * @example '::1'
29
     */
30
    protected $shortAddress;
31
32
    /**
33
     * The byte list of the IP address.
34
     *
35
     * @var int[]|null
36
     */
37
    protected $bytes;
38
39
    /**
40
     * The word list of the IP address.
41
     *
42
     * @var int[]|null
43
     */
44
    protected $words;
45
46
    /**
47
     * The type of the range of this IP address.
48
     *
49
     * @var int|null
50
     */
51
    protected $rangeType;
52
53
    /**
54
     * An array containing RFC designated address ranges.
55
     *
56
     * @var array|null
57
     */
58
    private static $reservedRanges = null;
59
60
    /**
61
     * Initializes the instance.
62
     *
63
     * @param string $longAddress
64
     */
65 444
    public function __construct($longAddress)
66
    {
67 444
        $this->longAddress = $longAddress;
68 444
        $this->shortAddress = null;
69 444
        $this->bytes = null;
70 444
        $this->words = null;
71 444
        $this->rangeType = null;
72 444
    }
73
74
    /**
75
     * {@inheritdoc}
76
     *
77
     * @see \IPLib\Address\AddressInterface::__toString()
78
     */
79 24
    public function __toString()
80
    {
81 24
        return $this->toString();
82
    }
83
84
    /**
85
     * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
86
     *
87
     * @param string|mixed $address the address to parse
88
     * @param bool $mayIncludePort set to false to avoid parsing addresses with ports
89
     * @param bool $mayIncludeZoneID set to false to avoid parsing addresses with zone IDs (see RFC 4007)
90
     *
91
     * @return static|null
92
     */
93 495
    public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
94
    {
95 495
        $result = null;
96 495
        if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) {
97 457
            $matches = null;
98 457
            if ($mayIncludePort && $address[0] === '[' && preg_match('/^\[(.+)\]:\d+$/', $address, $matches)) {
99 2
                $address = $matches[1];
100
            }
101 457
            if ($mayIncludeZoneID) {
102 457
                $percentagePos = strpos($address, '%');
103 457
                if ($percentagePos > 0) {
104 4
                    $address = substr($address, 0, $percentagePos);
105
                }
106
            }
107 457
            if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
108 9
                $address6 = static::fromString($matches[1] . '0:0', false);
109 9
                if ($address6 !== null) {
110 6
                    $address4 = IPv4::fromString($matches[2], false);
111 6
                    if ($address4 !== null) {
112 6
                        $bytes4 = $address4->getBytes();
113 6
                        $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
114 9
                        $result = $address6;
115
                    }
116
                }
117
            } else {
118 457
                if (strpos($address, '::') === false) {
119 379
                    $chunks = explode(':', $address);
120
                } else {
121 91
                    $chunks = array();
122 91
                    $parts = explode('::', $address);
123 91
                    if (count($parts) === 2) {
124 89
                        $before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
125 89
                        $after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
126 89
                        $missing = 8 - count($before) - count($after);
127 89
                        if ($missing >= 0) {
128 87
                            $chunks = $before;
129 87
                            if ($missing !== 0) {
130 86
                                $chunks = array_merge($chunks, array_fill(0, $missing, '0'));
131
                            }
132 87
                            $chunks = array_merge($chunks, $after);
133
                        }
134
                    }
135
                }
136 457
                if (count($chunks) === 8) {
137 448
                    $nums = array_map(
138 448
                        function ($chunk) {
139 448
                            return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
140 448
                        },
141 448
                        $chunks
142
                    );
143 448
                    if (!in_array(false, $nums, true)) {
144 440
                        $longAddress = implode(
145 440
                            ':',
146 440
                            array_map(
147 440
                                function ($num) {
148 440
                                    return sprintf('%04x', $num);
149 440
                                },
150 440
                                $nums
151
                            )
152
                        );
153 440
                        $result = new static($longAddress);
154
                    }
155
                }
156
            }
157
        }
158
159 495
        return $result;
160
    }
161
162
    /**
163
     * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
164
     *
165
     * @param int[]|array $bytes
166
     *
167
     * @return static|null
168
     */
169 298
    public static function fromBytes(array $bytes)
170
    {
171 298
        $result = null;
172 298
        if (count($bytes) === 16) {
173 278
            $address = '';
174 278
            for ($i = 0; $i < 16; $i++) {
175 278
                if ($i !== 0 && $i % 2 === 0) {
176 278
                    $address .= ':';
177
                }
178 278
                $byte = $bytes[$i];
179 278
                if (is_int($byte) && $byte >= 0 && $byte <= 255) {
180 278
                    $address .= sprintf('%02x', $byte);
181
                } else {
182
                    $address = null;
183
                    break;
184
                }
185
            }
186 278
            if ($address !== null) {
187 278
                $result = new static($address);
188
            }
189
        }
190
191 298
        return $result;
192
    }
193
194
    /**
195
     * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
196
     *
197
     * @param int[]|array $words
198
     *
199
     * @return static|null
200
     */
201 49
    public static function fromWords(array $words)
202
    {
203 49
        $result = null;
204 49
        if (count($words) === 8) {
205 29
            $chunks = array();
206 29
            for ($i = 0; $i < 8; $i++) {
207 29
                $word = $words[$i];
208 29
                if (is_int($word) && $word >= 0 && $word <= 0xffff) {
209 29
                    $chunks[] = sprintf('%04x', $word);
210
                } else {
211
                    $chunks = null;
212
                    break;
213
                }
214
            }
215 29
            if ($chunks !== null) {
216 29
                $result = new static(implode(':', $chunks));
217
            }
218
        }
219
220 49
        return $result;
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     *
226
     * @see \IPLib\Address\AddressInterface::toString()
227
     */
228 269
    public function toString($long = false)
229
    {
230 269
        if ($long) {
231 38
            $result = $this->longAddress;
232
        } else {
233 263
            if ($this->shortAddress === null) {
234 263
                if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
235 7
                    $lastBytes = array_slice($this->getBytes(), -4);
236 7
                    $this->shortAddress = '::ffff:' . implode('.', $lastBytes);
237
                } else {
238 256
                    $chunks = array_map(
239 256
                        function ($word) {
240 256
                            return dechex($word);
241 256
                        },
242 256
                        $this->getWords()
243
                    );
244 256
                    $shortAddress = implode(':', $chunks);
245 256
                    $matches = null;
246 256
                    for ($i = 8; $i > 1; $i--) {
247 256
                        $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)';
248 256
                        if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) {
249 201
                            $shortAddress = $matches[1] . '::' . $matches[2];
250 201
                            break;
251
                        }
252
                    }
253 256
                    $this->shortAddress = $shortAddress;
254
                }
255
            }
256 263
            $result = $this->shortAddress;
257
        }
258
259 269
        return $result;
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     *
265
     * @see \IPLib\Address\AddressInterface::getBytes()
266
     */
267 289
    public function getBytes()
268
    {
269 289
        if ($this->bytes === null) {
270 289
            $bytes = array();
271 289
            foreach ($this->getWords() as $word) {
272 289
                $bytes[] = $word >> 8;
273 289
                $bytes[] = $word & 0xff;
274
            }
275 289
            $this->bytes = $bytes;
276
        }
277
278 289
        return $this->bytes;
279
    }
280
281
    /**
282
     * Get the word list of the IP address.
283
     *
284
     * @return int[]
285
     */
286 430
    public function getWords()
287
    {
288 430
        if ($this->words === null) {
289 430
            $this->words = array_map(
290 430
                function ($chunk) {
291 430
                    return hexdec($chunk);
292 430
                },
293 430
                explode(':', $this->longAddress)
294
            );
295
        }
296
297 430
        return $this->words;
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     *
303
     * @see \IPLib\Address\AddressInterface::getAddressType()
304
     */
305 258
    public function getAddressType()
306
    {
307 258
        return Type::T_IPv6;
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     *
313
     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
314
     */
315 100
    public static function getDefaultReservedRangeType()
316
    {
317 100
        return RangeType::T_RESERVED;
318
    }
319
320
    /**
321
     * {@inheritdoc}
322
     *
323
     * @see \IPLib\Address\AddressInterface::getReservedRanges()
324
     */
325 116
    public static function getReservedRanges()
326
    {
327 116
        if (self::$reservedRanges === null) {
328 1
            $reservedRanges = array();
329
            foreach (array(
330
                // RFC 4291
331 1
                '::/128' => array(RangeType::T_UNSPECIFIED),
332
                // RFC 4291
333
                '::1/128' => array(RangeType::T_LOOPBACK),
334
                // RFC 4291
335
                '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
336
                //'2002::/16' => array(RangeType::),
337
                // RFC 4291
338
                '2000::/3' => array(RangeType::T_PUBLIC),
339
                // RFC 4193
340
                'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
341
                // RFC 4291
342
                'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
343
                // RFC 4291
344
                'ff00::/8' => array(RangeType::T_MULTICAST),
345
                // RFC 4291
346
                //'::/8' => array(RangeType::T_RESERVED),
347
                // RFC 4048
348
                //'200::/7' => array(RangeType::T_RESERVED),
349
                // RFC 4291
350
                //'400::/6' => array(RangeType::T_RESERVED),
351
                // RFC 4291
352
                //'800::/5' => array(RangeType::T_RESERVED),
353
                // RFC 4291
354
                //'1000::/4' => array(RangeType::T_RESERVED),
355
                // RFC 4291
356
                //'4000::/3' => array(RangeType::T_RESERVED),
357
                // RFC 4291
358
                //'6000::/3' => array(RangeType::T_RESERVED),
359
                // RFC 4291
360
                //'8000::/3' => array(RangeType::T_RESERVED),
361
                // RFC 4291
362
                //'a000::/3' => array(RangeType::T_RESERVED),
363
                // RFC 4291
364
                //'c000::/3' => array(RangeType::T_RESERVED),
365
                // RFC 4291
366
                //'e000::/4' => array(RangeType::T_RESERVED),
367
                // RFC 4291
368
                //'f000::/5' => array(RangeType::T_RESERVED),
369
                // RFC 4291
370
                //'f800::/6' => array(RangeType::T_RESERVED),
371
                // RFC 4291
372
                //'fe00::/9' => array(RangeType::T_RESERVED),
373
                // RFC 3879
374
                //'fec0::/10' => array(RangeType::T_RESERVED),
375
            ) as $range => $data) {
376 1
                $exceptions = array();
377 1 View Code Duplication
                if (isset($data[1])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
378 1
                    foreach ($data[1] as $exceptionRange => $exceptionType) {
379 1
                        $exceptions[] = new AssignedRange(Subnet::fromString($exceptionRange), $exceptionType);
1 ignored issue
show
Bug introduced by
It seems like \IPLib\Range\Subnet::fromString($exceptionRange) can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
380
                    }
381
                }
382 1
                $reservedRanges[] = new AssignedRange(Subnet::fromString($range), $data[0], $exceptions);
1 ignored issue
show
Bug introduced by
It seems like \IPLib\Range\Subnet::fromString($range) can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
383
            }
384 1
            self::$reservedRanges = $reservedRanges;
385
        }
386
387 116
        return self::$reservedRanges;
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     *
393
     * @see \IPLib\Address\AddressInterface::getRangeType()
394
     */
395 59
    public function getRangeType()
396
    {
397 59
        if ($this->rangeType === null) {
398 59
            $ipv4 = $this->toIPv4();
399 59 View Code Duplication
            if ($ipv4 !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
400 6
                $this->rangeType = $ipv4->getRangeType();
401
            } else {
402 53
                $rangeType = null;
403 53
                foreach (static::getReservedRanges() as $reservedRange) {
404 53
                    $rangeType = $reservedRange->getAddressType($this);
405 53
                    if ($rangeType !== null) {
406 53
                        break;
407
                    }
408
                }
409 53
                $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
410
            }
411
        }
412
413 59
        return $this->rangeType;
414
    }
415
416
    /**
417
     * Create an IPv4 representation of this address (if possible, otherwise returns null).
418
     *
419
     * @return \IPLib\Address\IPv4|null
420
     */
421 76
    public function toIPv4()
422
    {
423 76 View Code Duplication
        if (strpos($this->longAddress, '2002:') === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
424
            // 6to4
425 19
            return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
426
        }
427 57 View Code Duplication
        if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
428
            // IPv4-mapped IPv6 addresses
429 4
            return IPv4::fromBytes(array_slice($this->getBytes(), -4));
430
        }
431
432 53
        return null;
433
    }
434
435
    /**
436
     * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax.
437
     *
438
     * @param bool $ipV6Long render the IPv6 part in "long" format?
439
     * @param bool $ipV4Long render the IPv4 part in "long" format?
440
     *
441
     * @return string
442
     *
443
     * @example '::13.1.68.3'
444
     * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true
445
     * @example '::013.001.068.003' when $ipV4Long is true
446
     * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true
447
     *
448
     * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3.
449
     */
450 6
    public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false)
451
    {
452 6
        $myBytes = $this->getBytes();
453 6
        $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff));
454 6
        $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long);
455 6
        $ipv4Bytes = array_slice($myBytes, 12, 4);
456 6
        $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long);
457
458 6
        return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String);
459
    }
460
461
    /**
462
     * {@inheritdoc}
463
     *
464
     * @see \IPLib\Address\AddressInterface::getComparableString()
465
     */
466 155
    public function getComparableString()
467
    {
468 155
        return $this->longAddress;
469
    }
470
471
    /**
472
     * {@inheritdoc}
473
     *
474
     * @see \IPLib\Address\AddressInterface::matches()
475
     */
476 13
    public function matches(RangeInterface $range)
477
    {
478 13
        return $range->contains($this);
479
    }
480
481
    /**
482
     * {@inheritdoc}
483
     *
484
     * @see \IPLib\Address\AddressInterface::getNextAddress()
485
     */
486 8 View Code Duplication
    public function getNextAddress()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
487
    {
488 8
        $overflow = false;
489 8
        $words = $this->getWords();
490 8
        for ($i = count($words) - 1; $i >= 0; $i--) {
491 8
            if ($words[$i] === 0xffff) {
492 5
                if ($i === 0) {
493 1
                    $overflow = true;
494 1
                    break;
495
                }
496 5
                $words[$i] = 0;
497
            } else {
498 8
                $words[$i]++;
499 8
                break;
500
            }
501
        }
502
503 8
        return $overflow ? null : static::fromWords($words);
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     *
509
     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
510
     */
511 8 View Code Duplication
    public function getPreviousAddress()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
512
    {
513 8
        $overflow = false;
514 8
        $words = $this->getWords();
515 8
        for ($i = count($words) - 1; $i >= 0; $i--) {
516 8
            if ($words[$i] === 0) {
517 5
                if ($i === 0) {
518 1
                    $overflow = true;
519 1
                    break;
520
                }
521 5
                $words[$i] = 0xffff;
522
            } else {
523 8
                $words[$i]--;
524 8
                break;
525
            }
526
        }
527
528 8
        return $overflow ? null : static::fromWords($words);
529
    }
530
531
    /**
532
     * {@inheritdoc}
533
     *
534
     * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
535
     */
536 5
    public function getReverseDNSLookupName()
537
    {
538 5
        return implode(
539 5
            '.',
540 5
            array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1))
541 5
        ) . '.ip6.arpa';
542
    }
543
}
544