• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

azjezz / psl / 22790737329

07 Mar 2026 03:05AM UTC coverage: 98.571% (+0.02%) from 98.55%
22790737329

Pull #613

github

azjezz
feat: introduce `IP` component

Signed-off-by: azjezz <azjezz@protonmail.com>
Pull Request #613: feat: introduce `IP` component

130 of 130 new or added lines in 3 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

9518 of 9656 relevant lines covered (98.57%)

34.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.43
/src/Psl/CIDR/Block.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\CIDR;
6

7
use function chr;
8
use function count;
9
use function ctype_digit;
10
use function explode;
11
use function inet_pton;
12
use function str_pad;
13
use function str_repeat;
14
use function strlen;
15

16
/**
17
 * Represents a CIDR (Classless Inter-Domain Routing) block.
18
 *
19
 * Supports both IPv4 and IPv6 addresses. IPv4 addresses are internally
20
 * converted to IPv4-mapped IPv6 for unified comparison.
21
 *
22
 * Usage:
23
 *   $block = new Block('192.168.1.0/24');
24
 *   $block->contains('192.168.1.100'); // true
25
 *   $block->contains('192.168.2.1');   // false
26
 *
27
 * @psalm-immutable
28
 *
29
 * @mago-expect analysis:invalid-operand - bitwise on strings is okay.
30
 */
31
final readonly class Block
32
{
33
    private string $networkBytes;
34
    private string $maskBytes;
35

36
    /**
37
     * @param non-empty-string $cidr CIDR notation, e.g. "192.168.1.0/24" or "2001:db8::/32".
38
     *
39
     * @throws Exception\InvalidArgumentException If the CIDR notation is invalid.
40
     */
41
    public function __construct(string $cidr)
42
    {
43
        $parts = explode('/', $cidr, 2);
22✔
44
        if (count($parts) !== 2) {
22✔
45
            throw new Exception\InvalidArgumentException(
1✔
46
                "Invalid CIDR notation: '{$cidr}'. Expected format: 'address/prefix'.",
1✔
47
            );
1✔
48
        }
49

50
        [$address, $prefixStr] = $parts;
21✔
51

52
        if (!ctype_digit($prefixStr)) {
21✔
53
            throw new Exception\InvalidArgumentException(
3✔
54
                "Invalid prefix length: '{$prefixStr}'. Must be a non-negative integer.",
3✔
55
            );
3✔
56
        }
57

58
        $networkBytes = inet_pton($address);
18✔
59
        if ($networkBytes === false) {
18✔
60
            throw new Exception\InvalidArgumentException("Invalid IP address in CIDR notation: '{$address}'.");
1✔
61
        }
62

63
        // Normalize IPv4 to IPv4-mapped IPv6
64
        $prefix = (int) $prefixStr;
17✔
65
        if (strlen($networkBytes) === 4) {
17✔
66
            $networkBytes = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $networkBytes;
11✔
67
            $prefix += 96;
11✔
68
        }
69

70
        if ($prefix < 0 || $prefix > 128) {
17✔
UNCOV
71
            throw new Exception\InvalidArgumentException(
×
UNCOV
72
                "Invalid prefix length: {$prefixStr}. Must be 0-32 for IPv4 or 0-128 for IPv6.",
×
73
            );
×
74
        }
75

76
        // Build the bitmask
77
        $fullBytes = (int) ($prefix / 8);
17✔
78
        $remainingBits = $prefix % 8;
17✔
79

80
        $mask = str_repeat("\xff", $fullBytes);
17✔
81
        if ($remainingBits > 0) {
17✔
82
            $mask .= chr((0xFF << (8 - $remainingBits)) & 0xFF);
4✔
83
        }
84

85
        $mask = str_pad($mask, 16, "\x00");
17✔
86

87
        $this->networkBytes = $networkBytes;
17✔
88
        $this->maskBytes = $mask;
17✔
89
    }
90

91
    /**
92
     * Check whether the given IP address falls within this CIDR block.
93
     *
94
     * @param non-empty-string $ip IPv4 or IPv6 address to check.
95
     */
96
    public function contains(string $ip): bool
97
    {
98
        $ipBytes = inet_pton($ip);
17✔
99
        if ($ipBytes === false) {
17✔
100
            return false;
1✔
101
        }
102

103
        // Normalize IPv4 to IPv4-mapped IPv6
104
        if (strlen($ipBytes) === 4) {
16✔
105
            $ipBytes = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . $ipBytes;
12✔
106
        }
107

108
        // Compare: (ip & mask) === (network & mask)
109
        return ($ipBytes & $this->maskBytes) === ($this->networkBytes & $this->maskBytes);
16✔
110
    }
111
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc