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

markrogoyski / ipv4-subnet-calculator-php / 20906371849

12 Jan 2026 02:37AM UTC coverage: 97.831% (-1.8%) from 99.588%
20906371849

push

github

markrogoyski
Update CHANGELOG with historic versions.

406 of 415 relevant lines covered (97.83%)

94.41 hits per line

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

99.64
/src/SubnetCalculator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace IPv4;
6

7
/**
8
 * Network calculator for subnet mask and other classless (CIDR) network information.
9
 *
10
 * Given an IP address and CIDR network size, it calculates the following information:
11
 *   - IP address network subnet masks, network and host portions, and provides aggregated reports.
12
 *   - Subnet mask
13
 *   - Network portion
14
 *   - Host portion
15
 *   - Number of IP addresses in the network
16
 *   - Number of addressable hosts in the network
17
 *   - IP address range
18
 *   - Broadcast address
19
 *   - Min and max host
20
 *   - All IP addresses
21
 *   - IPv4 ARPA Domain
22
 * Provides each data in dotted quads, hexadecimal, and binary formats, as well as array of quads.
23
 *
24
 * Aggregated network calculation reports:
25
 *  - Associative array
26
 *  - JSON
27
 *  - String
28
 *  - Printed to STDOUT
29
 *
30
 * Special handling for /31 and /32 networks:
31
 *  - /32: Single host network. Min and max host are the same as the IP address.
32
 *  - /31: Point-to-point link per RFC 3021. Both addresses are usable hosts (no reserved
33
 *         network or broadcast addresses). Min host is the lower IP, max host is the higher IP.
34
 *
35
 * @link https://datatracker.ietf.org/doc/html/rfc3021 RFC 3021 - Using 31-Bit Prefixes on IPv4 Point-to-Point Links
36
 */
37
class SubnetCalculator implements \JsonSerializable
38
{
39
    /** @var string IP address as dotted quads: xxx.xxx.xxx.xxx */
40
    private $ipAddress;
41

42
    /** @var int CIDR network size */
43
    private $networkSize;
44

45
    /** @var string[] of four elements containing the four quads of the IP address */
46
    private $quads = [];
47

48
    /** @var int Subnet mask in format used for subnet calculations */
49
    private $subnetMask;
50

51
    /** @var SubnetReportInterface */
52
    private $report;
53

54
    private const FORMAT_QUADS  = '%d';
55
    private const FORMAT_HEX    = '%02X';
56
    private const FORMAT_BINARY = '%08b';
57

58
    /**
59
     * Constructor - Takes IP address and network size, validates inputs, and assigns class attributes.
60
     * For example: 192.168.1.120/24 would be $ip = 192.168.1.120 $network_size = 24
61
     *
62
     * @param string                     $ipAddress   IP address in dotted quad notation.
63
     * @param int                        $networkSize CIDR network size.
64
     * @param SubnetReportInterface|null $report
65
     */
66
    public function __construct(string $ipAddress, int $networkSize, ?SubnetReportInterface $report = null)
67
    {
68
        $this->validateInputs($ipAddress, $networkSize);
1,164✔
69

70
        $this->ipAddress   = $ipAddress;
1,154✔
71
        $this->networkSize = $networkSize;
1,154✔
72
        $this->quads       = \explode('.', $ipAddress);
1,154✔
73
        $this->subnetMask  = $this->calculateSubnetMask($networkSize);
1,154✔
74
        $this->report      = $report ?? new SubnetReport();
1,154✔
75
    }
1,154✔
76

77
    /* **************** *
78
     * PUBLIC INTERFACE
79
     * **************** */
80

81
    /**
82
     * Get IP address as dotted quads: xxx.xxx.xxx.xxx
83
     *
84
     * @return string IP address as dotted quads.
85
     */
86
    public function getIPAddress(): string
87
    {
88
        return $this->ipAddress;
97✔
89
    }
90

91
    /**
92
     * Get IP address as array of quads: [xxx, xxx, xxx, xxx]
93
     *
94
     * @return string[]
95
     */
96
    public function getIPAddressQuads(): array
97
    {
98
        return $this->quads;
3✔
99
    }
100

101
    /**
102
     * Get IP address as hexadecimal
103
     *
104
     * @return string IP address in hex
105
     */
106
    public function getIPAddressHex(): string
107
    {
108
        return $this->ipAddressCalculation(self::FORMAT_HEX);
68✔
109
    }
110

111
    /**
112
     * Get IP address as binary
113
     *
114
     * @return string IP address in binary
115
     */
116
    public function getIPAddressBinary(): string
117
    {
118
        return $this->ipAddressCalculation(self::FORMAT_BINARY);
68✔
119
    }
120

121
    /**
122
     * Get the IP address as an integer
123
     *
124
     * @return int
125
     */
126
    public function getIPAddressInteger(): int
127
    {
128
        return $this->convertIpToInt($this->ipAddress);
213✔
129
    }
130

131
    /**
132
     * Get network size
133
     *
134
     * @return int network size
135
     */
136
    public function getNetworkSize(): int
137
    {
138
        return $this->networkSize;
104✔
139
    }
140

141
    /**
142
     * Get the CIDR notation of the subnet: IP Address/Network size.
143
     * Example: 192.168.0.15/24
144
     *
145
     * @return string
146
     */
147
    public function getCidrNotation(): string
148
    {
149
        return $this->ipAddress . '/' . $this->networkSize;
113✔
150
    }
151

152
    /**
153
     * Get the number of IP addresses in the network
154
     *
155
     * @return int Number of IP addresses
156
     */
157
    public function getNumberIPAddresses(): int
158
    {
159
        return $this->getNumberIPAddressesOfNetworkSize($this->networkSize);
525✔
160
    }
161

162
    /**
163
     * Get the number of addressable hosts in the network
164
     *
165
     * For most networks, this is the total IP count minus 2 (network and broadcast addresses).
166
     * Special cases per RFC 3021:
167
     *  - /32: Returns 1 (single host network)
168
     *  - /31: Returns 2 (point-to-point link where both addresses are usable)
169
     *
170
     * @return int Number of IP addresses that are addressable
171
     */
172
    public function getNumberAddressableHosts(): int
173
    {
174
        if ($this->networkSize == 32) {
108✔
175
            return 1;
20✔
176
        }
177
        if ($this->networkSize == 31) {
106✔
178
            return 2;
18✔
179
        }
180

181
        return ($this->getNumberIPAddresses() - 2);
88✔
182
    }
183

184
    /**
185
     * Get range of IP addresses in the network
186
     *
187
     * @return string[] containing start and end of IP address range. IP addresses in dotted quad notation.
188
     */
189
    public function getIPAddressRange(): array
190
    {
191
        return [$this->getNetworkPortion(), $this->getBroadcastAddress()];
273✔
192
    }
193

194
    /**
195
     * Get range of IP addresses in the network
196
     *
197
     * @return string[] containing start and end of IP address range. IP addresses in dotted quad notation.
198
     */
199
    public function getAddressableHostRange(): array
200
    {
201
        return [$this->getMinHost(), $this->getMaxHost()];
38✔
202
    }
203

204
    /**
205
     * Get the broadcast IP address
206
     *
207
     * @return string IP address as dotted quads
208
     */
209
    public function getBroadcastAddress(): string
210
    {
211
        $network_quads       = $this->getNetworkPortionQuads();
358✔
212
        $number_ip_addresses = $this->getNumberIPAddresses();
358✔
213

214
        $network_range_quads = [
215
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[0] & ($this->subnetMask >> 24)) + ((($number_ip_addresses - 1) >> 24) & 0xFF)),
358✔
216
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[1] & ($this->subnetMask >> 16)) + ((($number_ip_addresses - 1) >> 16) & 0xFF)),
358✔
217
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[2] & ($this->subnetMask >>  8)) + ((($number_ip_addresses - 1) >>  8) & 0xFF)),
358✔
218
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[3] & ($this->subnetMask >>  0)) + ((($number_ip_addresses - 1) >>  0) & 0xFF)),
358✔
219
        ];
