Completed
Pull Request — master (#60)
by Michele
08:45
created

IPv6::getAddressAtOffset()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 25

Duplication

Lines 25
Ratio 100 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
dl 25
loc 25
ccs 12
cts 12
cp 1
rs 9.2088
c 0
b 0
f 0
cc 5
nc 7
nop 1
crap 5
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 494
    public function __construct($longAddress)
66
    {
67 494
        $this->longAddress = $longAddress;
68 494
        $this->shortAddress = null;
69 494
        $this->bytes = null;
70 494
        $this->words = null;
71 494
        $this->rangeType = null;
72 494
    }
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
     * {@inheritdoc}
86
     *
87
     * @see \IPLib\Address\AddressInterface::getNumberOfBits()
88
     */
89 21
    public static function getNumberOfBits()
90
    {
91 21
        return 128;
92
    }
93
94
    /**
95
     * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
96
     *
97
     * @param string|mixed $address the address to parse
98
     * @param bool $mayIncludePort set to false to avoid parsing addresses with ports
99
     * @param bool $mayIncludeZoneID set to false to avoid parsing addresses with zone IDs (see RFC 4007)
100
     *
101
     * @return static|null
102
     */
103 548
    public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
104
    {
105 548
        $result = null;
106 548
        if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) {
107 507
            $matches = null;
108 507
            if ($mayIncludePort && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) {
109 2
                $address = $matches[1];
110
            }
111 507
            if ($mayIncludeZoneID) {
112 507
                $percentagePos = strpos($address, '%');
113 507
                if ($percentagePos > 0) {
114 4
                    $address = substr($address, 0, $percentagePos);
115
                }
116
            }
117 507
            if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
118 9
                $address6 = static::fromString($matches[1] . '0:0', false);
119 9
                if ($address6 !== null) {
120 6
                    $address4 = IPv4::fromString($matches[2], false);
121 6
                    if ($address4 !== null) {
122 6
                        $bytes4 = $address4->getBytes();
123 6
                        $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
124 9
                        $result = $address6;
125
                    }
126
                }
127
            } else {
128 507
                if (strpos($address, '::') === false) {
129 385
                    $chunks = explode(':', $address);
130
                } else {
131 136
                    $chunks = array();
132 136
                    $parts = explode('::', $address);
133 136
                    if (count($parts) === 2) {
134 134
                        $before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
135 134
                        $after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
136 134
                        $missing = 8 - count($before) - count($after);
137 134
                        if ($missing >= 0) {
138 132
                            $chunks = $before;
139 132
                            if ($missing !== 0) {
140 131
                                $chunks = array_merge($chunks, array_fill(0, $missing, '0'));
141
                            }
142 132
                            $chunks = array_merge($chunks, $after);
143
                        }
144
                    }
145
                }
146 507
                if (count($chunks) === 8) {
147 498
                    $nums = array_map(
148 498
                        function ($chunk) {
149 498
                            return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
150 498
                        },
151 498
                        $chunks
152
                    );
153 498
                    if (!in_array(false, $nums, true)) {
154 490
                        $longAddress = implode(
155 490
                            ':',
156 490
                            array_map(
157 490
                                function ($num) {
158 490
                                    return sprintf('%04x', $num);
159 490
                                },
160 490
                                $nums
161
                            )
162
                        );
163 490
                        $result = new static($longAddress);
164
                    }
165
                }
166
            }
167
        }
168
169 548
        return $result;
170
    }
171
172
    /**
173
     * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
174
     *
175
     * @param int[]|array $bytes
176
     *
177
     * @return static|null
178
     */
179 319
    public static function fromBytes(array $bytes)
180
    {
181 319
        $result = null;
182 319
        if (count($bytes) === 16) {
183 299
            $address = '';
184 299
            for ($i = 0; $i < 16; $i++) {
185 299
                if ($i !== 0 && $i % 2 === 0) {
186 299
                    $address .= ':';
187
                }
188 299
                $byte = $bytes[$i];
189 299
                if (is_int($byte) && $byte >= 0 && $byte <= 255) {
190 299
                    $address .= sprintf('%02x', $byte);
191
                } else {
192
                    $address = null;
193
                    break;
194
                }
195
            }
196 299
            if ($address !== null) {
197 299
                $result = new static($address);
198
            }
199
        }
200
201 319
        return $result;
202
    }
203
204
    /**
205
     * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
206
     *
207
     * @param int[]|array $words
208
     *
209
     * @return static|null
210
     */
211 56
    public static function fromWords(array $words)
