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

markrogoyski / ipv4-subnet-calculator-php / 20947789662

13 Jan 2026 07:01AM UTC coverage: 97.017% (-0.8%) from 97.831%
20947789662

push

github

markrogoyski
Add CIDR aggregation and supernetting.

88 of 98 new or added lines in 1 file covered. (89.8%)

7 existing lines in 2 files now uncovered.

618 of 637 relevant lines covered (97.02%)

77.94 hits per line

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

99.5
/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,437✔
69

70
        $this->ipAddress   = $ipAddress;
1,427✔
71
        $this->networkSize = $networkSize;
1,427✔
72
        $this->quads       = \explode('.', $ipAddress);
1,427✔
73
        $this->subnetMask  = $this->calculateSubnetMask($networkSize);
1,427✔
74
        $this->report      = $report ?? new SubnetReport();
1,427✔
75
    }
1,427✔
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;
183✔
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);
698✔
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) {
171✔
175
            return 1;
25✔
176
        }
177
        if ($this->networkSize == 31) {
164✔
178
            return 2;
23✔
179
        }
180

181
        return ($this->getNumberIPAddresses() - 2);
141✔
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()];
324✔
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();
409✔
212
        $number_ip_addresses = $this->getNumberIPAddresses();
409✔
213

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

221
        return \implode('.', $network_range_quads);
409✔
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, '.');
485✔
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, '.'));
544✔
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, '.'));
211✔
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);
120✔
704
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
120✔
705

706
        return $ipAddress >= $startIp && $ipAddress <= $endIp;
120✔
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();
80✔
723
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
80✔
724

725
        // Two ranges overlap if one starts before the other ends and vice versa
726
        return $thisStart <= $otherEnd && $otherStart <= $thisEnd;
80✔
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();
82✔
742
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
82✔
743

744
        // This subnet contains other if other's entire range is within this range
745
        return $thisStart <= $otherStart && $thisEnd >= $otherEnd;
82✔
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
     * EXCLUDE/DIFFERENCE OPERATIONS
764
     * ****************************** */
765

766
    /**
767
     * Exclude a subnet from this subnet.
768
     *
769
     * Returns an array of subnets representing the remainder after removing
770
     * the excluded subnet. Useful for carving out reserved ranges.
771
     *
772
     * The result is the minimal set of CIDR blocks covering the remaining space.
773
     * If the subnets don't overlap, returns this subnet unchanged.
774
     * If the excluded subnet fully contains this subnet, returns an empty array.
775
     *
776
     * @param SubnetCalculator $exclude Subnet to exclude
777
     *
778
     * @return SubnetCalculator[] Remaining subnets (empty if fully excluded)
779
     */
780
    public function exclude(SubnetCalculator $exclude): array
