Issues (37)

src/Address/IPv6.php (5 issues)

Labels
Severity
1
<?php
2
3
namespace IPLib\Address;
4
5
use IPLib\ParseStringFlag;
6
use IPLib\Range\RangeInterface;
7
use IPLib\Range\Subnet;
8
use IPLib\Range\Type as RangeType;
9
10
/**
11
 * An IPv6 address.
12
 */
13
class IPv6 implements AddressInterface
14
{
15
    /**
16
     * The long string representation of the address.
17
     *
18
     * @var string
19
     *
20
     * @example '0000:0000:0000:0000:0000:0000:0000:0001'
21
     */
22
    protected $longAddress;
23
24
    /**
25
     * The long string representation of the address.
26
     *
27
     * @var string|null
28
     *
29
     * @example '::1'
30
     */
31
    protected $shortAddress;
32
33
    /**
34
     * The byte list of the IP address.
35
     *
36
     * @var int[]|null
37
     */
38
    protected $bytes;
39
40
    /**
41
     * The word list of the IP address.
42
     *
43
     * @var int[]|null
44
     */
45
    protected $words;
46
47
    /**
48
     * The type of the range of this IP address.
49
     *
50
     * @var int|null
51
     */
52
    protected $rangeType;
53
54
    /**
55
     * An array containing RFC designated address ranges.
56
     *
57
     * @var array|null
58
     */
59
    private static $reservedRanges;
60
61
    /**
62
     * Initializes the instance.
63
     *
64
     * @param string $longAddress
65
     */
66 709
    public function __construct($longAddress)
67
    {
68 709
        $this->longAddress = $longAddress;
69 709
        $this->shortAddress = null;
70 709
        $this->bytes = null;
71 709
        $this->words = null;
72 709
        $this->rangeType = null;
73 709
    }
74
75
    /**
76
     * {@inheritdoc}
77
     *
78
     * @see \IPLib\Address\AddressInterface::__toString()
79
     */
80 215
    public function __toString()
81
    {
82 215
        return $this->toString();
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     *
88
     * @see \IPLib\Address\AddressInterface::getNumberOfBits()
89
     */
90 33
    public static function getNumberOfBits()
91
    {
92 33
        return 128;
93
    }
94
95
    /**
96
     * @deprecated since 1.17.0: use the parseString() method instead.
97
     * For upgrading:
98
     * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
99
     * - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag
100
     *
101
     * @param string|mixed $address
102
     * @param bool $mayIncludePort
103
     * @param bool $mayIncludeZoneID
104
     *
105
     * @return static|null
106
     *
107
     * @see \IPLib\Address\IPv6::parseString()
108
     * @since 1.1.0 added the $mayIncludePort argument
109
     * @since 1.3.0 added the $mayIncludeZoneID argument
110
     */
111 20
    public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
112
    {
113 20
        return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0));
114
    }
115
116
    /**
117
     * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
118
     *
119
     * @param string|mixed $address the address to parse
120
     * @param int $flags A combination or zero or more flags
121
     *
122
     * @return static|null
123
     *
124
     * @see \IPLib\ParseStringFlag
125
     * @since 1.17.0
126
     */
127 788
    public static function parseString($address, $flags = 0)
128
    {
129 788
        if (!is_string($address)) {
130 3
            return null;
131
        }
132 785
        $matches = null;
133 785
        $flags = (int) $flags;
134 785
        if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) {
135 5
            if (preg_match('/^([0-9a-f](?:\.[0-9a-f]){31})\.ip6\.arpa\.?/i', $address, $matches)) {
136 5
                $nibbles = array_reverse(explode('.', $matches[1]));
137 5
                $quibbles = array();
138 5
                foreach (array_chunk($nibbles, 4) as $n) {
139 5
                    $quibbles[] = implode('', $n);
140
                }
141 5
                $address = implode(':', $quibbles);
142
            }
143
        }
144 785
        $result = null;