212
    {
213 56
        $result = null;
214 56
        if (count($words) === 8) {
215 36
            $chunks = array();
216 36
            for ($i = 0; $i < 8; $i++) {
217 36
                $word = $words[$i];
218 36
                if (is_int($word) && $word >= 0 && $word <= 0xffff) {
219 36
                    $chunks[] = sprintf('%04x', $word);
220
                } else {
221
                    $chunks = null;
222
                    break;
223
                }
224
            }
225 36
            if ($chunks !== null) {
226 36
                $result = new static(implode(':', $chunks));
227
            }
228
        }
229
230 56
        return $result;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     *
236
     * @see \IPLib\Address\AddressInterface::toString()
237
     */
238 330
    public function toString($long = false)
239
    {
240 330
        if ($long) {
241 55
            $result = $this->longAddress;
242
        } else {
243 307
            if ($this->shortAddress === null) {
244 307
                if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
245 7
                    $lastBytes = array_slice($this->getBytes(), -4);
246 7
                    $this->shortAddress = '::ffff:' . implode('.', $lastBytes);
247
                } else {
248 300
                    $chunks = array_map(
249 300
                        function ($word) {
250 300
                            return dechex($word);
251 300
                        },
252 300
                        $this->getWords()
253
                    );
254 300
                    $shortAddress = implode(':', $chunks);
255 300
                    $matches = null;
256 300
                    for ($i = 8; $i > 1; $i--) {
257 300
                        $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)';
258 300
                        if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) {
259 242
                            $shortAddress = $matches[1] . '::' . $matches[2];
260 242
                            break;
261
                        }
262
                    }
263 300
                    $this->shortAddress = $shortAddress;
264
                }
265
            }
266 307
            $result = $this->shortAddress;
267
        }
268
269 330
        return $result;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     *
275
     * @see \IPLib\Address\AddressInterface::getBytes()
276
     */
277 327
    public function getBytes()
278
    {
279 327
        if ($this->bytes === null) {
280 327
            $bytes = array();
281 327
            foreach ($this->getWords() as $word) {
282 327
                $bytes[] = $word >> 8;
283 327
                $bytes[] = $word & 0xff;
284
            }
285 327
            $this->bytes = $bytes;
286
        }
287
288 327
        return $this->bytes;
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     *
294
     * @see \IPLib\Address\AddressInterface::getBits()
295
     */
296 31 View Code Duplication
    public function getBits()
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...
297
    {
298 31
        $parts = array();
299 31
        foreach ($this->getBytes() as $byte) {
300 31
            $parts[] = sprintf('%08b', $byte);
301
        }
302
303 31
        return implode('', $parts);
304
    }
305
306
    /**
307
     * Get the word list of the IP address.
308
     *
309
     * @return int[]
310
     */
311 476
    public function getWords()
312
    {
313 476
        if ($this->words === null) {
314 476
            $this->words = array_map(
315 476
                function ($chunk) {
316 476
                    return hexdec($chunk);
317 476
                },
318 476
                explode(':', $this->longAddress)
319
            );
320
        }
321
322 476
        return $this->words;
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     *
328
     * @see \IPLib\Address\AddressInterface::getAddressType()
329
     */
330 273
    public function getAddressType()
331
    {
332 273
        return Type::T_IPv6;
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     *
338
     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
339
     */
340 100
    public static function getDefaultReservedRangeType()
341
    {
342 100
        return RangeType::T_RESERVED;
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     *
348
     * @see \IPLib\Address\AddressInterface::getReservedRanges()
349
     */
350 116
    public static function getReservedRanges()
351
    {
352 116
        if (self::$reservedRanges === null) {
353 1
            $reservedRanges = array();
354
            foreach (array(
355
                // RFC 4291
356 1
                '::/128' => array(RangeType::T_UNSPECIFIED),
357
                // RFC 4291
358
                '::1/128' => array(RangeType::T_LOOPBACK),
359
                // RFC 4291
360
                '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
361
                //'2002::/16' => array(RangeType::),
362
                // RFC 4291
363
                '2000::/3' => array(RangeType::T_PUBLIC),
364
                // RFC 4193
365
                'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
366
                // RFC 4291
367
                'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
368
                // RFC 4291
369
                'ff00::/8' => array(RangeType::T_MULTICAST),
370
                // RFC 4291
371
                //'::/8' => array(RangeType::T_RESERVED),
372
                // RFC 4048
373
                //'200::/7' => array(RangeType::T_RESERVED),
374
                // RFC 4291
375
                //'400::/6' => array(RangeType::T_RESERVED),
376
                // RFC 4291
377
                //'800::/5' => array(RangeType::T_RESERVED),
378
                // RFC 4291
379
                //'1000::/4' => array(RangeType::T_RESERVED),
380
                // RFC 4291
381
                //'4000::/3' => array(RangeType::T_RESERVED),
382
                // RFC 4291
383
                //'6000::/3' => array(RangeType::T_RESERVED),
384
                // RFC 4291
385
                //'8000::/3' => array(RangeType::T_RESERVED),
386
                // RFC 4291
387
                //'a000::/3' => array(RangeType::T_RESERVED),
388
                // RFC 4291
389
                //'c000::/3' => array(RangeType::T_RESERVED),
390
                // RFC 4291
391
                //'e000::/4' => array(RangeType::T_RESERVED),
392
                // RFC 4291
393
                //'f000::/5' => array(RangeType::T_RESERVED),
394
                // RFC 4291
395
                //'f800::/6' => array(RangeType::T_RESERVED),
396
                // RFC 4291
397
                //'fe00::/9' => array(RangeType::T_RESERVED),
398
                // RFC 3879
399
                //'fec0::/10' => array(RangeType::T_RESERVED),
400
            ) as $range => $data) {
401 1
                $exceptions = array();
402 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...
403 1
                    foreach ($data[1] as $exceptionRange => $exceptionType) {
404 1
                        $exceptions[] = new AssignedRange(Subnet::fromString($exceptionRange), $exceptionType);
405
                    }
406
                }
407 1
                $reservedRanges[] = new AssignedRange(Subnet::fromString($range), $data[0], $exceptions);
408
            }
409 1
            self::$reservedRanges = $reservedRanges;
410
        }
411
412 116
        return self::$reservedRanges;
413
    }
414
415
    /**
416
     * {@inheritdoc}
417
     *
418
     * @see \IPLib\Address\AddressInterface::getRangeType()
419
     */
420 59
    public function getRangeType()
421
    {
422 59
        if ($this->rangeType === null) {
423 59
            $ipv4 = $this->toIPv4();
424 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...
425 6
                $this->rangeType = $ipv4->getRangeType();
426
            } else {
427 53
                $rangeType = null;
428 53
                foreach (static::getReservedRanges() as $reservedRange) {
429 53
                    $rangeType = $reservedRange->getAddressType($this);
430 53
                    if ($rangeType !== null) {
431 53
                        break;
432
                    }
433
                }
434 53
                $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
435
            }
436
        }
437
438 59
        return $this->rangeType;
439
    }
440
441
    /**
442
     * Create an IPv4 representation of this address (if possible, otherwise returns null).
443
     *
444
     * @return \IPLib\Address\IPv4|null
445
     */
446 76
    public function toIPv4()
447
    {
448 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...
449
            // 6to4
450 19
            return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
451
        }
452 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...
453
            // IPv4-mapped IPv6 addresses
454 4
            return IPv4::fromBytes(array_slice($this->getBytes(), -4));
455
        }
456
457 53
        return null;
458
    }