220

221
        return \implode('.', $network_range_quads);
358✔
222
    }
223

224
    /**
225
     * Get minimum host IP address as dotted quads: xxx.xxx.xxx.xxx
226
     *
227
     * For most networks, this is the network address + 1.
228
     * Special cases:
229
     *  - /32: Returns the IP address itself (single host)
230
     *  - /31: Returns the network portion (lower IP of the point-to-point pair per RFC 3021)
231
     *
232
     * @return string min host as dotted quads
233
     */
234
    public function getMinHost(): string
235
    {
236
        if ($this->networkSize === 32) {
141✔
237
            return $this->ipAddress;
20✔
238
        }
239
        if ($this->networkSize === 31) {
139✔
240
            return $this->getNetworkPortion();
30✔
241
        }
242
        return $this->minHostCalculation(self::FORMAT_QUADS, '.');
109✔
243
    }
244

245
    /**
246
     * Get minimum host IP address as array of quads: [xxx, xxx, xxx, xxx]
247
     *
248
     * @return string[] min host portion as dotted quads.
249
     */
250
    public function getMinHostQuads(): array
251
    {
252
        if ($this->networkSize === 32) {
17✔
253
            return $this->quads;
1✔
254
        }
255
        if ($this->networkSize === 31) {
16✔
256
            return $this->getNetworkPortionQuads();
7✔
257
        }
258
        return \explode('.', $this->minHostCalculation(self::FORMAT_QUADS, '.'));
9✔
259
    }
260

261
    /**
262
     * Get minimum host IP address as hex
263
     *
264
     * @return string min host portion as hex
265
     */
266
    public function getMinHostHex(): string
267
    {
268
        if ($this->networkSize === 32) {
17✔
269
            return \implode('', \array_map(
1✔
270
                function ($quad) {
271
                    return \sprintf(self::FORMAT_HEX, $quad);
1✔
272
                },
1✔
273
                $this->quads
1✔
274
            ));
275
        }
276
        if ($this->networkSize === 31) {
16✔
277
            return $this->getNetworkPortionHex();
7✔
278
        }
279
        return $this->minHostCalculation(self::FORMAT_HEX);
9✔
280
    }
281

282
    /**
283
     * Get minimum host IP address as binary
284
     *
285
     * @return string min host portion as binary
286
     */
287
    public function getMinHostBinary(): string
288
    {
289
        if ($this->networkSize === 32) {
17✔
290
            return \implode('', \array_map(
1✔
291
                function ($quad) {
292
                    return \sprintf(self::FORMAT_BINARY, $quad);
1✔
293
                },
1✔
294
                $this->quads
1✔
295
            ));
296
        }
297
        if ($this->networkSize === 31) {
16✔
298
            return $this->getNetworkPortionBinary();
7✔
299
        }
300
        return $this->minHostCalculation(self::FORMAT_BINARY);
9✔
301
    }
302

303
    /**
304
     * Get minimum host IP address as an Integer
305
     *
306
     * @return int min host portion as integer
307
     */
308
    public function getMinHostInteger(): int
309
    {
310
        if ($this->networkSize === 32) {
17✔
311
            return $this->convertIpToInt(\implode('.', $this->quads));
1✔
312
        }
313
        if ($this->networkSize === 31) {
16✔
314
            return $this->getNetworkPortionInteger();
7✔
315
        }
316
        return $this->convertIpToInt($this->minHostCalculation(self::FORMAT_QUADS, '.'));
9✔
317
    }
318

319
    /**
320
     * Get maximum host IP address as dotted quads: xxx.xxx.xxx.xxx
321
     *
322
     * For most networks, this is the broadcast address - 1.
323
     * Special cases:
324
     *  - /32: Returns the IP address itself (single host)
325
     *  - /31: Returns the broadcast address (higher IP of the point-to-point pair per RFC 3021)
326
     *
327
     * @return string max host as dotted quads.
328
     */
329
    public function getMaxHost(): string
330
    {
331
        if ($this->networkSize === 32) {
141✔
332
            return $this->ipAddress;
20✔
333
        }
334
        if ($this->networkSize === 31) {
139✔
335
            return $this->getBroadcastAddress();
30✔
336
        }
337
        return $this->maxHostCalculation(self::FORMAT_QUADS, '.');
109✔
338
    }
339

340
    /**
341
     * Get maximum host IP address as array of quads: [xxx, xxx, xxx, xxx]
342
     *
343
     * @return string[] max host portion as dotted quads
344
     */
345
    public function getMaxHostQuads(): array
346
    {
347
        if ($this->networkSize === 32) {
17✔
348
            return $this->quads;
1✔
349
        }
350
        if ($this->networkSize === 31) {
16✔
351
            return \explode('.', $this->getBroadcastAddress());
7✔
352
        }
353
        return \explode('.', $this->maxHostCalculation(self::FORMAT_QUADS, '.'));
9✔
354
    }
355

