Issues (37)

src/Address/IPv4.php (4 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 IPv4 address.
12
 */
13
class IPv4 implements AddressInterface
14
{
15
    /**
16
     * The string representation of the address.
17
     *
18
     * @var string
19
     *
20
     * @example '127.0.0.1'
21
     */
22
    protected $address;
23
24
    /**
25
     * The byte list of the IP address.
26
     *
27
     * @var int[]|null
28
     */
29
    protected $bytes;
30
31
    /**
32
     * The type of the range of this IP address.
33
     *
34
     * @var int|null
35
     */
36
    protected $rangeType;
37
38
    /**
39
     * A string representation of this address than can be used when comparing addresses and ranges.
40
     *
41
     * @var string
42
     */
43
    protected $comparableString;
44
45
    /**
46
     * An array containing RFC designated address ranges.
47
     *
48
     * @var array|null
49
     */
50
    private static $reservedRanges;
51
52
    /**
53
     * Initializes the instance.
54
     *
55
     * @param string $address
56
     */
57 977
    protected function __construct($address)
58
    {
59 977
        $this->address = $address;
60 977
        $this->bytes = null;
61 977
        $this->rangeType = null;
62 977
        $this->comparableString = null;
63 977
    }
64
65
    /**
66
     * {@inheritdoc}
67
     *
68
     * @see \IPLib\Address\AddressInterface::__toString()
69
     */
70 258
    public function __toString()
71
    {
72 258
        return $this->address;
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     *
78
     * @see \IPLib\Address\AddressInterface::getNumberOfBits()
79
     */
80 60
    public static function getNumberOfBits()
81
    {
82 60
        return 32;
83
    }
84
85
    /**
86
     * @deprecated since 1.17.0: use the parseString() method instead.
87
     * For upgrading:
88
     * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
89
     * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag
90
     *
91
     * @param string|mixed $address the address to parse
92
     * @param bool $mayIncludePort
93
     * @param bool $supportNonDecimalIPv4
94
     *
95
     * @return static|null
96
     *
97
     * @see \IPLib\Address\IPv4::parseString()
98
     * @since 1.1.0 added the $mayIncludePort argument
99
     * @since 1.10.0 added the $supportNonDecimalIPv4 argument
100
     */
101 20
    public static function fromString($address, $mayIncludePort = true, $supportNonDecimalIPv4 = false)
102
    {
103 20
        return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0));
104
    }
105
106
    /**
107
     * Parse a string and returns an IPv4 instance if the string is valid, or null otherwise.
108
     *
109
     * @param string|mixed $address the address to parse
110
     * @param int $flags A combination or zero or more flags
111
     *
112
     * @return static|null
113
     *
114
     * @see \IPLib\ParseStringFlag
115
     * @since 1.17.0
116
     */
117 1691
    public static function parseString($address, $flags = 0)
118
    {
119 1691
        if (!is_string($address)) {
120 3
            return null;
121
        }
122 1688
        $flags = (int) $flags;
123 1688
        $matches = null;
124 1688
        if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) {
125 9
            if (preg_match('/^([12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2})\.in-addr\.arpa\.?$/i', $address, $matches)) {
126 4
                $address = implode('.', array_reverse(explode('.', $matches[1])));
127 4
                $flags = $flags & ~(ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED);
128
            }
129
        }
130 1688
        if ($flags & ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED) {
131 13
            if (strpos($address, '.') === 0) {
132 1
                return null;
133
            }
134 12
            $lengthNonHex = '{1,11}';
135 12
            $lengthHex = '{1,8}';
136 12
            $chunk234Optional = true;
137
        } else {
138 1675
            if (!strpos($address, '.')) {
139 707
                return null;
140
            }
141 991
            $lengthNonHex = '{1,3}';
142 991
            $lengthHex = '{1,2}';
143 991
            $chunk234Optional = false;
144
        }
145 1003
        $rxChunk1 = "0?[0-9]{$lengthNonHex}";
146 1003
        if ($flags & ParseStringFlag::IPV4_MAYBE_NON_DECIMAL) {
147 44
            $rxChunk1 = "(?:0[Xx]0*[0-9A-Fa-f]{$lengthHex})|(?:{$rxChunk1})";
148 44
            $onlyDecimal = false;
149
        } else {
150 971
            $onlyDecimal = true;
151
        }
152 1003
        $rxChunk1 = "0*?({$rxChunk1})";
153 1003
        $rxChunk234 = "\.{$rxChunk1}";
154 1003
        if ($chunk234Optional) {
155 12
            $rxChunk234 = "(?:{$rxChunk234})?";
156
        }
157 1003
        $rx = "{$rxChunk1}{$rxChunk234}{$rxChunk234}{$rxChunk234}";
158 1003
        if ($flags & ParseStringFlag::MAY_INCLUDE_PORT) {
159 455
            $rx .= '(?::\d+)?';
160
        }
161 1003
        if (!preg_match('/^' . $rx . '$/', $address, $matches)) {
162 42
            return null;
163
        }
164 974
        $math = new \IPLib\Service\UnsignedIntegerMath();
165 974
        $nums = array();
166 974
        $maxChunkIndex = count($matches) - 1;
167 974
        for ($i = 1; $i <= $maxChunkIndex; $i++) {
168 974
            $numBytes = $i === $maxChunkIndex ? 5 - $i : 1;
169 974
            $chunkBytes = $math->getBytes($matches[$i], $numBytes, $onlyDecimal);
170 974
            if ($chunkBytes === null) {
171 12
                return null;
172
            }
173 968
            $nums = array_merge($nums, $chunkBytes);
174
        }
175
176 962
        return new static(implode('.', $nums));
177
    }