459
460
    /**
461
     * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax.
462
     *
463
     * @param bool $ipV6Long render the IPv6 part in "long" format?
464
     * @param bool $ipV4Long render the IPv4 part in "long" format?
465
     *
466
     * @return string
467
     *
468
     * @example '::13.1.68.3'
469
     * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true
470
     * @example '::013.001.068.003' when $ipV4Long is true
471
     * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true
472
     *
473
     * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3.
474
     */
475 6
    public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false)
476
    {
477 6
        $myBytes = $this->getBytes();
478 6
        $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff));
479 6
        $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long);
480 6
        $ipv4Bytes = array_slice($myBytes, 12, 4);
481 6
        $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long);
482
483 6
        return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String);
484
    }
485
486
    /**
487
     * {@inheritdoc}
488
     *
489
     * @see \IPLib\Address\AddressInterface::getComparableString()
490
     */
491 171
    public function getComparableString()
492
    {
493 171
        return $this->longAddress;
494
    }
495
496
    /**
497
     * {@inheritdoc}
498
     *
499
     * @see \IPLib\Address\AddressInterface::matches()
500
     */
501 13
    public function matches(RangeInterface $range)
502
    {
503 13
        return $range->contains($this);
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     *
509
     * @see \IPLib\Address\AddressInterface::getAddressAtOffset()
510
     */
511 8 View Code Duplication
    public function getAddressAtOffset($n)
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
        if (!is_int($n)) {
514 8
            return null;
515 8
        }
516 8
517 5
        $boundary = 0x10000;
518 1
        $mod = $n;
519 1
        $words = $this->getWords();
520
        for ($i = count($words) - 1; $i >= 0; $i--) {
521 5
            $tmp = ($words[$i] + $mod) % $boundary;
522
            $mod = (int) floor(($words[$i] + $mod) / $boundary);
523 8
            if ($tmp < 0) {
524 8
                $tmp += $boundary;
525
            }
526
527
            $words[$i] = $tmp;
528 8
        }
529
530
        if ($mod !== 0) {
531
            return null;
532
        }
533
534
        return static::fromWords($words);
535
    }
536 8
537
    /**
538 8
     * {@inheritdoc}
539 8
     *
540 8
     * @see \IPLib\Address\AddressInterface::getNextAddress()
541 8
     */
542 5
    public function getNextAddress()
543 1
    {
544 1
        return $this->getAddressAtOffset(1);
545
    }
546 5
547
    /**
548 8
     * {@inheritdoc}
549 8
     *
550
     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
551
     */
552
    public function getPreviousAddress()
553 8
    {
554
        return $this->getAddressAtOffset(-1);
555
    }
556
557
    /**
558
     * {@inheritdoc}
559
     *
560
     * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
561 22
     */
562
    public function getReverseDNSLookupName()
563 22
    {
564 22
        return implode(
565 22
            '.',
566 22
            array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1))
567
        ) . '.ip6.arpa';
568
    }
569
}
570