356
    /**
357
     * Get maximum host IP address as hex
358
     *
359
     * @return string max host portion as hex
360
     */
361
    public function getMaxHostHex(): string
362
    {
363
        if ($this->networkSize === 32) {
17✔
364
            return \implode('', \array_map(
1✔
365
                function ($quad) {
366
                    return \sprintf(self::FORMAT_HEX, $quad);
1✔
367
                },
1✔
368
                $this->quads
1✔
369
            ));
370
        }
371
        if ($this->networkSize === 31) {
16✔
372
            return \implode('', \array_map(
7✔
373
                function ($quad) {
374
                    return \sprintf(self::FORMAT_HEX, $quad);
7✔
375
                },
7✔
376
                \explode('.', $this->getBroadcastAddress())
7✔
377
            ));
378
        }
379
        return $this->maxHostCalculation(self::FORMAT_HEX);
9✔
380
    }
381

382
    /**
383
     * Get maximum host IP address as binary
384
     *
385
     * @return string max host portion as binary
386
     */
387
    public function getMaxHostBinary(): string
388
    {
389
        if ($this->networkSize === 32) {
17✔
390
            return \implode('', \array_map(
1✔
391
                function ($quad) {
392
                    return \sprintf(self::FORMAT_BINARY, $quad);
1✔
393
                },
1✔
394
                $this->quads
1✔
395
            ));
396
        }
397
        if ($this->networkSize === 31) {
16✔
398
            return \implode('', \array_map(
7✔
399
                function ($quad) {
400
                    return \sprintf(self::FORMAT_BINARY, $quad);
7✔
401
                },
7✔
402
                \explode('.', $this->getBroadcastAddress())
7✔
403
            ));
404
        }
405
        return $this->maxHostCalculation(self::FORMAT_BINARY);
9✔
406
    }
407

408
    /**
409
     * Get maximum host IP address as an Integer
410
     *
411
     * @return int max host portion as integer
412
     */
413
    public function getMaxHostInteger(): int
414
    {
415
        if ($this->networkSize === 32) {
17✔
416
            return $this->convertIpToInt(\implode('.', $this->quads));
1✔
417
        }
418
        if ($this->networkSize === 31) {
16✔
419
            return $this->convertIpToInt($this->getBroadcastAddress());
7✔
420
        }
421
        return $this->convertIpToInt($this->maxHostCalculation(self::FORMAT_QUADS, '.'));
9✔
422
    }
423

424
    /**
425
     * Get subnet mask as dotted quads: xxx.xxx.xxx.xxx
426
     *
427
     * @return string subnet mask as dotted quads
428
     */
429
    public function getSubnetMask(): string
430
    {
431
        return $this->subnetCalculation(self::FORMAT_QUADS, '.');
110✔
432
    }
433

434
    /**
435
     * Get subnet mask as array of quads: [xxx, xxx, xxx, xxx]
436
     *
437
     * @return string[] of four elements containing the four quads of the subnet mask.
438
     */
439
    public function getSubnetMaskQuads(): array
440
    {
441
        return \explode('.', $this->subnetCalculation(self::FORMAT_QUADS, '.'));
32✔
442
    }
443

444
    /**
445
     * Get subnet mask as hexadecimal
446
     *
447
     * @return string subnet mask in hex
448
     */
449
    public function getSubnetMaskHex(): string
450
    {
451
        return $this->subnetCalculation(self::FORMAT_HEX);
97✔
452
    }
453

454
    /**
455
     * Get subnet mask as binary
456
     *
457
     * @return string subnet mask in binary
458
     */
459
    public function getSubnetMaskBinary(): string
460
    {
461
        return $this->subnetCalculation(self::FORMAT_BINARY);
97✔
462
    }
463

464
    /**
465
     * Get subnet mask as an integer
466
     *
467
     * @return int
468
     */
469
    public function getSubnetMaskInteger(): int
470
    {
471
        return $this->convertIpToInt($this->subnetCalculation(self::FORMAT_QUADS, '.'));
103✔
472
    }
473

474
    /**
475
     * Get wildcard mask as dotted quads: xxx.xxx.xxx.xxx
476
     *
477
     * The wildcard mask is the inverse of the subnet mask, commonly used in
478
     * Cisco ACLs (Access Control Lists) and OSPF network statements.
479
     * For a /24 (subnet mask 255.255.255.0), the wildcard mask is 0.0.0.255.
480
     *
481
     * @see https://www.cisco.com/c/en/us/support/docs/security/ios-firewall/23602-confaccesslists.html Cisco IOS ACL Configuration Guide
482
     *
483
     * @return string wildcard mask as dotted quads
484
     */
485
    public function getWildcardMask(): string
486
    {
487
        return $this->wildcardCalculation(self::FORMAT_QUADS, '.');
87✔
488
    }
489

490
    /**
491
     * Get wildcard mask as array of quads: [xxx, xxx, xxx, xxx]
492
     *
493
     * @return string[] of four elements containing the four quads of the wildcard mask.
494
     */
495
    public function getWildcardMaskQuads(): array
496
    {
497
        return \explode('.', $this->wildcardCalculation(self::FORMAT_QUADS, '.'));
12✔
498
    }
499

500
    /**
501
     * Get wildcard mask as hexadecimal
502
     *
503
     * @return string wildcard mask in hex
504
     */
505
    public function getWildcardMaskHex(): string
506
    {
507
        return $this->wildcardCalculation(self::FORMAT_HEX);
74✔
508
    }
509

510
    /**
511
     * Get wildcard mask as binary
512
     *
513
     * @return string wildcard mask in binary
514
     */
515
    public function getWildcardMaskBinary(): string
516
    {
517
        return $this->wildcardCalculation(self::FORMAT_BINARY);
74✔
518
    }
519

520
    /**
521
     * Get wildcard mask as an integer
522
     *
523
     * @return int
524
     */
525
    public function getWildcardMaskInteger(): int
526
    {
527
        return $this->convertIpToInt($this->wildcardCalculation(self::FORMAT_QUADS, '.'));
82✔
528
    }
529

530
    /**
531
     * Split the network into smaller networks
532
     *
533
     * @param int $networkSize
534
     * @return SubnetCalculator[]
535
     */
536
    public function split(int $networkSize): array