145 785
        if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) {
146 722
            if ($flags & ParseStringFlag::MAY_INCLUDE_PORT && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) {
147 2
                $address = $matches[1];
148
            }
149 722
            if ($flags & ParseStringFlag::MAY_INCLUDE_ZONEID) {
150 243
                $percentagePos = strpos($address, '%');
151 243
                if ($percentagePos > 0) {
152 4
                    $address = substr($address, 0, $percentagePos);
153
                }
154
            }
155 722
            if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
156 9
                $address6 = static::parseString($matches[1] . '0:0');
157 9
                if ($address6 !== null) {
158 6
                    $address4 = IPv4::parseString($matches[2]);
159 6
                    if ($address4 !== null) {
160 6
                        $bytes4 = $address4->getBytes();
161 6
                        $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
162 9
                        $result = $address6;
163
                    }
164
                }
165
            } else {
166 722
                if (strpos($address, '::') === false) {
167 413
                    $chunks = explode(':', $address);
168
                } else {
169 330
                    $chunks = array();
170 330
                    $parts = explode('::', $address);
171 330
                    if (count($parts) === 2) {
172 328
                        $before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
173 328
                        $after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
174 328
                        $missing = 8 - count($before) - count($after);
175 328
                        if ($missing >= 0) {
176 326
                            $chunks = $before;
177 326
                            if ($missing !== 0) {
178 325
                                $chunks = array_merge($chunks, array_fill(0, $missing, '0'));
179
                            }
180 326
                            $chunks = array_merge($chunks, $after);
181
                        }
182
                    }
183
                }
184 722
                if (count($chunks) === 8) {
185 713
                    $nums = array_map(
186
                        function ($chunk) {
187 713
                            return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
188 713
                        },
189 713
                        $chunks
190
                    );
191 713
                    if (!in_array(false, $nums, true)) {
192 705
                        $longAddress = implode(
193 705
                            ':',
194 705
                            array_map(
195
                                function ($num) {
196 705
                                    return sprintf('%04x', $num);
197 705
                                },
198 705
                                $nums
199
                            )
200
                        );
201 705
                        $result = new static($longAddress);
202
                    }
203
                }
204
            }
205
        }
206
207 785
        return $result;
208
    }
209
210
    /**
211
     * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
212
     *
213
     * @param int[]|array $bytes
214
     *
215
     * @return static|null
216
     */
217 349
    public static function fromBytes(array $bytes)
218
    {
219 349
        $result = null;
220 349
        if (count($bytes) === 16) {
221 329
            $address = '';
222 329
            for ($i = 0; $i < 16; $i++) {
223 329
                if ($i !== 0 && $i % 2 === 0) {
224 329
                    $address .= ':';
225
                }
226 329
                $byte = $bytes[$i];
227 329
                if (is_int($byte) && $byte >= 0 && $byte <= 255) {
228 329
                    $address .= sprintf('%02x', $byte);
229
                } else {
230
                    $address = null;
231
                    break;
232
                }
233
            }
234 329
            if ($address !== null) {
235 329
                $result = new static($address);
236
            }
237
        }
238
239 349
        return $result;
240
    }
241
242
    /**
243
     * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
244
     *
245
     * @param int[]|array $words
246
     *
247
     * @return static|null
248
     */
249 218
    public static function fromWords(array $words)
250
    {
251 218
        $result = null;
252 218
        if (count($words) === 8) {
253 198
            $chunks = array();
254 198
            for ($i = 0; $i < 8; $i++) {
255 198
                $word = $words[$i];
256 198
                if (is_int($word) && $word >= 0 && $word <= 0xffff) {
257 198
                    $chunks[] = sprintf('%04x', $word);
258
                } else {
259
                    $chunks = null;
260
                    break;
261
                }
262
            }
263 198
            if ($chunks !== null) {
264 198
                $result = new static(implode(':', $chunks));
265
            }
266
        }
267
268 218
        return $result;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     *
274
     * @see \IPLib\Address\AddressInterface::toString()
275
     */
276 518
    public function toString($long = false)
277
    {
278 518
        if ($long) {
279 47
            $result = $this->longAddress;
280
        } else {
281 500
            if ($this->shortAddress === null) {
282 500
                if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
283 8
                    $lastBytes = array_slice($this->getBytes(), -4);
284 8
                    $this->shortAddress = '::ffff:' . implode('.', $lastBytes);
285
                } else {
286 492
                    $chunks = array_map(
287
                        function ($word) {
288 492
                            return dechex($word);
289 492
                        },
290 492
                        $this->getWords()
291
                    );
292 492
                    $shortAddress = implode(':', $chunks);
293 492
                    $matches = null;
294 492
                    for ($i = 8; $i > 1; $i--) {
295 492
                        $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)';
296 492
                        if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) {
297 434
                            $shortAddress = $matches[1] . '::' . $matches[2];
298 434
                            break;
299
                        }
300
                    }
301 492
                    $this->shortAddress = $shortAddress;
302
                }
303
            }
