Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
IpRange
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
16
100.00% covered (success)
100.00%
1 / 1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 contains
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 parse
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Util;
6
7/**
8 * Single IP addresses and CIDR ranges like `10.0.0.0/8` or `fd00::/8`,
9 * IPv4 and IPv6 alike.
10 *
11 * @internal
12 */
13final class IpRange
14{
15    public static function valid(string $range): bool
16    {
17        return self::parse($range) !== null;
18    }
19
20    public static function contains(string $range, string $ip): bool
21    {
22        $parsed = self::parse($range);
23        $address = filter_var($ip, FILTER_VALIDATE_IP) === false ? false : inet_pton($ip);
24
25        if ($parsed === null || $address === false || strlen($address) !== strlen($parsed[0])) {
26            return false;
27        }
28
29        [$network, $bits] = $parsed;
30        $bytes = intdiv($bits, 8);
31
32        if (substr($network, 0, $bytes) !== substr($address, 0, $bytes)) {
33            return false;
34        }
35
36        $rest = $bits % 8;
37
38        if ($rest === 0) {
39            return true;
40        }
41
42        $mask = (0xFF << (8 - $rest)) & 0xFF;
43
44        return (ord($network[$bytes]) & $mask) === (ord($address[$bytes]) & $mask);
45    }
46
47    /** @return ?array{string, int} the network in binary and its prefix length */
48    private static function parse(string $range): ?array
49    {
50        $slash = strpos($range, '/');
51        $ip = $slash === false ? $range : substr($range, 0, $slash);
52        $bits = $slash === false ? null : substr($range, $slash + 1);
53        $network = filter_var($ip, FILTER_VALIDATE_IP) === false ? false : inet_pton($ip);
54
55        if ($network === false) {
56            return null;
57        }
58
59        $max = strlen($network) * 8;
60
61        if ($bits === null) {
62            return [$network, $max];
63        }
64
65        if (preg_match('/^\d{1,3}$/', $bits) !== 1 || (int) $bits > $max) {
66            return null;
67        }
68
69        return [$network, (int) $bits];
70    }
71}