537
    {
538
        if ($networkSize <= $this->networkSize) {
30✔
539
            throw new \RuntimeException('New networkSize must be larger than the base networkSize.');
6✔
540
        }
541

542
        if ($networkSize > 32) {
24✔
543
            throw new \RuntimeException('New networkSize must be smaller than the maximum networkSize.');
8✔
544
        }
545

546
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
16✔
547

548
        $addressCount = $this->getNumberIPAddressesOfNetworkSize($networkSize);
16✔
549

550
        $ranges = [];
16✔
551
        for ($ip = $startIp; $ip <= $endIp; $ip += $addressCount) {
16✔
552
            $ranges[] = new SubnetCalculator($this->convertIpToDottedQuad($ip), $networkSize);
16✔
553
        }
554

555
        return $ranges;
16✔
556
    }
557

558
    /**
559
     * Get network portion of IP address as dotted quads: xxx.xxx.xxx.xxx
560
     *
561
     * @return string network portion as dotted quads
562
     */
563
    public function getNetworkPortion(): string
564
    {
565
        return $this->networkCalculation(self::FORMAT_QUADS, '.');
374✔
566
    }
567

568
    /**
569
     * Get network portion as array of quads: [xxx, xxx, xxx, xxx]
570
     *
571
     * @return string[] of four elements containing the four quads of the network portion
572
     */
573
    public function getNetworkPortionQuads(): array
574
    {
575
        return \explode('.', $this->networkCalculation(self::FORMAT_QUADS, '.'));
493✔
576
    }
577

578
    /**
579
     * Get network portion of IP address as hexadecimal
580
     *
581
     * @return string network portion in hex
582
     */
583
    public function getNetworkPortionHex(): string
584
    {
585
        return $this->networkCalculation(self::FORMAT_HEX);
104✔
586
    }
587

588
    /**
589
     * Get network portion of IP address as binary
590
     *
591
     * @return string network portion in binary
592
     */
593
    public function getNetworkPortionBinary(): string
594
    {
595
        return $this->networkCalculation(self::FORMAT_BINARY);
104✔
596
    }
597

598
    /**
599
     * Get network portion of IP address as an integer
600
     *
601
     * @return int
602
     */
603
    public function getNetworkPortionInteger(): int
604
    {
605
        return $this->convertIpToInt($this->networkCalculation(self::FORMAT_QUADS, '.'));
104✔
606
    }
607

608
    /**
609
     * Get host portion of IP address as dotted quads: xxx.xxx.xxx.xxx
610
     *
611
     * @return string host portion as dotted quads
612
     */
613
    public function getHostPortion(): string
614
    {
615
        return $this->hostCalculation(self::FORMAT_QUADS, '.');
97✔
616
    }
617

618
    /**
619
     * Get host portion as array of quads: [xxx, xxx, xxx, xxx]
620
     *
621
     * @return string[] of four elements containing the four quads of the host portion
622
     */
623
    public function getHostPortionQuads(): array
624
    {
625
        return \explode('.', $this->hostCalculation(self::FORMAT_QUADS, '.'));
32✔
626
    }
627

628
    /**
629
     * Get host portion of IP address as hexadecimal
630
     *
631
     * @return string host portion in hex
632
     */
633
    public function getHostPortionHex(): string
634
    {
635
        return $this->hostCalculation(self::FORMAT_HEX);
97✔
636
    }
637

638
    /**
639
     * Get host portion of IP address as binary
640
     *
641
     * @return string host portion in binary
642
     */
643
    public function getHostPortionBinary(): string
644
    {
645
        return $this->hostCalculation(self::FORMAT_BINARY);
97✔
646
    }
647

648
    /**
649
     * Get host portion of IP address as an integer
650
     *
651
     * @return int
652
     */
653
    public function getHostPortionInteger(): int
654
    {
655
        return $this->convertIpToInt($this->hostCalculation(self::FORMAT_QUADS, '.'));
97✔
656
    }
657

658
    /**
659
     * Get all IP addresses
660
     *
661
     * @return \Generator|string[]|false[]
662
     */
663
    public function getAllIPAddresses(): \Generator
664
    {
665
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
25✔
666

667
        for ($ip = $startIp; $ip <= $endIp; $ip++) {
24✔
668
            yield $this->convertIpToDottedQuad($ip);
24✔
669
        }
670
    }
24✔
671

672
    /**
673
     * Get all host IP addresses
674
     * Removes broadcast and network address if they exist.
675
     *
676
     * @return \Generator|string[]|false[]
677
     *
678
     * @throws \RuntimeException if there is an error in the IP address range calculation
679
     */
680
    public function getAllHostIPAddresses(): \Generator
681
    {
682
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
22✔
683

684
        if ($this->getNetworkSize() < 31) {
21✔
685
            $startIp += 1;
18✔
686
            $endIp   -= 1;
18✔
687
        }
688

689
        for ($ip = $startIp; $ip <= $endIp; $ip++) {
21✔
690
            yield $this->convertIpToDottedQuad($ip);
21✔
691
        }
692
    }
21✔
693

694
    /**
695
     * Is the IP address in the subnet?
696
     *
697
     * @param string $ipAddressString
698
     *
699
     * @return bool
700
     */
701
    public function isIPAddressInSubnet(string $ipAddressString): bool
702
    {
703
        $ipAddress = \ip2long($ipAddressString);
108✔
704
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
108✔
705

706
        return $ipAddress >= $startIp && $ipAddress <= $endIp;
108✔
707
    }
708

709
    /**
710
     * Check if this subnet overlaps with another subnet.
711
     *
712
     * Two subnets overlap if they share any IP addresses.
713
     * This is useful for network planning and conflict prevention,
714
     * including firewall rule validation and routing table conflict detection.
715
     *
716
     * @param SubnetCalculator $other The other subnet to compare against
717
     *
718
     * @return bool True if the subnets share any IP addresses
719
     */
720
    public function overlaps(SubnetCalculator $other): bool
721
    {
722
        [$thisStart, $thisEnd] = $this->getIPAddressRangeAsInts();
29✔
723
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
29✔
724

725
        // Two ranges overlap if one starts before the other ends and vice versa
726
        return $thisStart <= $otherEnd && $otherStart <= $thisEnd;
29✔
727
    }
728

729
    /**
730
     * Check if this subnet fully contains another subnet.
731
     *
732
     * A subnet contains another if all IP addresses in the contained subnet
733
     * are also within this subnet's range.
734
     *
735
     * @param SubnetCalculator $other The subnet to check for containment
736
     *
737
     * @return bool True if this subnet fully contains the other subnet
738
     */
739
    public function contains(SubnetCalculator $other): bool