304 500
            $result = $this->shortAddress;
305
        }
306
307 518
        return $result;
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     *
313
     * @see \IPLib\Address\AddressInterface::getBytes()
314
     */
315 482
    public function getBytes()
316
    {
317 482
        if ($this->bytes === null) {
318 482
            $bytes = array();
319 482
            foreach ($this->getWords() as $word) {
320 482
                $bytes[] = $word >> 8;
321 482
                $bytes[] = $word & 0xff;
322
            }
323 482
            $this->bytes = $bytes;
324
        }
325
326 482
        return $this->bytes;
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     *
332
     * @see \IPLib\Address\AddressInterface::getBits()
333
     */
334 159
    public function getBits()
335
    {
336 159
        $parts = array();
337 159
        foreach ($this->getBytes() as $byte) {
338 159
            $parts[] = sprintf('%08b', $byte);
339
        }
340
341 159
        return implode('', $parts);
342
    }
343
344
    /**
345
     * Get the word list of the IP address.
346
     *
347
     * @return int[]
348
     */
349 687
    public function getWords()
350
    {
351 687
        if ($this->words === null) {
352 687
            $this->words = array_map(
353
                function ($chunk) {
354 687
                    return hexdec($chunk);
355 687
                },
356 687
                explode(':', $this->longAddress)
357
            );
358
        }
359
360 687
        return $this->words;
361
    }
362
363
    /**
364
     * {@inheritdoc}
365
     *
366
     * @see \IPLib\Address\AddressInterface::getAddressType()
367
     */
368 296
    public function getAddressType()
369
    {
370 296
        return Type::T_IPv6;
371
    }
372
373
    /**
374
     * {@inheritdoc}
375
     *
376
     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
377
     */
378 100
    public static function getDefaultReservedRangeType()
379
    {
380 100
        return RangeType::T_RESERVED;
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     *
386
     * @see \IPLib\Address\AddressInterface::getReservedRanges()
387
     */
388 116
    public static function getReservedRanges()
389
    {
390 116
        if (self::$reservedRanges === null) {
391 1
            $reservedRanges = array();
392
            foreach (array(
393
                // RFC 4291
394 1
                '::/128' => array(RangeType::T_UNSPECIFIED),
395
                // RFC 4291
396
                '::1/128' => array(RangeType::T_LOOPBACK),
397
                // RFC 4291
398
                '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
399
                //'2002::/16' => array(RangeType::),
400
                // RFC 4291
401
                '2000::/3' => array(RangeType::T_PUBLIC),
402
                // RFC 4193
403
                'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
404
                // RFC 4291
405
                'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
406
                // RFC 4291
407
                'ff00::/8' => array(RangeType::T_MULTICAST),
408
                // RFC 4291
409
                //'::/8' => array(RangeType::T_RESERVED),
410
                // RFC 4048
411
                //'200::/7' => array(RangeType::T_RESERVED),
412
                // RFC 4291
413
                //'400::/6' => array(RangeType::T_RESERVED),
414
                // RFC 4291
415
                //'800::/5' => array(RangeType::T_RESERVED),
416
                // RFC 4291
417
                //'1000::/4' => array(RangeType::T_RESERVED),
418
                // RFC 4291
419
                //'4000::/3' => array(RangeType::T_RESERVED),
420
                // RFC 4291
421
                //'6000::/3' => array(RangeType::T_RESERVED),
422
                // RFC 4291
423
                //'8000::/3' => array(RangeType::T_RESERVED),
424
                // RFC 4291
425
                //'a000::/3' => array(RangeType::T_RESERVED),
426
                // RFC 4291
427
                //'c000::/3' => array(RangeType::T_RESERVED),
428
                // RFC 4291
429
                //'e000::/4' => array(RangeType::T_RESERVED),
430
                // RFC 4291
431
                //'f000::/5' => array(RangeType::T_RESERVED),
432
                // RFC 4291
433
                //'f800::/6' => array(RangeType::T_RESERVED),
434
                // RFC 4291
435
                //'fe00::/9' => array(RangeType::T_RESERVED),
436
                // RFC 3879
437
                //'fec0::/10' => array(RangeType::T_RESERVED),
438
            ) as $range => $data) {
439 1
                $exceptions = array();
440 1
                if (isset($data[1])) {
441 1
                    foreach ($data[1] as $exceptionRange => $exceptionType) {
442 1
                        $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType);
0 ignored issues
show
It seems like IPLib\Range\Subnet::parseString($exceptionRange) can also be of type null; however, parameter $range of IPLib\Address\AssignedRange::__construct() does only seem to accept IPLib\Range\RangeInterface, 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

442
                        $exceptions[] = new AssignedRange(/** @scrutinizer ignore-type */ Subnet::parseString($exceptionRange), $exceptionType);
Loading history...
443
                    }
444
                }
445 1
                $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions);
446
            }
447 1
            self::$reservedRanges = $reservedRanges;
448
        }
449
450 116
        return self::$reservedRanges;
451
    }