781
    {
782
        // If no overlap, return this subnet unchanged
783
        if (!$this->overlaps($exclude)) {
51✔
784
            return [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
11✔
785
        }
786

787
        // If exclude fully contains this subnet, nothing remains
788
        if ($exclude->contains($this)) {
46✔
789
            return [];
6✔
790
        }
791

792
        // If this subnet fully contains exclude, we need to split
793
        // Recursively split this subnet in half until we isolate the excluded portion
794
        return $this->excludeRecursive($exclude);
42✔
795
    }
796

797
    /**
798
     * Exclude multiple subnets from this subnet.
799
     *
800
     * Applies multiple exclusions sequentially. The result is the set of subnets
801
     * remaining after all exclusions are applied.
802
     *
803
     * @param SubnetCalculator[] $excludes Subnets to exclude
804
     *
805
     * @return SubnetCalculator[] Remaining subnets
806
     */
807
    public function excludeAll(array $excludes): array
808
    {
809
        if (empty($excludes)) {
9✔
810
            return [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
1✔
811
        }
812

813
        // Start with this subnet
814
        $remaining = [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
8✔
815

816
        // Apply each exclusion to all remaining subnets
817
        foreach ($excludes as $exclude) {
8✔
818
            $newRemaining = [];
8✔
819
            foreach ($remaining as $subnet) {
8✔
820
                $afterExclude = $subnet->exclude($exclude);
8✔
821
                foreach ($afterExclude as $s) {
8✔
822
                    $newRemaining[] = $s;
8✔
823
                }
824
            }
825
            $remaining = $newRemaining;
8✔
826
        }
827

828
        return $remaining;
8✔
829
    }
830

831
    /**
832
     * Recursively exclude a subnet by splitting into halves.
833
     *
834
     * This method splits the current subnet into two halves and recursively
835
     * processes each half. Halves that don't overlap with the exclusion are
836
     * kept as-is. Halves that are fully contained by the exclusion are discarded.
837
     * Halves that partially overlap are split further.
838
     *
839
     * @param SubnetCalculator $exclude Subnet to exclude
840
     *
841
     * @return SubnetCalculator[] Remaining subnets
842
     */
843
    private function excludeRecursive(SubnetCalculator $exclude): array
844
    {
845
        // Can't split smaller than /32
846
        if ($this->networkSize >= 32) {
42✔
847
            // This /32 overlaps with exclude, so it's excluded
UNCOV
848
            return [];
×
849
        }
850

851
        // Split into two halves
852
        $newPrefix = $this->networkSize + 1;
42✔
853
        $firstHalf = new SubnetCalculator($this->getNetworkPortion(), $newPrefix);
42✔
854
        $secondHalfStart = $this->getNetworkPortionInteger() + ($this->getNumberIPAddresses() / 2);
42✔
855
        // Handle signed/unsigned conversion
856
        $secondHalfIp = $this->convertIpToDottedQuad((int) $secondHalfStart);
42✔
857
        $secondHalf = new SubnetCalculator($secondHalfIp, $newPrefix);
42✔
858

859
        $result = [];
42✔
860

861
        // Process first half
862
        if (!$firstHalf->overlaps($exclude)) {
42✔
863
            // No overlap, keep the whole half
864
            $result[] = $firstHalf;
21✔
865
        } elseif ($exclude->contains($firstHalf)) {
32✔
866
            // Fully excluded, discard
867
        } else {
868
            // Partial overlap, recurse
869
            $subResult = $firstHalf->excludeRecursive($exclude);
23✔
870
            foreach ($subResult as $s) {
23✔
871
                $result[] = $s;
23✔
872
            }
873
        }
874

875
        // Process second half
876
        if (!$secondHalf->overlaps($exclude)) {
42✔
877
            // No overlap, keep the whole half
878
            $result[] = $secondHalf;
32✔
879
        } elseif ($exclude->contains($secondHalf)) {
21✔
880
            // Fully excluded, discard
881
        } else {
882
            // Partial overlap, recurse
883
            $subResult = $secondHalf->excludeRecursive($exclude);
9✔
884
            foreach ($subResult as $s) {
9✔
885
                $result[] = $s;
9✔
886
            }
887
        }
888

889
        return $result;
42✔
890
    }
891

892
    /* ****************************** *
893
     * ADJACENT SUBNET NAVIGATION
894
     * ****************************** */
895

896
    /**
897
     * Get the next subnet of the same size.
898
     *
899
     * Returns a new SubnetCalculator representing the subnet immediately following
900
     * this one in the IP address space, using the same network size (CIDR prefix).
901
     *
902
     * Useful for sequential IP allocation and network expansion planning.
903
     *
904
     * @return SubnetCalculator The next adjacent subnet
905
     *
906
     * @throws \RuntimeException If the next subnet would exceed the valid IPv4 range (255.255.255.255)
907
     */
908
    public function getNextSubnet(): SubnetCalculator
909
    {
910
        $addressCount = $this->getNumberIPAddresses();
33✔
911
        $currentNetworkStart = $this->getNetworkPortionInteger();
33✔
912

913
        // Calculate next subnet start as unsigned integer
914
        $nextNetworkStartUnsigned = (int) \sprintf('%u', $currentNetworkStart) + $addressCount;
33✔
915

916
        // Check if we would exceed the valid IPv4 range (max is 4294967295 = 255.255.255.255)
917
        // The next subnet start must be within valid range, AND the entire subnet must fit
918
        $maxValidIp = 4294967295; // 0xFFFFFFFF
33✔
919
        if ($nextNetworkStartUnsigned > $maxValidIp || ($nextNetworkStartUnsigned + $addressCount - 1) > $maxValidIp) {
33✔
920
            throw new \RuntimeException('Next subnet would exceed valid IPv4 address range.');
3✔
921
        }
922

923
        // Convert back to signed int for long2ip
924
        $nextNetworkStart = $nextNetworkStartUnsigned > 2147483647
30✔
925
            ? $nextNetworkStartUnsigned - 4294967296
21✔
926
            : $nextNetworkStartUnsigned;
30✔
927

928
        $nextIp = $this->convertIpToDottedQuad((int) $nextNetworkStart);
30✔
929

930
        return new SubnetCalculator($nextIp, $this->networkSize);
30✔
931
    }
932

933
    /**
934
     * Get the previous subnet of the same size.
935
     *
936
     * Returns a new SubnetCalculator representing the subnet immediately preceding
937
     * this one in the IP address space, using the same network size (CIDR prefix).
938
     *
939
     * Useful for navigating backward through allocated IP ranges.
940
     *
941
     * @return SubnetCalculator The previous adjacent subnet
942
     *
943
     * @throws \RuntimeException If the previous subnet would be below 0.0.0.0
944
     */
945
    public function getPreviousSubnet(): SubnetCalculator
946
    {
947
        $addressCount = $this->getNumberIPAddresses();
33✔
948
        $currentNetworkStart = $this->getNetworkPortionInteger();
33✔
949

950
        // Convert to unsigned for calculation
951
        $currentNetworkStartUnsigned = (int) \sprintf('%u', $currentNetworkStart);
33✔
952

953
        // Check if we would go below 0.0.0.0
954
        if ($currentNetworkStartUnsigned < $addressCount) {
33✔
955
            throw new \RuntimeException('Previous subnet would be below valid IPv4 address range (0.0.0.0).');
3✔
956
        }
957

958
        $previousNetworkStartUnsigned = $currentNetworkStartUnsigned - $addressCount;
31✔
959

960
        // Convert back to signed int for long2ip
961
        $previousNetworkStart = $previousNetworkStartUnsigned > 2147483647
31✔
962
            ? $previousNetworkStartUnsigned - 4294967296
16✔
963
            : $previousNetworkStartUnsigned;
31✔
964

965
        $previousIp = $this->convertIpToDottedQuad((int) $previousNetworkStart);
31✔
966

967
        return new SubnetCalculator($previousIp, $this->networkSize);
31✔
968
    }
969

970
    /**
971
     * Get multiple adjacent subnets.
972
     *
973
     * Returns an array of SubnetCalculator objects representing adjacent subnets
974
     * of the same size. Positive count returns subnets forward (higher IPs),
975
     * negative count returns subnets backward (lower IPs).
976
     *
977
     * Useful for bulk allocation planning or viewing a range of subnets.
978
     *
979
     * @param int $count Number of subnets to return (positive = forward, negative = backward)
980
     *
981
     * @return SubnetCalculator[] Array of adjacent subnets
982
     *
983
     * @throws \RuntimeException If any requested subnet would exceed valid IPv4 range
984
     */
985
    public function getAdjacentSubnets(int $count): array
986
    {
987
        if ($count === 0) {
16✔
988
            return [];
1✔
989
        }
990

991
        $subnets = [];
15✔
992
        $current = $this;
15✔
993

994
        if ($count > 0) {
15✔
995
            // Forward direction
996
            for ($i = 0; $i < $count; $i++) {
8✔
997
                $current = $current->getNextSubnet();
8✔
998
                $subnets[] = $current;
7✔
999
            }
1000
        } else {
1001
            // Backward direction
1002
            for ($i = 0; $i > $count; $i--) {
7✔
1003
                $current = $current->getPreviousSubnet();
7✔
1004
                $subnets[] = $current;
7✔
1005
            }
1006
        }
1007

1008
        return $subnets;
13✔
1009
    }
1010

1011
    /* ******************************************* *
1012
     * PRIVATE/RESERVED IP RANGE DETECTION METHODS
1013
     * ******************************************* */
1014

1015
    /**
1016
     * Check if the IP address is in a private range (RFC 1918).
1017
     *
1018
     * Private address ranges:
1019
     *   - 10.0.0.0/8     (10.0.0.0 - 10.255.255.255)
1020
     *   - 172.16.0.0/12  (172.16.0.0 - 172.31.255.255)
1021
     *   - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
1022
     *
1023
     * @link https://datatracker.ietf.org/doc/html/rfc1918 RFC 1918 - Address Allocation for Private Internets
1024
     *
1025
     * @return bool True if the IP address is in a private range
1026
     */
1027
    public function isPrivate(): bool
1028
    {
1029
        $ip = $this->getIPAddressInteger();
134✔
1030

1031
        // 10.0.0.0/8: 10.0.0.0 - 10.255.255.255
1032
        if ($this->isInRange($ip, 0x0A000000, 0x0AFFFFFF)) {
134✔
1033
            return true;
29✔
1034
        }
1035

1036
        // 172.16.0.0/12: 172.16.0.0 - 172.31.255.255
1037
        if ($this->isInRange($ip, 0xAC100000, 0xAC1FFFFF)) {
106✔
1038
            return true;
14✔
1039
        }
1040

1041
        // 192.168.0.0/16: 192.168.0.0 - 192.168.255.255
1042
        if ($this->isInRange($ip, 0xC0A80000, 0xC0A8FFFF)) {
92✔
1043
            return true;
43✔
1044
        }
1045

1046
        return false;
52✔
1047
    }
1048

1049
    /**
1050
     * Check if the IP address is publicly routable.
1051
     *
1052
     * An IP is public if it is not in any of the special-purpose address ranges:
1053
     * private, loopback, link-local, multicast, CGN, documentation, benchmarking,
1054
     * reserved, limited broadcast, or "this" network.
1055
     *
1056
     * @return bool True if the IP address is publicly routable
1057
     */
1058
    public function isPublic(): bool
1059
    {
1060
        return !$this->isPrivate()
28✔
1061
            && !$this->isLoopback()
28✔
1062
            && !$this->isLinkLocal()
28✔
1063
            && !$this->isMulticast()
28✔
1064
            && !$this->isCarrierGradeNat()
28✔
1065
            && !$this->isDocumentation()
28✔
1066
            && !$this->isBenchmarking()
28✔
1067
            && !$this->isReserved()
28✔
1068
            && !$this->isThisNetwork();
28✔
1069
    }
1070

1071
    /**
1072
     * Check if the IP address is in the loopback range (127.0.0.0/8).
1073
     *
1074
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
1075
     *
1076
     * @return bool True if the IP address is in the loopback range
1077
     */
1078
    public function isLoopback(): bool
1079
    {
1080
        $ip = $this->getIPAddressInteger();
54✔
1081

1082
        // 127.0.0.0/8: 127.0.0.0 - 127.255.255.255
1083
        return $this->isInRange($ip, 0x7F000000, 0x7FFFFFFF);
54✔
1084
    }
1085

1086
    /**
1087
     * Check if the IP address is link-local (169.254.0.0/16).
1088
     *
1089
     * Link-local addresses are used for automatic private IP addressing (APIPA)
1090
     * when DHCP is not available.
1091
     *
1092
     * @link https://datatracker.ietf.org/doc/html/rfc3927 RFC 3927 - Dynamic Configuration of IPv4 Link-Local Addresses
1093
     *
1094
     * @return bool True if the IP address is link-local
1095
     */
1096
    public function isLinkLocal(): bool
1097
    {
1098
        $ip = $this->getIPAddressInteger();
49✔
1099

1100
        // 169.254.0.0/16: 169.254.0.0 - 169.254.255.255
1101
        return $this->isInRange($ip, 0xA9FE0000, 0xA9FEFFFF);
49✔
1102
    }
1103

1104
    /**
1105
     * Check if the IP address is multicast (224.0.0.0/4).
1106
     *
1107
     * @link https://datatracker.ietf.org/doc/html/rfc5771 RFC 5771 - IANA Guidelines for IPv4 Multicast Address Assignments
1108
     *
1109
     * @return bool True if the IP address is multicast
1110
     */
1111
    public function isMulticast(): bool
1112
    {
1113
        $ip = $this->getIPAddressInteger();
39✔
1114

1115
        // 224.0.0.0/4: 224.0.0.0 - 239.255.255.255
1116
        return $this->isInRange($ip, 0xE0000000, 0xEFFFFFFF);
39✔
1117
    }
1118

1119
    /**
1120
     * Check if the IP address is in Carrier-Grade NAT range (100.64.0.0/10).
1121
     *
1122
     * Also known as Shared Address Space, used by ISPs for CGN deployments.
1123
     *
1124
     * @link https://datatracker.ietf.org/doc/html/rfc6598 RFC 6598 - IANA-Reserved IPv4 Prefix for Shared Address Space
1125
     *
1126
     * @return bool True if the IP address is in the CGN range
1127
     */
1128
    public function isCarrierGradeNat(): bool
1129
    {
1130
        $ip = $this->getIPAddressInteger();
43✔
1131

1132
        // 100.64.0.0/10: 100.64.0.0 - 100.127.255.255
1133
        return $this->isInRange($ip, 0x64400000, 0x647FFFFF);
43✔
1134
    }
1135

1136
    /**
1137
     * Check if the IP address is reserved for documentation (RFC 5737).
1138
     *
1139
     * Documentation ranges (TEST-NET-1, TEST-NET-2, TEST-NET-3):
1140
     *   - 192.0.2.0/24   (TEST-NET-1)
1141
     *   - 198.51.100.0/24 (TEST-NET-2)
1142
     *   - 203.0.113.0/24  (TEST-NET-3)
1143
     *
1144
     * @link https://datatracker.ietf.org/doc/html/rfc5737 RFC 5737 - IPv4 Address Blocks Reserved for Documentation
1145
     *
1146
     * @return bool True if the IP address is reserved for documentation
1147
     */
1148
    public function isDocumentation(): bool
1149
    {
1150
        $ip = $this->getIPAddressInteger();
48✔
1151

1152
        // 192.0.2.0/24 (TEST-NET-1): 192.0.2.0 - 192.0.2.255
1153
        if ($this->isInRange($ip, 0xC0000200, 0xC00002FF)) {
48✔
1154
            return true;
6✔
1155
        }
1156

1157
        // 198.51.100.0/24 (TEST-NET-2): 198.51.100.0 - 198.51.100.255
1158
        if ($this->isInRange($ip, 0xC6336400, 0xC63364FF)) {
42✔
1159
            return true;
4✔
1160
        }
1161

1162
        // 203.0.113.0/24 (TEST-NET-3): 203.0.113.0 - 203.0.113.255
1163
        if ($this->isInRange($ip, 0xCB007100, 0xCB0071FF)) {
38✔
1164
            return true;
4✔
1165
        }
1166

1167
        return false;
34✔
1168
    }
1169

1170
    /**
1171
     * Check if the IP address is reserved for benchmarking (198.18.0.0/15).
1172
     *
1173
     * @link https://datatracker.ietf.org/doc/html/rfc2544 RFC 2544 - Benchmarking Methodology for Network Interconnect Devices
1174
     *
1175
     * @return bool True if the IP address is reserved for benchmarking
1176
     */
1177
    public function isBenchmarking(): bool
1178
    {
1179
        $ip = $this->getIPAddressInteger();
35✔
1180

1181
        // 198.18.0.0/15: 198.18.0.0 - 198.19.255.255
1182
        return $this->isInRange($ip, 0xC6120000, 0xC613FFFF);
35✔
1183
    }
1184

1185
    /**
1186
     * Check if the IP address is reserved for future use (240.0.0.0/4).
1187
     *
1188
     * Note: This includes 255.255.255.255 (limited broadcast), which can be
1189
     * separately identified using isLimitedBroadcast().
1190
     *
1191
     * @link https://datatracker.ietf.org/doc/html/rfc1112 RFC 1112 - Host Extensions for IP Multicasting
1192
     *
1193
     * @return bool True if the IP address is reserved for future use
1194
     */
1195
    public function isReserved(): bool
1196
    {
1197
        $ip = $this->getIPAddressInteger();
27✔
1198

1199
        // 240.0.0.0/4: 240.0.0.0 - 255.255.255.255
1200
        return $this->isInRange($ip, 0xF0000000, 0xFFFFFFFF);
27✔
1201
    }
1202

1203
    /**
1204
     * Check if the IP address is the broadcast address (255.255.255.255/32).
1205
     *
1206
     * @link https://datatracker.ietf.org/doc/html/rfc919 RFC 919 - Broadcasting Internet Datagrams
1207
     *
1208
     * @return bool True if the IP address is the limited broadcast address
1209
     */
1210
    public function isLimitedBroadcast(): bool
1211
    {
1212
        return $this->ipAddress === '255.255.255.255';
13✔
1213
    }
1214

1215
    /**
1216
     * Check if the IP address is in the "this" network range (0.0.0.0/8).
1217
     *
1218
     * Addresses in this range represent "this host on this network" and are
1219
     * only valid as source addresses.
1220
     *
1221
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
1222
     *
1223
     * @return bool True if the IP address is in the "this" network range
1224
     */
1225
    public function isThisNetwork(): bool
1226
    {
1227
        $ip = $this->getIPAddressInteger();
104✔
1228

1229
        // 0.0.0.0/8: 0.0.0.0 - 0.255.255.255
1230
        return $this->isInRange($ip, 0x00000000, 0x00FFFFFF);
104✔
1231
    }
1232

1233
    /**
1234
     * Get the address type classification.
1235
     *
1236
     * Returns a string identifying the type of address. The order of checks matters:
1237
     * more specific types (like limited-broadcast) are checked before broader types
1238
     * (like reserved) that would also match.
1239
     *
1240
     * @return string Address type: 'private', 'public', 'loopback', 'link-local',
1241
     *                'multicast', 'carrier-grade-nat', 'documentation', 'benchmarking',
1242
     *                'reserved', 'limited-broadcast', 'this-network'
1243
     */
1244
    public function getAddressType(): string
1245
    {
1246
        // Check specific types first, then broader types
1247
        if ($this->isThisNetwork()) {
89✔
1248
            return 'this-network';
2✔
1249
        }
1250
        if ($this->isPrivate()) {
87✔
1251
            return 'private';
68✔
1252
        }
1253
        if ($this->isLoopback()) {
21✔
1254
            return 'loopback';
2✔
1255
        }
1256
        if ($this->isLinkLocal()) {
19✔
1257
            return 'link-local';
2✔
1258
        }
1259
        if ($this->isCarrierGradeNat()) {
17✔
1260
            return 'carrier-grade-nat';
2✔
1261
        }
1262
        if ($this->isDocumentation()) {
15✔
1263
            return 'documentation';
3✔
1264
        }
1265
        if ($this->isBenchmarking()) {
12✔
1266
            return 'benchmarking';
2✔
1267
        }
1268
        if ($this->isMulticast()) {
10✔
1269
            return 'multicast';
2✔
1270
        }
1271
        // Check limited broadcast before reserved (since it's a subset)
1272
        if ($this->isLimitedBroadcast()) {
8✔
1273
            return 'limited-broadcast';
1✔
1274
        }
1275
        if ($this->isReserved()) {
7✔
1276
            return 'reserved';
2✔
1277
        }
1278

1279
        return 'public';
5✔
1280
    }
1281

1282
    /**
1283
     * Check if an IP integer is within a given range (inclusive).
1284
     *
1285
     * @param int $ip    IP address as integer
1286
     * @param int $start Start of range as integer
1287
     * @param int $end   End of range as integer
1288
     *
1289
     * @return bool True if the IP is within the range
1290
     */
1291
    private function isInRange(int $ip, int $start, int $end): bool
1292
    {
1293
        // Handle PHP's signed integer representation for high IP addresses
1294
        // Convert to unsigned for comparison using sprintf
1295
        $ipUnsigned    = \sprintf('%u', $ip);
210✔
1296
        $startUnsigned = \sprintf('%u', $start);
210✔
1297
        $endUnsigned   = \sprintf('%u', $end);
210✔
1298

1299
        return $ipUnsigned >= $startUnsigned && $ipUnsigned <= $endUnsigned;
210✔
1300
    }
1301

1302
    /* ****************************************** *
1303
     * NETWORK CLASS INFORMATION (LEGACY) METHODS
1304
     * ****************************************** */
1305

1306
    /**
1307
     * Get the legacy network class.
1308
     *
1309
     * Returns the classful network class based on the first octet of the IP address.
1310
     * While classful networking is obsolete (RFC 4632 established CIDR), it's still
1311
     * referenced in education, certifications, and some legacy systems.
1312
     *
1313
     * Network class definitions (RFC 791):
1314
     *   Class A: 0-127   (leading bit 0)     - Note: 0 is "this network", 127 is loopback
1315
     *   Class B: 128-191 (leading bits 10)
1316
     *   Class C: 192-223 (leading bits 110)
1317
     *   Class D: 224-239 (leading bits 1110) - Multicast
1318
     *   Class E: 240-255 (leading bits 1111) - Reserved for future use
1319
     *
1320
     * @link https://datatracker.ietf.org/doc/html/rfc791 RFC 791 - Internet Protocol
1321
     * @link https://datatracker.ietf.org/doc/html/rfc4632 RFC 4632 - CIDR (obsoletes classful routing)
1322
     *
1323
     * @return string 'A', 'B', 'C', 'D' (multicast), or 'E' (reserved)
1324
     */
1325
    public function getNetworkClass(): string
1326
    {
1327
        $firstOctet = (int) $this->quads[0];
88✔
1328

1329
        // Class A: 0-127 (includes 0.x.x.x "this network" and 127.x.x.x loopback)
1330
        if ($firstOctet <= 127) {
88✔
1331
            return 'A';
30✔
1332
        }
1333

1334
        // Class B: 128-191
1335
        if ($firstOctet <= 191) {
60✔
1336
            return 'B';
21✔
1337
        }
1338

1339
        // Class C: 192-223
1340
        if ($firstOctet <= 223) {
41✔
1341
            return 'C';
22✔
1342
        }
1343

1344
        // Class D: 224-239 (Multicast)
1345
        if ($firstOctet <= 239) {
20✔
1346
            return 'D';
11✔
1347
        }
1348

1349
        // Class E: 240-255 (Reserved)
1350
        return 'E';
10✔
1351
    }
1352

1353
    /**
1354
     * Get the default classful mask for this IP.
1355
     *
1356
     * Returns the default subnet mask for the IP's network class. Classes D and E
1357
     * (multicast and reserved) do not have a default mask and return null.
1358
     *
1359
     * Default masks:
1360
     *   Class A: 255.0.0.0 (/8)
1361
     *   Class B: 255.255.0.0 (/16)
1362
     *   Class C: 255.255.255.0 (/24)
1363
     *   Class D/E: null (no default mask)
1364
     *
1365
     * @link https://datatracker.ietf.org/doc/html/rfc791 RFC 791 - Internet Protocol
1366
     *
1367
     * @return string|null Dotted quad mask (e.g., "255.0.0.0" for Class A), or null for D/E
1368
     */
1369
    public function getDefaultClassMask(): ?string
1370
    {
1371
        $class = $this->getNetworkClass();
19✔
1372

1373
        switch ($class) {
19✔
1374
            case 'A':
19✔
1375
                return '255.0.0.0';
7✔
1376
            case 'B':
12✔
1377
                return '255.255.0.0';
4✔
1378
            case 'C':
8✔
1379
                return '255.255.255.0';
4✔
1380
            default:
1381
                // Class D (multicast) and E (reserved) have no default mask
1382
                return null;
4✔
1383
        }
1384
    }
1385

1386
    /**
1387
     * Get the default classful prefix.
1388
     *
1389
     * Returns the default CIDR prefix for the IP's network class. Classes D and E
1390
     * (multicast and reserved) do not have a default prefix and return null.
1391
     *
1392
     * Default prefixes:
1393
     *   Class A: 8
1394
     *   Class B: 16
1395
     *   Class C: 24
1396
     *   Class D/E: null (no default prefix)
1397
     *
1398
     * @link https://datatracker.ietf.org/doc/html/rfc791 RFC 791 - Internet Protocol
1399
     *
1400
     * @return int|null CIDR prefix (8, 16, 24), or null for D/E
1401
     */
1402
    public function getDefaultClassPrefix(): ?int
1403
    {
1404
        $class = $this->getNetworkClass();
40✔
1405

1406
        switch ($class) {
40✔
1407
            case 'A':
40✔
1408
                return 8;
13✔
1409
            case 'B':
28✔
1410
                return 16;
10✔
1411
            case 'C':
19✔
1412
                return 24;
11✔
1413
            default:
1414
                // Class D (multicast) and E (reserved) have no default prefix
1415
                return null;
8✔
1416
        }
1417
    }
1418

1419
    /**
1420
     * Check if the current subnet uses the classful default mask.
1421
     *
1422
     * Returns true if the network size matches the default classful prefix for
1423
     * this IP's class. Classes D and E always return false as they have no
1424
     * default mask.
1425
     *
1426
     * Examples:
1427
     *   10.0.0.0/8 → true (Class A with default /8)
1428
     *   10.0.0.0/24 → false (Class A but subnetted to /24)
1429
     *   172.16.0.0/16 → true (Class B with default /16)
1430
     *   172.16.0.0/12 → false (Class B but supernetted to /12)
1431
     *   224.0.0.1/32 → false (Class D, no default)
1432
     *
1433
     * @return bool True if the subnet uses the classful default mask
1434
     */
1435
    public function isClassful(): bool
1436
    {
1437
        $defaultPrefix = $this->getDefaultClassPrefix();
26✔
1438

1439
        // Class D and E have no default prefix, so they can never be classful
1440
        if ($defaultPrefix === null) {
26✔
1441
            return false;
4✔
1442
        }
1443

1444
        return $this->networkSize === $defaultPrefix;
22✔
1445
    }
1446

1447
    /* ************************** *
1448
     * UTILIZATION STATISTICS
1449
     * ************************** */
1450

1451
    /**
1452
     * Get the percentage of addresses that are usable hosts.
1453
     *
1454
     * Calculates (usable hosts / total IP addresses) * 100.
1455
     *
1456
     * Special cases per RFC 3021:
1457
     *   - /31: 100% (2 usable of 2 total - no network/broadcast overhead)
1458
     *   - /32: 100% (1 usable of 1 total - single host)
1459
     *
1460
     * Helpful for capacity planning and choosing optimal subnet sizes.
1461
     *
1462
     * @return float Percentage (0.0 to 100.0)
1463
     */
1464
    public function getUsableHostPercentage(): float
1465
    {
1466
        $totalAddresses = $this->getNumberIPAddresses();
9✔
1467
        $usableHosts = $this->getNumberAddressableHosts();
9✔
1468

1469
        return ($usableHosts / $totalAddresses) * 100.0;
9✔
1470
    }
1471

1472
    /**
1473
     * Get the number of addresses not usable as hosts.
1474
     *
1475
     * For most networks, this is 2 (network address + broadcast address).
1476
     * Special cases per RFC 3021:
1477
     *   - /31: 0 (both addresses are usable - point-to-point link)
1478
     *   - /32: 0 (single host network)
1479
     *
1480
     * @return int Number of unusable addresses
1481
     */
1482
    public function getUnusableAddressCount(): int
1483
    {
1484
        $totalAddresses = $this->getNumberIPAddresses();
7✔
1485
        $usableHosts = $this->getNumberAddressableHosts();
7✔
1486

1487
        return $totalAddresses - $usableHosts;
7✔
1488
    }
1489

1490
    /**
1491
     * Calculate efficiency for a given host requirement.
1492
     *
1493
     * Returns what percentage of usable addresses would be utilized
1494
     * if the specified number of hosts were deployed in this subnet.
1495
     *
1496
     * Values > 100% indicate the subnet is too small to accommodate
1497
     * the required number of hosts.
1498
     *
1499
     * Helpful for evaluating subnet sizing decisions.
1500
     *
1501
     * @param int $requiredHosts Number of hosts needed (must be >= 0)
1502
     *
1503
     * @return float Percentage (0.0 to 100.0+), or > 100 if insufficient
1504
     *
1505
     * @throws \InvalidArgumentException If $requiredHosts is negative
1506
     */
1507
    public function getUtilizationForHosts(int $requiredHosts): float
1508
    {
1509
        if ($requiredHosts < 0) {
13✔
1510
            throw new \InvalidArgumentException('Required hosts cannot be negative.');
1✔
1511
        }
1512

1513
        if ($requiredHosts === 0) {
12✔
1514
            return 0.0;
2✔
1515
        }
1516

1517
        $usableHosts = $this->getNumberAddressableHosts();
10✔
1518

1519
        return ($requiredHosts / $usableHosts) * 100.0;
10✔
1520
    }
1521

1522
    /**
1523
     * Get wasted addresses for a given host requirement.
1524
     *
1525
     * Returns the number of usable addresses that would remain unused
1526
     * if the specified number of hosts were deployed in this subnet.
1527
     *
1528
     * A negative value indicates the subnet is too small (insufficient capacity).
1529
     *
1530
     * Helpful for capacity planning and minimizing IP address waste.
1531
     *
1532
     * @param int $requiredHosts Number of hosts needed (must be >= 0)
1533
     *
1534
     * @return int Number of unused usable addresses (negative if insufficient)
1535
     *
1536
     * @throws \InvalidArgumentException If $requiredHosts is negative
1537
     */
1538
    public function getWastedAddresses(int $requiredHosts): int
1539
    {
1540
        if ($requiredHosts < 0) {
13✔
1541
            throw new \InvalidArgumentException('Required hosts cannot be negative.');
1✔
1542
        }
1543

1544
        $usableHosts = $this->getNumberAddressableHosts();
12✔
1545

1546
        return $usableHosts - $requiredHosts;
12✔
1547
    }
1548

1549
    /**
1550
     * Get the IPv4 Arpa Domain
1551
     *
1552
     * Reverse DNS lookups for IPv4 addresses use the special domain in-addr.arpa.
1553
     * In this domain, an IPv4 address is represented as a concatenated sequence of four decimal numbers,
1554
     * separated by dots, to which is appended the second level domain suffix .in-addr.arpa.
1555
     *
1556
     * The four decimal numbers are obtained by splitting the 32-bit IPv4 address into four octets and converting
1557
     * each octet into a decimal number. These decimal numbers are then concatenated in the order:
1558
     * least significant octet first (leftmost), to most significant octet last (rightmost).
1559
     * It is important to note that this is the reverse order to the usual dotted-decimal convention for writing
1560
     * IPv4 addresses in textual form.
1561
     *
1562
     * 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.
1563
     *
1564
     * @link https://en.wikipedia.org/wiki/Reverse_DNS_lookup
1565
     *
1566
     * @return string
1567
     */
1568
    public function getIPv4ArpaDomain(): string
1569
    {
1570
        $reverseQuads = \implode('.', \array_reverse($this->quads));
73✔
1571
        return $reverseQuads . '.in-addr.arpa';
73✔
1572
    }
1573

1574
    /**
1575
     * Get subnet calculations as an associated array
1576
     *
1577
     * @return mixed[] of subnet calculations
1578
     */
1579
    public function getSubnetArrayReport(): array
1580
    {
1581
        return $this->report->createArrayReport($this);
1✔
1582
    }
1583

1584
    /**
1585
     * Get subnet calculations as a JSON string
1586
     *
1587
     * @return string JSON string of subnet calculations
1588
     *
1589
     * @throws \RuntimeException if there is a JSON encode error
1590
     */
1591
    public function getSubnetJsonReport(): string
1592
    {
1593
        $json = $this->report->createJsonReport($this);
2✔
1594

1595
        if ($json === false) {
2✔
1596
            throw new \RuntimeException('JSON report failure: ' . json_last_error_msg());
1✔
1597
        }
1598

1599
        return $json;
1✔
1600
    }
1601

1602
    /**
1603
     * Print a report of subnet calculations
1604
     */
1605
    public function printSubnetReport(): void
1606
    {
1607
        $this->report->printReport($this);
1✔
1608
    }
1✔
1609

1610
    /**
1611
     * Print a report of subnet calculations
1612
     *
1613
     * @return string Subnet Calculator report
1614
     */
1615
    public function getPrintableReport(): string
1616
    {
1617
        return $this->report->createPrintableReport($this);
1✔
1618
    }
1619

1620
    /**
1621
     * String representation of a report of subnet calculations
1622
     *
1623
     * @return string
1624
     */
1625
    public function __toString(): string
1626
    {
1627
        return $this->report->createPrintableReport($this);
58✔
1628
    }
1629

1630
    /* ************** *
1631
     * PHP INTERFACES
1632
     * ************** */
1633

1634
    /**
1635
     * \JsonSerializable interface
1636
     *
1637
     * @return mixed[]
1638
     */
1639
    public function jsonSerialize(): array
1640
    {
1641
        return $this->report->createArrayReport($this);
1✔
1642
    }
1643

1644
    /* ********************** *
1645
     * PRIVATE IMPLEMENTATION
1646
     * ********************** */
1647

1648
    /**
1649
     * Calculate subnet mask
1650
     *
1651
     * @param  int $networkSize
1652
     *
1653
     * @return int
1654
     */
1655
    private function calculateSubnetMask(int $networkSize): int
1656
    {
1657
        return 0xFFFFFFFF << (32 - $networkSize);
1,427✔
1658
    }
1659

1660
    /**
1661
     * Calculate IP address for formatting
1662
     *
1663
     * @param string $format    sprintf format to determine if decimal, hex or binary
1664
     * @param string $separator implode separator for formatting quads vs hex and binary
1665
     *
1666
     * @return string formatted IP address
1667
     */
1668
    private function ipAddressCalculation(string $format, string $separator = ''): string
1669
    {
1670
        return \implode($separator, array_map(
71✔
1671
            function ($quad) use ($format) {
1672
                return \sprintf($format, $quad);
71✔
1673
            },
71✔
1674
            $this->quads
71✔
1675
        ));
1676
    }
1677

1678
    /**
1679
     * Subnet calculation
1680
     *
1681
     * @param string $format    sprintf format to determine if decimal, hex or binary
1682
     * @param string $separator implode separator for formatting quads vs hex and binary
1683
     *
1684
     * @return string subnet
1685
     */
1686
    private function subnetCalculation(string $format, string $separator = ''): string
1687
    {
1688
        $maskQuads = [
1689
            \sprintf($format, ($this->subnetMask >> 24) & 0xFF),
116✔
1690
            \sprintf($format, ($this->subnetMask >> 16) & 0xFF),
116✔
1691
            \sprintf($format, ($this->subnetMask >>  8) & 0xFF),
116✔
1692
            \sprintf($format, ($this->subnetMask >>  0) & 0xFF),
116✔
1693
        ];
1694

1695
        return implode($separator, $maskQuads);
116✔
1696
    }
1697

1698
    /**
1699
     * Wildcard mask calculation
1700
     *
1701
     * The wildcard mask is the bitwise inverse of the subnet mask.
1702
     *
1703
     * @param string $format    sprintf format to determine if decimal, hex or binary
1704
     * @param string $separator implode separator for formatting quads vs hex and binary
1705
     *
1706
     * @return string wildcard mask
1707
     */
1708
    private function wildcardCalculation(string $format, string $separator = ''): string
1709
    {
1710
        $wildcardMask = ~$this->subnetMask;
124✔
1711
        $maskQuads = [
1712
            \sprintf($format, ($wildcardMask >> 24) & 0xFF),
124✔
1713
            \sprintf($format, ($wildcardMask >> 16) & 0xFF),
124✔
1714
            \sprintf($format, ($wildcardMask >>  8) & 0xFF),
124✔
1715
            \sprintf($format, ($wildcardMask >>  0) & 0xFF),
124✔
1716
        ];
1717

1718
        return implode($separator, $maskQuads);
124✔
1719
    }
1720

1721
    /**
1722
     * Calculate network portion for formatting
1723
     *
1724
     * @param string $format    sprintf format to determine if decimal, hex or binary
1725
     * @param string $separator implode separator for formatting quads vs hex and binary
1726
     *
1727
     * @return string formatted subnet mask
1728
     */
1729
    private function networkCalculation(string $format, string $separator = ''): string
1730
    {
1731
        $networkQuads = [
1732
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
682✔
1733
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
682✔
1734
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
682✔
1735
            \sprintf($format, (int) $this->quads[3] & ($this->subnetMask >>  0)),
682✔
1736
        ];
1737

1738
        return implode($separator, $networkQuads);
682✔
1739
    }
1740

1741
    /**
1742
     * Calculate host portion for formatting
1743
     *
1744
     * @param string $format    sprintf format to determine if decimal, hex or binary
1745
     * @param string $separator implode separator for formatting quads vs hex and binary
1746
     *
1747
     * @return string formatted subnet mask
1748
     */
1749
    private function hostCalculation(string $format, string $separator = ''): string
1750
    {
1751
        $networkQuads = [
1752
            \sprintf($format, (int) $this->quads[0] & ~($this->subnetMask >> 24)),
97✔
1753
            \sprintf($format, (int) $this->quads[1] & ~($this->subnetMask >> 16)),
97✔
1754
            \sprintf($format, (int) $this->quads[2] & ~($this->subnetMask >>  8)),
97✔
1755
            \sprintf($format, (int) $this->quads[3] & ~($this->subnetMask >>  0)),
97✔
1756
        ];
1757

1758
        return implode($separator, $networkQuads);
97✔
1759
    }
1760

1761
    /**
1762
     * Calculate min host for formatting
1763
     *
1764
     * @param string $format    sprintf format to determine if decimal, hex or binary
1765
     * @param string $separator implode separator for formatting quads vs hex and binary
1766
     *
1767
     * @return string formatted min host
1768
     */
1769
    private function minHostCalculation(string $format, string $separator = ''): string
1770
    {
1771
        $networkQuads = [
1772
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
145✔
1773
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
145✔
1774
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
145✔
1775
            \sprintf($format, ((int) $this->quads[3] & ($this->subnetMask >> 0)) + 1),
145✔
1776
        ];
1777

1778
        return implode($separator, $networkQuads);
145✔
1779
    }
1780

1781
    /**
1782
     * Calculate max host for formatting
1783
     *
1784
     * @param string $format    sprintf format to determine if decimal, hex or binary
1785
     * @param string $separator implode separator for formatting quads vs hex and binary
1786
     *
1787
     * @return string formatted max host
1788
     */
1789
    private function maxHostCalculation(string $format, string $separator = ''): string
1790
    {
1791
        $networkQuads      = $this->getNetworkPortionQuads();
145✔
1792
        $numberIpAddresses = $this->getNumberIPAddresses();
145✔
1793

1794
        $network_range_quads = [
1795
            \sprintf($format, ((int) $networkQuads[0] & ($this->subnetMask >> 24)) + ((($numberIpAddresses - 1) >> 24) & 0xFF)),
145✔
1796
            \sprintf($format, ((int) $networkQuads[1] & ($this->subnetMask >> 16)) + ((($numberIpAddresses - 1) >> 16) & 0xFF)),
145✔
1797
            \sprintf($format, ((int) $networkQuads[2] & ($this->subnetMask >>  8)) + ((($numberIpAddresses - 1) >>  8) & 0xFF)),
145✔
1798
            \sprintf($format, ((int) $networkQuads[3] & ($this->subnetMask >>  0)) + ((($numberIpAddresses - 1) >>  0) & 0xFE)),
145✔
1799
        ];
1800

1801
        return implode($separator, $network_range_quads);
145✔
1802
    }
1803

1804
    /**
1805
     * Validate IP address and network
1806
     *
1807
     * @param string $ipAddress   IP address in dotted quads format
1808
     * @param int    $networkSize Network size
1809
     *
1810
     * @throws \UnexpectedValueException IP or network size not valid
1811
     */
1812
    private function validateInputs(string $ipAddress, int $networkSize): void
1813
    {
1814
        if (!\filter_var($ipAddress, FILTER_VALIDATE_IP)) {
1,437✔
1815
            throw new \UnexpectedValueException("IP address $ipAddress not valid.");
13✔
1816
        }
1817
        if (($networkSize < 1) || ($networkSize > 32)) {
1,430✔
1818
            throw new \UnexpectedValueException("Network size $networkSize not valid.");
15✔
1819
        }
1820
    }
1,427✔
1821

1822
    /**
1823
     * Get the start and end of the IP address range as ints
1824
     *
1825
     * @return int[] [start IP, end IP]
1826
     */
1827
    private function getIPAddressRangeAsInts(): array
1828
    {
1829
        [$startIp, $endIp] = $this->getIPAddressRange();
285✔
1830
        $startIp = $this->convertIpToInt($startIp);
285✔
1831
        $endIp   = $this->convertIpToInt($endIp);
283✔
1832

1833
        return [$startIp, $endIp];
283✔
1834
    }
1835

1836
    /**
1837
     * Get the number of IP addresses in the given network size
1838
     *
1839
     * @param int $networkSize
1840
     *
1841
     * @return int Number of IP addresses
1842
     */
1843
    private function getNumberIPAddressesOfNetworkSize($networkSize): int
1844
    {
1845
        return \pow(2, (32 - $networkSize));
698✔
1846
    }
1847

1848

1849
    /**
1850
     * Convert a dotted-quad IP address to an integer
1851
     *
1852
     * @param string $ipAddress Dotted-quad IP address
1853
     *
1854
     * @return int Integer representation of an IP address
1855
     */
1856
    private function convertIpToInt(string $ipAddress): int
1857
    {
1858
        $ipAsInt = \ip2long($ipAddress);
655✔
1859
        if ($ipAsInt === false) {
655✔
1860
            throw new \RuntimeException('Invalid IP address string. Could not convert dotted-quad string address to an integer: ' . $ipAddress);
3✔
1861
        }
1862
        return $ipAsInt;
652✔
1863
    }
1864

1865
    /**
1866
     * Convert an integer IP address to a dotted-quad IP string
1867
     *
1868
     * @param int $ipAsInt Integer representation of an IP address
1869
     *
1870
     * @return string Dotted-quad IP address
1871
     */
1872
    private function convertIpToDottedQuad(int $ipAsInt): string
1873
    {
1874
        $ipDottedQuad = \long2ip($ipAsInt);
163✔
1875
        if ($ipDottedQuad == false) {
163✔
UNCOV
1876
            throw new \RuntimeException('Invalid IP address integer. Could not convert integer address to dotted-quad string: ' . $ipAsInt);
×
1877
        }
1878
        return $ipDottedQuad;
163✔
1879
    }
1880
}
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