740
    {
741
        [$thisStart, $thisEnd] = $this->getIPAddressRangeAsInts();
36✔
742
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
36✔
743

744
        // This subnet contains other if other's entire range is within this range
745
        return $thisStart <= $otherStart && $thisEnd >= $otherEnd;
36✔
746
    }
747

748
    /**
749
     * Check if this subnet is fully contained within another subnet.
750
     *
751
     * This is the inverse of contains(): $a->isContainedIn($b) === $b->contains($a)
752
     *
753
     * @param SubnetCalculator $other The subnet to check if this subnet is within
754
     *
755
     * @return bool True if this subnet is fully contained within the other subnet
756
     */
757
    public function isContainedIn(SubnetCalculator $other): bool
758
    {
759
        return $other->contains($this);
21✔
760
    }
761

762
    /* ******************************************* *
763
     * PRIVATE/RESERVED IP RANGE DETECTION METHODS
764
     * ******************************************* */
765

766
    /**
767
     * Check if the IP address is in a private range (RFC 1918).
768
     *
769
     * Private address ranges:
770
     *   - 10.0.0.0/8     (10.0.0.0 - 10.255.255.255)
771
     *   - 172.16.0.0/12  (172.16.0.0 - 172.31.255.255)
772
     *   - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
773
     *
774
     * @link https://datatracker.ietf.org/doc/html/rfc1918 RFC 1918 - Address Allocation for Private Internets
775
     *
776
     * @return bool True if the IP address is in a private range
777
     */
778
    public function isPrivate(): bool
779
    {
780
        $ip = $this->getIPAddressInteger();
134✔
781

782
        // 10.0.0.0/8: 10.0.0.0 - 10.255.255.255
783
        if ($this->isInRange($ip, 0x0A000000, 0x0AFFFFFF)) {
134✔
784
            return true;
29✔
785
        }
786

787
        // 172.16.0.0/12: 172.16.0.0 - 172.31.255.255
788
        if ($this->isInRange($ip, 0xAC100000, 0xAC1FFFFF)) {
106✔
789
            return true;
14✔
790
        }
791

792
        // 192.168.0.0/16: 192.168.0.0 - 192.168.255.255
793
        if ($this->isInRange($ip, 0xC0A80000, 0xC0A8FFFF)) {
92✔
794
            return true;
43✔
795
        }
796

797
        return false;
52✔
798
    }
799

800
    /**
801
     * Check if the IP address is publicly routable.
802
     *
803
     * An IP is public if it is not in any of the special-purpose address ranges:
804
     * private, loopback, link-local, multicast, CGN, documentation, benchmarking,
805
     * reserved, limited broadcast, or "this" network.
806
     *
807
     * @return bool True if the IP address is publicly routable
808
     */
809
    public function isPublic(): bool
810
    {
811
        return !$this->isPrivate()
28✔
812
            && !$this->isLoopback()
28✔
813
            && !$this->isLinkLocal()
28✔
814
            && !$this->isMulticast()
28✔
815
            && !$this->isCarrierGradeNat()
28✔
816
            && !$this->isDocumentation()
28✔
817
            && !$this->isBenchmarking()
28✔
818
            && !$this->isReserved()
28✔
819
            && !$this->isThisNetwork();
28✔
820
    }
821

822
    /**
823
     * Check if the IP address is in the loopback range (127.0.0.0/8).
824
     *
825
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
826
     *
827
     * @return bool True if the IP address is in the loopback range
828
     */
829
    public function isLoopback(): bool
830
    {
831
        $ip = $this->getIPAddressInteger();
54✔
832

833
        // 127.0.0.0/8: 127.0.0.0 - 127.255.255.255
834
        return $this->isInRange($ip, 0x7F000000, 0x7FFFFFFF);
54✔
835
    }
836

837
    /**
838
     * Check if the IP address is link-local (169.254.0.0/16).
839
     *
840
     * Link-local addresses are used for automatic private IP addressing (APIPA)
841
     * when DHCP is not available.
842
     *
843
     * @link https://datatracker.ietf.org/doc/html/rfc3927 RFC 3927 - Dynamic Configuration of IPv4 Link-Local Addresses
844
     *
845
     * @return bool True if the IP address is link-local
846
     */
847
    public function isLinkLocal(): bool
848
    {
849
        $ip = $this->getIPAddressInteger();
49✔
850

851
        // 169.254.0.0/16: 169.254.0.0 - 169.254.255.255
852
        return $this->isInRange($ip, 0xA9FE0000, 0xA9FEFFFF);
49✔
853
    }
854

855
    /**
856
     * Check if the IP address is multicast (224.0.0.0/4).
857
     *
858
     * @link https://datatracker.ietf.org/doc/html/rfc5771 RFC 5771 - IANA Guidelines for IPv4 Multicast Address Assignments
859
     *
860
     * @return bool True if the IP address is multicast
861
     */
862
    public function isMulticast(): bool
863
    {
864
        $ip = $this->getIPAddressInteger();
39✔
865

866
        // 224.0.0.0/4: 224.0.0.0 - 239.255.255.255
867
        return $this->isInRange($ip, 0xE0000000, 0xEFFFFFFF);
39✔
868
    }
869

870
    /**
871
     * Check if the IP address is in Carrier-Grade NAT range (100.64.0.0/10).
872
     *
873
     * Also known as Shared Address Space, used by ISPs for CGN deployments.
874
     *
875
     * @link https://datatracker.ietf.org/doc/html/rfc6598 RFC 6598 - IANA-Reserved IPv4 Prefix for Shared Address Space
876
     *
877
     * @return bool True if the IP address is in the CGN range
878
     */
879
    public function isCarrierGradeNat(): bool
880
    {
881
        $ip = $this->getIPAddressInteger();
43✔
882

883
        // 100.64.0.0/10: 100.64.0.0 - 100.127.255.255
884
        return $this->isInRange($ip, 0x64400000, 0x647FFFFF);
43✔
885
    }
886

887
    /**
888
     * Check if the IP address is reserved for documentation (RFC 5737).
889
     *
890
     * Documentation ranges (TEST-NET-1, TEST-NET-2, TEST-NET-3):
891
     *   - 192.0.2.0/24   (TEST-NET-1)
892
     *   - 198.51.100.0/24 (TEST-NET-2)
893
     *   - 203.0.113.0/24  (TEST-NET-3)
894
     *
895
     * @link https://datatracker.ietf.org/doc/html/rfc5737 RFC 5737 - IPv4 Address Blocks Reserved for Documentation
896
     *
897
     * @return bool True if the IP address is reserved for documentation
898
     */
899
    public function isDocumentation(): bool
900
    {
901
        $ip = $this->getIPAddressInteger();
48✔
902

903
        // 192.0.2.0/24 (TEST-NET-1): 192.0.2.0 - 192.0.2.255
904
        if ($this->isInRange($ip, 0xC0000200, 0xC00002FF)) {
48✔
905
            return true;
6✔
906
        }
907

908
        // 198.51.100.0/24 (TEST-NET-2): 198.51.100.0 - 198.51.100.255
909
        if ($this->isInRange($ip, 0xC6336400, 0xC63364FF)) {
42✔
910
            return true;
4✔
911
        }
912

913
        // 203.0.113.0/24 (TEST-NET-3): 203.0.113.0 - 203.0.113.255
914
        if ($this->isInRange($ip, 0xCB007100, 0xCB0071FF)) {
38✔
915
            return true;
4✔
916
        }
917

918
        return false;
34✔
919
    }
920

921
    /**
922
     * Check if the IP address is reserved for benchmarking (198.18.0.0/15).
923
     *
924
     * @link https://datatracker.ietf.org/doc/html/rfc2544 RFC 2544 - Benchmarking Methodology for Network Interconnect Devices
925
     *
926
     * @return bool True if the IP address is reserved for benchmarking
927
     */
928
    public function isBenchmarking(): bool
929
    {
930
        $ip = $this->getIPAddressInteger();
35✔
931

932
        // 198.18.0.0/15: 198.18.0.0 - 198.19.255.255
933
        return $this->isInRange($ip, 0xC6120000, 0xC613FFFF);
35✔
934
    }
935

936
    /**
937
     * Check if the IP address is reserved for future use (240.0.0.0/4).
938
     *
939
     * Note: This includes 255.255.255.255 (limited broadcast), which can be
940
     * separately identified using isLimitedBroadcast().
941
     *
942
     * @link https://datatracker.ietf.org/doc/html/rfc1112 RFC 1112 - Host Extensions for IP Multicasting
943
     *
944
     * @return bool True if the IP address is reserved for future use
945
     */
946
    public function isReserved(): bool
947
    {
948
        $ip = $this->getIPAddressInteger();
27✔
949

950
        // 240.0.0.0/4: 240.0.0.0 - 255.255.255.255
951
        return $this->isInRange($ip, 0xF0000000, 0xFFFFFFFF);
27✔
952
    }
953

954
    /**
955
     * Check if the IP address is the broadcast address (255.255.255.255/32).
956
     *
957
     * @link https://datatracker.ietf.org/doc/html/rfc919 RFC 919 - Broadcasting Internet Datagrams
958
     *
959
     * @return bool True if the IP address is the limited broadcast address
960
     */
961
    public function isLimitedBroadcast(): bool
962
    {
963
        return $this->ipAddress === '255.255.255.255';
13✔
964
    }
965

966
    /**
967
     * Check if the IP address is in the "this" network range (0.0.0.0/8).
968
     *
969
     * Addresses in this range represent "this host on this network" and are
970
     * only valid as source addresses.
971
     *
972
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
973
     *
974
     * @return bool True if the IP address is in the "this" network range
975
     */
976
    public function isThisNetwork(): bool
977
    {
978
        $ip = $this->getIPAddressInteger();
104✔
979

980
        // 0.0.0.0/8: 0.0.0.0 - 0.255.255.255
981
        return $this->isInRange($ip, 0x00000000, 0x00FFFFFF);
104✔
982
    }
983

984
    /**
985
     * Get the address type classification.
986
     *
987
     * Returns a string identifying the type of address. The order of checks matters:
988
     * more specific types (like limited-broadcast) are checked before broader types
989
     * (like reserved) that would also match.
990
     *
991
     * @return string Address type: 'private', 'public', 'loopback', 'link-local',
992
     *                'multicast', 'carrier-grade-nat', 'documentation', 'benchmarking',
993
     *                'reserved', 'limited-broadcast', 'this-network'
994
     */
995
    public function getAddressType(): string
996
    {
997
        // Check specific types first, then broader types
998
        if ($this->isThisNetwork()) {
89✔
999
            return 'this-network';
2✔
1000
        }
1001
        if ($this->isPrivate()) {
87✔
1002
            return 'private';
68✔
1003
        }
1004
        if ($this->isLoopback()) {
21✔
1005
            return 'loopback';
2✔
1006
        }
1007
        if ($this->isLinkLocal()) {
19✔
1008
            return 'link-local';
2✔
1009
        }
1010
        if ($this->isCarrierGradeNat()) {
17✔
1011
            return 'carrier-grade-nat';
2✔
1012
        }
1013
        if ($this->isDocumentation()) {
15✔
1014
            return 'documentation';
3✔
1015
        }
1016
        if ($this->isBenchmarking()) {
12✔
1017
            return 'benchmarking';
2✔
1018
        }
1019
        if ($this->isMulticast()) {
10✔
1020
            return 'multicast';
2✔
1021
        }
1022
        // Check limited broadcast before reserved (since it's a subset)
1023
        if ($this->isLimitedBroadcast()) {
8✔
1024
            return 'limited-broadcast';
1✔
1025
        }
1026
        if ($this->isReserved()) {
7✔
1027
            return 'reserved';
2✔
1028
        }
1029

1030
        return 'public';
5✔
1031
    }
1032

1033
    /**
1034
     * Check if an IP integer is within a given range (inclusive).
1035
     *
1036
     * @param int $ip    IP address as integer
1037
     * @param int $start Start of range as integer
1038
     * @param int $end   End of range as integer
1039
     *
1040
     * @return bool True if the IP is within the range
1041
     */
1042
    private function isInRange(int $ip, int $start, int $end): bool
1043
    {
1044
        // Handle PHP's signed integer representation for high IP addresses
1045
        // Convert to unsigned for comparison using sprintf
1046
        $ipUnsigned    = \sprintf('%u', $ip);
210✔
1047
        $startUnsigned = \sprintf('%u', $start);
210✔
1048
        $endUnsigned   = \sprintf('%u', $end);
210✔
1049

1050
        return $ipUnsigned >= $startUnsigned && $ipUnsigned <= $endUnsigned;
210✔
1051
    }
1052