452
453
    /**
454
     * {@inheritdoc}
455
     *
456
     * @see \IPLib\Address\AddressInterface::getRangeType()
457
     */
458 59
    public function getRangeType()
459
    {
460 59
        if ($this->rangeType === null) {
461 59
            $ipv4 = $this->toIPv4();
462 59
            if ($ipv4 !== null) {
463 6
                $this->rangeType = $ipv4->getRangeType();
464
            } else {
465 53
                $rangeType = null;
466 53
                foreach (static::getReservedRanges() as $reservedRange) {
467 53
                    $rangeType = $reservedRange->getAddressType($this);
468 53
                    if ($rangeType !== null) {
469 16
                        break;
470
                    }
471
                }
472 53
                $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
473
            }
474
        }
475
476 59
        return $this->rangeType;
477
    }
478
479
    /**
480
     * Create an IPv4 representation of this address (if possible, otherwise returns null).
481
     *
482
     * @return \IPLib\Address\IPv4|null
483
     */
484 76
    public function toIPv4()
485
    {
486 76
        if (strpos($this->longAddress, '2002:') === 0) {
487
            // 6to4
488 19
            return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
489
        }
490 57
        if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
491
            // IPv4-mapped IPv6 addresses
492 4
            return IPv4::fromBytes(array_slice($this->getBytes(), -4));
493
        }
494
495 53
        return null;
496
    }
497
498
    /**
499
     * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax.
500
     *
501
     * @param bool $ipV6Long render the IPv6 part in "long" format?
502
     * @param bool $ipV4Long render the IPv4 part in "long" format?
503
     *
504
     * @return string
505
     *
506
     * @example '::13.1.68.3'
507
     * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true
508
     * @example '::013.001.068.003' when $ipV4Long is true
509
     * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true
510
     *
511
     * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3.
512
     * @since 1.9.0
513
     */
514 6
    public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false)
515
    {
516 6
        $myBytes = $this->getBytes();
517 6
        $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff));
518 6
        $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long);
519 6
        $ipv4Bytes = array_slice($myBytes, 12, 4);
520 6
        $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long);
521
522 6
        return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String);
523
    }
524
525
    /**
526
     * {@inheritdoc}
527
     *
528
     * @see \IPLib\Address\AddressInterface::getComparableString()
529
     */
530 186
    public function getComparableString()
531
    {
532 186
        return $this->longAddress;
533
    }
534
535
    /**
536
     * {@inheritdoc}
537
     *
538
     * @see \IPLib\Address\AddressInterface::matches()
539
     */
540 13
    public function matches(RangeInterface $range)