178
179
    /**
180
     * Parse an array of bytes and returns an IPv4 instance if the array is valid, or null otherwise.
181
     *
182
     * @param int[]|array $bytes
183
     *
184
     * @return static|null
185
     */
186 759
    public static function fromBytes(array $bytes)
187
    {
188 759
        $result = null;
189 759
        if (count($bytes) === 4) {
190 445
            $chunks = array_map(
191
                function ($byte) {
192 445
                    return (is_int($byte) && $byte >= 0 && $byte <= 255) ? (string) $byte : false;
193 445
                },
194 445
                $bytes
195
            );
196 445
            if (in_array(false, $chunks, true) === false) {
197 445
                $result = new static(implode('.', $chunks));
198
            }
199
        }
200
201 759
        return $result;
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     *
207
     * @see \IPLib\Address\AddressInterface::toString()
208
     */
209 651
    public function toString($long = false)
210
    {
211 651
        if ($long) {
212 22
            return $this->getComparableString();
213
        }
214
215 651
        return $this->address;
216
    }
217
218
    /**
219
     * Get the octal representation of this IP address.
220
     *
221
     * @param bool $long
222
     *
223
     * @return string
224
     *
225
     * @since 1.10.0
226
     *
227
     * @example if $long == false: if the decimal representation is '0.7.8.255': '0.7.010.0377'
228
     * @example if $long == true: if the decimal representation is '0.7.8.255': '0000.0007.0010.0377'
229
     */
230 12
    public function toOctal($long = false)
231
    {
232 12
        $chunks = array();
233 12
        foreach ($this->getBytes() as $byte) {
234 12
            if ($long) {
235 12
                $chunks[] = sprintf('%04o', $byte);
236
            } else {
237 12
                $chunks[] = '0' . decoct($byte);
238
            }
239
        }
240
241 12
        return implode('.', $chunks);
242
    }
243
244
    /**
245
     * Get the hexadecimal representation of this IP address.
246
     *
247
     * @param bool $long
248
     *
249
     * @return string
250
     *
251
     * @since 1.10.0
252
     *
253
     * @example if $long == false: if the decimal representation is '0.9.10.255': '0.9.0xa.0xff'
254
     * @example if $long == true: if the decimal representation is '0.9.10.255': '0x00.0x09.0x0a.0xff'
255
     */
256 12
    public function toHexadecimal($long = false)
257
    {
258 12
        $chunks = array();
259 12
        foreach ($this->getBytes() as $byte) {
260 12
            if ($long) {
261 12
                $chunks[] = sprintf('0x%02x', $byte);
262
            } else {
263 12
                $chunks[] = '0x' . dechex($byte);
264
            }
265
        }
266
267 12
        return implode('.', $chunks);
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     *
273
     * @see \IPLib\Address\AddressInterface::getBytes()
274
     */
275 753
    public function getBytes()
276
    {
277 753
        if ($this->bytes === null) {
278 753
            $this->bytes = array_map(
279
                function ($chunk) {
280 753
                    return (int) $chunk;
281 753
                },
282 753
                explode('.', $this->address)
283
            );
284
        }
285
286 753
        return $this->bytes;
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     *
292
     * @see \IPLib\Address\AddressInterface::getBits()
293
     */
294 144
    public function getBits()
295
    {
296 144
        $parts = array();
297 144
        foreach ($this->getBytes() as $byte) {
298 144
            $parts[] = sprintf('%08b', $byte);
299
        }
300
301 144
        return implode('', $parts);
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     *
307
     * @see \IPLib\Address\AddressInterface::getAddressType()
308
     */
309 611
    public function getAddressType()
310
    {
311 611
        return Type::T_IPv4;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     *
317
     * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
318
     */
319 173
    public static function getDefaultReservedRangeType()
320
    {
321 173
        return RangeType::T_PUBLIC;
322
    }
323
324
    /**
325
     * {@inheritdoc}
326
     *
327
     * @see \IPLib\Address\AddressInterface::getReservedRanges()
328
     */
329 341
    public static function getReservedRanges()
330
    {
331 341
        if (self::$reservedRanges === null) {
332 1
            $reservedRanges = array();
333
            foreach (array(
334
                // RFC 5735
335 1
                '0.0.0.0/8' => array(RangeType::T_THISNETWORK, array('0.0.0.0/32' => RangeType::T_UNSPECIFIED)),
336
                // RFC 5735
337
                '10.0.0.0/8' => array(RangeType::T_PRIVATENETWORK),
338
                // RFC 6598
339
                '100.64.0.0/10' => array(RangeType::T_CGNAT),
340
                // RFC 5735
341
                '127.0.0.0/8' => array(RangeType::T_LOOPBACK),
342
                // RFC 5735
343
                '169.254.0.0/16' => array(RangeType::T_LINKLOCAL),
344
                // RFC 5735
345
                '172.16.0.0/12' => array(RangeType::T_PRIVATENETWORK),
346
                // RFC 5735
347
                '192.0.0.0/24' => array(RangeType::T_RESERVED),
348
                // RFC 5735
349
                '192.0.2.0/24' => array(RangeType::T_RESERVED),
350
                // RFC 5735
351
                '192.88.99.0/24' => array(RangeType::T_ANYCASTRELAY),
352
                // RFC 5735
353
                '192.168.0.0/16' => array(RangeType::T_PRIVATENETWORK),
354
                // RFC 5735
355
                '198.18.0.0/15' => array(RangeType::T_RESERVED),
356
                // RFC 5735
357
                '198.51.100.0/24' => array(RangeType::T_RESERVED),
358
                // RFC 5735
359
                '203.0.113.0/24' => array(RangeType::T_RESERVED),
360
                // RFC 5735
361
                '224.0.0.0/4' => array(RangeType::T_MULTICAST),
362
                // RFC 5735
363
                '240.0.0.0/4' => array(RangeType::T_RESERVED, array('255.255.255.255/32' => RangeType::T_LIMITEDBROADCAST)),
364
            ) as $range => $data) {
365 1
                $exceptions = array();
366 1
                if (isset($data[1])) {
367 1
                    foreach ($data[1] as $exceptionRange => $exceptionType) {
368 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

368
                        $exceptions[] = new AssignedRange(/** @scrutinizer ignore-type */ Subnet::parseString($exceptionRange), $exceptionType);
Loading history...
369
                    }
370
                }
371 1
                $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions);
372
            }
373 1
            self::$reservedRanges = $reservedRanges;
374
        }
375
376 341
        return self::$reservedRanges;
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     *
382
     * @see \IPLib\Address\AddressInterface::getRangeType()
383
     */
384 171
    public function getRangeType()
385
    {
386 171
        if ($this->rangeType === null) {
387 171
            $rangeType = null;
388 171
            foreach (static::getReservedRanges() as $reservedRange) {
389 171
                $rangeType = $reservedRange->getAddressType($this);
390 171
                if ($rangeType !== null) {
391 168
                    break;
392
                }
393
            }
394 171
            $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
395
        }
396
397 171
        return $this->rangeType;
398
    }
399
400
    /**
401
     * Create an IPv6 representation of this address (in 6to4 notation).
402
     *
403
     * @return \IPLib\Address\IPv6
404
     */
405 4
    public function toIPv6()
406
    {
407 4
        $myBytes = $this->getBytes();
408
409 4
        return IPv6::parseString('2002:' . sprintf('%02x', $myBytes[0]) . sprintf('%02x', $myBytes[1]) . ':' . sprintf('%02x', $myBytes[2]) . sprintf('%02x', $myBytes[3]) . '::');
410
    }
411
412
    /**
413
     * Create an IPv6 representation of this address (in IPv6 IPv4-mapped notation).
414
     *
415
     * @return \IPLib\Address\IPv6
416
     *
417
     * @since 1.11.0
418
     */
419 4
    public function toIPv6IPv4Mapped()
420
    {
421 4
        return IPv6::fromBytes(array_merge(array(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff), $this->getBytes()));
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     *
427
     * @see \IPLib\Address\AddressInterface::getComparableString()
428
     */
429 443
    public function getComparableString()
430
    {
431 443
        if ($this->comparableString === null) {
432 443
            $chunks = array();
433 443
            foreach ($this->getBytes() as $byte) {
434 443
                $chunks[] = sprintf('%03d', $byte);
435
            }
436 443
            $this->comparableString = implode('.', $chunks);
437
        }
438
439 443
        return $this->comparableString;
440
    }
441
442
    /**
443
     * {@inheritdoc}
444
     *
445
     * @see \IPLib\Address\AddressInterface::matches()
446
     */
447 11
    public function matches(RangeInterface $range)
448
    {
449 11
        return $range->contains($this);
450
    }
451
452
    /**
453
     * {@inheritdoc}
454
     *
455
     * @see \IPLib\Address\AddressInterface::getAddressAtOffset()
456
     */
457 29
    public function getAddressAtOffset($n)
458
    {
459 29
        if (!is_int($n)) {
460 1
            return null;
461
        }
462
463 28
        $boundary = 256;
464 28
        $mod = $n;
465 28
        $bytes = $this->getBytes();
466 28
        for ($i = count($bytes) - 1; $i >= 0; $i--) {
467 28
            $tmp = ($bytes[$i] + $mod) % $boundary;
468 28
            $mod = (int) floor(($bytes[$i] + $mod) / $boundary);
469 28
            if ($tmp < 0) {
470 13
                $tmp += $boundary;
471
            }
472
473 28
            $bytes[$i] = $tmp;
474
        }
475
476 28
        if ($mod !== 0) {
477 8
            return null;
478
        }
479
480 22
        return static::fromBytes($bytes);
481
    }
482
483
    /**
484
     * {@inheritdoc}
485
     *
486
     * @see \IPLib\Address\AddressInterface::getNextAddress()
487
     */
488 9
    public function getNextAddress()
489
    {
490 9
        return $this->getAddressAtOffset(1);
491
    }
492
493
    /**
494
     * {@inheritdoc}
495
     *
496
     * @see \IPLib\Address\AddressInterface::getPreviousAddress()
497
     */
498 9
    public function getPreviousAddress()
499
    {
500 9
        return $this->getAddressAtOffset(-1);
501
    }
502
503
    /**
504
     * {@inheritdoc}
505
     *
506
     * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
507
     */
508 14
    public function getReverseDNSLookupName()
509
    {
510 14
        return implode(
511 14
            '.',
512 14
            array_reverse($this->getBytes())
513 14
        ) . '.in-addr.arpa';
514
    }
515
516
    /**
517
     * {@inheritdoc}
518
     *
519
     * @see \IPLib\Address\AddressInterface::shift()
520
     */
521 139
    public function shift($bits)
522
    {
523 139
        $bits = (int) $bits;
524 139
        if ($bits === 0) {
525 9
            return $this;
526
        }
527 130
        $absBits = abs($bits);
528 130
        if ($absBits >= 32) {
529 16
            return new self('0.0.0.0');
530
        }
531 114
        $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

531
        $pad = str_repeat('0', /** @scrutinizer ignore-type */ $absBits);
Loading history...
532 114
        $paddedBits = $this->getBits();
533 114
        if ($bits > 0) {
534 48
            $paddedBits = $pad . substr($paddedBits, 0, -$bits);
535
        } else {
536 66
            $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

536
            $paddedBits = substr($paddedBits, /** @scrutinizer ignore-type */ $absBits) . $pad;
Loading history...
537
        }
538 114
        $bytes = array_map('bindec', str_split($paddedBits, 8));
0 ignored issues
show
It seems like str_split($paddedBits, 8) 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

538
        $bytes = array_map('bindec', /** @scrutinizer ignore-type */ str_split($paddedBits, 8));
Loading history...
539
540 114
        return new static(implode('.', $bytes));
541
    }
542
543
    /**
544
     * {@inheritdoc}
545
     *
546
     * @see \IPLib\Address\AddressInterface::add()
547
     */
548 28
    public function add(AddressInterface $other)
549
    {
550 28
        if (!$other instanceof self) {
551 2
            return null;
552
        }
553 26
        $myBytes = $this->getBytes();
554 26
        $otherBytes = $other->getBytes();
555 26
        $sum = array_fill(0, 4, 0);
556 26
        $carry = 0;
557 26
        for ($index = 3; $index >= 0; $index--) {
558 26
            $byte = $myBytes[$index] + $otherBytes[$index] + $carry;
559 26
            if ($byte > 0xFF) {
560 17
                $carry = $byte >> 8;
561 17
                $byte &= 0xFF;
562
            } else {
563 24
                $carry = 0;
564
            }
565 26
            $sum[$index] = $byte;
566
        }
567 26
        if ($carry !== 0) {
568 3
            return null;
569
        }
570
571 23
        return new static(implode('.', $sum));
572
    }
573
}
574