1053
    /**
1054
     * Get the IPv4 Arpa Domain
1055
     *
1056
     * Reverse DNS lookups for IPv4 addresses use the special domain in-addr.arpa.
1057
     * In this domain, an IPv4 address is represented as a concatenated sequence of four decimal numbers,
1058
     * separated by dots, to which is appended the second level domain suffix .in-addr.arpa.
1059
     *
1060
     * The four decimal numbers are obtained by splitting the 32-bit IPv4 address into four octets and converting
1061
     * each octet into a decimal number. These decimal numbers are then concatenated in the order:
1062
     * least significant octet first (leftmost), to most significant octet last (rightmost).
1063
     * It is important to note that this is the reverse order to the usual dotted-decimal convention for writing
1064
     * IPv4 addresses in textual form.
1065
     *
1066
     * Ex: to do a reverse lookup of the IP address 8.8.4.4 the PTR record for the domain name 4.4.8.8.in-addr.arpa would be looked up.
1067
     *
1068
     * @link https://en.wikipedia.org/wiki/Reverse_DNS_lookup
1069
     *
1070
     * @return string
1071
     */
1072
    public function getIPv4ArpaDomain(): string
1073
    {
1074
        $reverseQuads = \implode('.', \array_reverse($this->quads));
73✔
1075
        return $reverseQuads . '.in-addr.arpa';
73✔
1076
    }
1077

1078
    /**
1079
     * Get subnet calculations as an associated array
1080
     *
1081
     * @return mixed[] of subnet calculations
1082
     */
1083
    public function getSubnetArrayReport(): array
1084
    {
1085
        return $this->report->createArrayReport($this);
1✔
1086
    }
1087

1088
    /**
1089
     * Get subnet calculations as a JSON string
1090
     *
1091
     * @return string JSON string of subnet calculations
1092
     *
1093
     * @throws \RuntimeException if there is a JSON encode error
1094
     */
1095
    public function getSubnetJsonReport(): string
1096
    {
1097
        $json = $this->report->createJsonReport($this);
2✔
1098

1099
        if ($json === false) {
2✔
1100
            throw new \RuntimeException('JSON report failure: ' . json_last_error_msg());
1✔
1101
        }
1102

1103
        return $json;
1✔
1104
    }
1105

1106
    /**
1107
     * Print a report of subnet calculations
1108
     */
1109
    public function printSubnetReport(): void
1110
    {
1111
        $this->report->printReport($this);
1✔
1112
    }
1✔
1113

1114
    /**
1115
     * Print a report of subnet calculations
1116
     *
1117
     * @return string Subnet Calculator report
1118
     */
1119
    public function getPrintableReport(): string
1120
    {
1121
        return $this->report->createPrintableReport($this);
1✔
1122
    }
1123

1124
    /**
1125
     * String representation of a report of subnet calculations
1126
     *
1127
     * @return string
1128
     */
1129
    public function __toString(): string
1130
    {
1131
        return $this->report->createPrintableReport($this);
58✔
1132
    }
1133

1134
    /* ************** *
1135
     * PHP INTERFACES
1136
     * ************** */
1137

1138
    /**
1139
     * \JsonSerializable interface
1140
     *
1141
     * @return mixed[]
1142
     */
1143
    public function jsonSerialize(): array
1144
    {
1145
        return $this->report->createArrayReport($this);
1✔
1146
    }
1147

1148
    /* ********************** *
1149
     * PRIVATE IMPLEMENTATION
1150
     * ********************** */
1151

1152
    /**
1153
     * Calculate subnet mask
1154
     *
1155
     * @param  int $networkSize
1156
     *
1157
     * @return int
1158
     */
1159
    private function calculateSubnetMask(int $networkSize): int
1160
    {
1161
        return 0xFFFFFFFF << (32 - $networkSize);
1,154✔
1162
    }
1163

1164
    /**
1165
     * Calculate IP address for formatting
1166
     *
1167
     * @param string $format    sprintf format to determine if decimal, hex or binary
1168
     * @param string $separator implode separator for formatting quads vs hex and binary
1169
     *
1170
     * @return string formatted IP address
1171
     */
1172
    private function ipAddressCalculation(string $format, string $separator = ''): string
1173
    {
1174
        return \implode($separator, array_map(
71✔
1175
            function ($quad) use ($format) {
1176
                return \sprintf($format, $quad);
71✔
1177
            },
71✔
1178
            $this->quads
71✔
1179
        ));
1180
    }
1181

1182
    /**
1183
     * Subnet calculation
1184
     *
1185
     * @param string $format    sprintf format to determine if decimal, hex or binary
1186
     * @param string $separator implode separator for formatting quads vs hex and binary
1187
     *
1188
     * @return string subnet
1189
     */
1190
    private function subnetCalculation(string $format, string $separator = ''): string
1191
    {
1192
        $maskQuads = [
1193
            \sprintf($format, ($this->subnetMask >> 24) & 0xFF),
116✔
1194
            \sprintf($format, ($this->subnetMask >> 16) & 0xFF),
116✔
1195
            \sprintf($format, ($this->subnetMask >>  8) & 0xFF),
116✔
1196
            \sprintf($format, ($this->subnetMask >>  0) & 0xFF),
116✔
1197
        ];
1198

1199
        return implode($separator, $maskQuads);
116✔
1200
    }
1201

1202
    /**
1203
     * Wildcard mask calculation
1204
     *
1205
     * The wildcard mask is the bitwise inverse of the subnet mask.
1206
     *
1207
     * @param string $format    sprintf format to determine if decimal, hex or binary
1208
     * @param string $separator implode separator for formatting quads vs hex and binary
1209
     *
1210
     * @return string wildcard mask
1211
     */
1212
    private function wildcardCalculation(string $format, string $separator = ''): string
1213
    {
1214
        $wildcardMask = ~$this->subnetMask;
124✔
1215
        $maskQuads = [
1216
            \sprintf($format, ($wildcardMask >> 24) & 0xFF),
124✔
1217
            \sprintf($format, ($wildcardMask >> 16) & 0xFF),
124✔
1218
            \sprintf($format, ($wildcardMask >>  8) & 0xFF),
124✔
1219
            \sprintf($format, ($wildcardMask >>  0) & 0xFF),
124✔
1220
        ];
1221

1222
        return implode($separator, $maskQuads);
124✔
1223
    }
1224

1225
    /**
1226
     * Calculate network portion for formatting
1227
     *
1228
     * @param string $format    sprintf format to determine if decimal, hex or binary
1229
     * @param string $separator implode separator for formatting quads vs hex and binary
1230
     *
1231
     * @return string formatted subnet mask
1232
     */
1233
    private function networkCalculation(string $format, string $separator = ''): string