541
    {
542 13
        return $range->contains($this);
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     *
548
     * @see \IPLib\Address\AddressInterface::getAddressAtOffset()
549
     */
550 31
    public function getAddressAtOffset($n)
551
    {
552 31
        if (!is_int($n)) {
553 1
            return null;
554
        }
555
556 30
        $boundary = 0x10000;
557 30
        $mod = $n;
558 30
        $words = $this->getWords();
559 30
        for ($i = count($words) - 1; $i >= 0; $i--) {
560 30
            $tmp = ($words[$i] + $mod) % $boundary;
561 30
            $mod = (int) floor(($words[$i] + $mod) / $boundary);
562 30
            if ($tmp < 0) {
563 13
                $tmp += $boundary;
564
            }
565
566 30
            $words[$i] = $tmp;
567
        }
568
569 30
        if ($mod !== 0) {
570 5
            return null;
571
        }
572
573 27
        return static::fromWords($words);
574
    }
575
576
    /**
577
     * {@inheritdoc}
578
     *
579
     * @see \IPLib\Address\AddressInterface::getNextAddress()
580
     */
581 8
    public function getNextAddress()
582
    {
583 8
        return $this->getAddressAtOffset(1);
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     *
589
     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
590
     */
591 8
    public function getPreviousAddress()
592
    {
593 8
        return $this->getAddressAtOffset(-1);
594
    }
595
596
    /**
597
     * {@inheritdoc}
598
     *
599
     * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
600
     */
601 22
    public function getReverseDNSLookupName()
602
    {
603 22
        return implode(
604 22
            '.',
605 22
            array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1))
0 ignored issues
show
It seems like str_split(str_replace(':...is->toString(true)), 1) can also be of type true; however, parameter $array of array_reverse() does only seem to accept array, 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

605
            array_reverse(/** @scrutinizer ignore-type */ str_split(str_replace(':', '', $this->toString(true)), 1))
Loading history...
606 22
        ) . '.ip6.arpa';
607
    }
608
609
    /**
610
     * {@inheritdoc}
611
     *
612
     * @see \IPLib\Address\AddressInterface::shift()
613
     */
614 152
    public function shift($bits)
615
    {
616 152
        $bits = (int) $bits;
617 152
        if ($bits === 0) {
618 8
            return $this;
619
        }
620 144
        $absBits = abs($bits);
621 144
        if ($absBits >= 128) {
622 16
            return new self('0000:0000:0000:0000:0000:0000:0000:0000');
623
        }
624 128
        $pad = str_repeat('0', $absBits);
0 ignored issues
show
It seems like $absBits can also be of type double; however, parameter $times of str_repeat() does only seem to accept integer, 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

624
        $pad = str_repeat('0', /** @scrutinizer ignore-type */ $absBits);
Loading history...
625 128
        $paddedBits = $this->getBits();
626 128
        if ($bits > 0) {
627 57
            $paddedBits = $pad . substr($paddedBits, 0, -$bits);
628
        } else {
629 71
            $paddedBits = substr($paddedBits, $absBits) . $pad;
0 ignored issues
show
It seems like $absBits can also be of type double; however, parameter $offset of substr() does only seem to accept integer, 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

629
            $paddedBits = substr($paddedBits, /** @scrutinizer ignore-type */ $absBits) . $pad;
Loading history...
630
        }
631 128
        $bytes = array_map('bindec', str_split($paddedBits, 16));
0 ignored issues
show
It seems like str_split($paddedBits, 16) can also be of type true; however, parameter $array of array_map() does only seem to accept array, 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

631
        $bytes = array_map('bindec', /** @scrutinizer ignore-type */ str_split($paddedBits, 16));
Loading history...
632
633 128
        return static::fromWords($bytes);
634
    }
635
636
    /**
637
     * {@inheritdoc}
638
     *
639
     * @see \IPLib\Address\AddressInterface::add()
640
     */
641 18
    public function add(AddressInterface $other)
642
    {
643 18
        if (!$other instanceof self) {
644 2
            return null;
645
        }
646 16
        $myWords = $this->getWords();
647 16
        $otherWords = $other->getWords();
648 16
        $sum = array_fill(0, 8, 0);
649 16
        $carry = 0;
650 16
        for ($index = 7; $index >= 0; $index--) {
651 16
            $word = $myWords[$index] + $otherWords[$index] + $carry;
652 16
            if ($word > 0xFFFF) {
653 7
                $carry = $word >> 16;
654 7
                $word &= 0xFFFF;
655
            } else {
656 16
                $carry = 0;
657
            }
658 16
            $sum[$index] = $word;
659
        }
660 16
        if ($carry !== 0) {
661 2
            return null;
662
        }
663
664 15
        return static::fromWords($sum);
665
    }
666
}
667