1234
    {
1235
        $networkQuads = [
1236
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
565✔
1237
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
565✔
1238
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
565✔
1239
            \sprintf($format, (int) $this->quads[3] & ($this->subnetMask >>  0)),
565✔
1240
        ];
1241

1242
        return implode($separator, $networkQuads);
565✔
1243
    }
1244

1245
    /**
1246
     * Calculate host portion for formatting
1247
     *
1248
     * @param string $format    sprintf format to determine if decimal, hex or binary
1249
     * @param string $separator implode separator for formatting quads vs hex and binary
1250
     *
1251
     * @return string formatted subnet mask
1252
     */
1253
    private function hostCalculation(string $format, string $separator = ''): string
1254
    {
1255
        $networkQuads = [
1256
            \sprintf($format, (int) $this->quads[0] & ~($this->subnetMask >> 24)),
97✔
1257
            \sprintf($format, (int) $this->quads[1] & ~($this->subnetMask >> 16)),
97✔
1258
            \sprintf($format, (int) $this->quads[2] & ~($this->subnetMask >>  8)),
97✔
1259
            \sprintf($format, (int) $this->quads[3] & ~($this->subnetMask >>  0)),
97✔
1260
        ];
1261

1262
        return implode($separator, $networkQuads);
97✔
1263
    }
1264

1265
    /**
1266
     * Calculate min host for formatting
1267
     *
1268
     * @param string $format    sprintf format to determine if decimal, hex or binary
1269
     * @param string $separator implode separator for formatting quads vs hex and binary
1270
     *
1271
     * @return string formatted min host
1272
     */
1273
    private function minHostCalculation(string $format, string $separator = ''): string
1274
    {
1275
        $networkQuads = [
1276
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
145✔
1277
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
145✔
1278
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
145✔
1279
            \sprintf($format, ((int) $this->quads[3] & ($this->subnetMask >> 0)) + 1),
145✔
1280
        ];
1281

1282
        return implode($separator, $networkQuads);
145✔
1283
    }
1284

1285
    /**
1286
     * Calculate max host for formatting
1287
     *
1288
     * @param string $format    sprintf format to determine if decimal, hex or binary
1289
     * @param string $separator implode separator for formatting quads vs hex and binary
1290
     *
1291
     * @return string formatted max host
1292
     */
1293
    private function maxHostCalculation(string $format, string $separator = ''): string
1294
    {
1295
        $networkQuads      = $this->getNetworkPortionQuads();
145✔
1296
        $numberIpAddresses = $this->getNumberIPAddresses();
145✔
1297

1298
        $network_range_quads = [
1299
            \sprintf($format, ((int) $networkQuads[0] & ($this->subnetMask >> 24)) + ((($numberIpAddresses - 1) >> 24) & 0xFF)),
145✔
1300
            \sprintf($format, ((int) $networkQuads[1] & ($this->subnetMask >> 16)) + ((($numberIpAddresses - 1) >> 16) & 0xFF)),
145✔
1301
            \sprintf($format, ((int) $networkQuads[2] & ($this->subnetMask >>  8)) + ((($numberIpAddresses - 1) >>  8) & 0xFF)),
145✔
1302
            \sprintf($format, ((int) $networkQuads[3] & ($this->subnetMask >>  0)) + ((($numberIpAddresses - 1) >>  0) & 0xFE)),
145✔
1303
        ];
1304

1305
        return implode($separator, $network_range_quads);
145✔
1306
    }
1307

1308
    /**
1309
     * Validate IP address and network
1310
     *
1311
     * @param string $ipAddress   IP address in dotted quads format
1312
     * @param int    $networkSize Network size
1313
     *
1314
     * @throws \UnexpectedValueException IP or network size not valid
1315
     */
1316
    private function validateInputs(string $ipAddress, int $networkSize): void
1317
    {
1318
        if (!\filter_var($ipAddress, FILTER_VALIDATE_IP)) {
1,164✔
1319
            throw new \UnexpectedValueException("IP address $ipAddress not valid.");
13✔
1320
        }
1321
        if (($networkSize < 1) || ($networkSize > 32)) {
1,157✔
1322
            throw new \UnexpectedValueException("Network size $networkSize not valid.");
15✔
1323
        }
1324
    }
1,154✔
1325

1326
    /**
1327
     * Get the start and end of the IP address range as ints
1328
     *
1329
     * @return int[] [start IP, end IP]
1330
     */
1331
    private function getIPAddressRangeAsInts(): array
1332
    {
1333
        [$startIp, $endIp] = $this->getIPAddressRange();
234✔
1334
        $startIp = $this->convertIpToInt($startIp);
234✔
1335
        $endIp   = $this->convertIpToInt($endIp);
232✔
1336

1337
        return [$startIp, $endIp];
232✔
1338
    }
1339

1340
    /**
1341
     * Get the number of IP addresses in the given network size
1342
     *
1343
     * @param int $networkSize
1344
     *
1345
     * @return int Number of IP addresses
1346
     */
1347
    private function getNumberIPAddressesOfNetworkSize($networkSize): int
1348
    {
1349
        return \pow(2, (32 - $networkSize));
525✔
1350
    }
1351

1352

1353
    /**
1354
     * Convert a dotted-quad IP address to an integer
1355
     *
1356
     * @param string $ipAddress Dotted-quad IP address
1357
     *
1358
     * @return int Integer representation of an IP address
1359
     */
1360
    private function convertIpToInt(string $ipAddress): int
1361
    {
1362
        $ipAsInt = \ip2long($ipAddress);
539✔
1363
        if ($ipAsInt === false) {
539✔
1364
            throw new \RuntimeException('Invalid IP address string. Could not convert dotted-quad string address to an integer: ' . $ipAddress);
3✔
1365
        }
1366
        return $ipAsInt;
536✔
1367
    }
1368

1369
    /**
1370
     * Convert an integer IP address to a dotted-quad IP string
1371
     *
1372
     * @param int $ipAsInt Integer representation of an IP address
1373
     *
1374
     * @return string Dotted-quad IP address
1375
     */
1376
    private function convertIpToDottedQuad(int $ipAsInt): string
1377
    {
1378
        $ipDottedQuad = \long2ip($ipAsInt);
61✔
1379
        if ($ipDottedQuad == false) {
61✔
1380
            throw new \RuntimeException('Invalid IP address integer. Could not convert integer address to dotted-quad string: ' . $ipAsInt);
×
1381
        }
1382
        return $ipDottedQuad;
61✔
1383
    }
1384
}
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