assertTrue($cidr->isIpv4()); $this->assertSame(8, $cidr->originalPrefix()); $this->assertSame(104, $cidr->prefixLength()); $this->assertSame('10.0.0.0/8', $cidr->text()); } public function testParsesSlash32SingleHost(): void { $cidr = Cidr::fromString('192.168.1.42/32'); $this->assertTrue($cidr->contains(IpAddress::fromString('192.168.1.42'))); $this->assertFalse($cidr->contains(IpAddress::fromString('192.168.1.43'))); } public function testV4Slash24Containment(): void { $cidr = Cidr::fromString('10.0.0.0/24'); $this->assertTrue($cidr->contains(IpAddress::fromString('10.0.0.0'))); $this->assertTrue($cidr->contains(IpAddress::fromString('10.0.0.1'))); $this->assertTrue($cidr->contains(IpAddress::fromString('10.0.0.255'))); $this->assertFalse($cidr->contains(IpAddress::fromString('10.0.1.0'))); $this->assertFalse($cidr->contains(IpAddress::fromString('11.0.0.0'))); } public function testV4Slash16Containment(): void { $cidr = Cidr::fromString('172.16.0.0/16'); $this->assertTrue($cidr->contains(IpAddress::fromString('172.16.0.0'))); $this->assertTrue($cidr->contains(IpAddress::fromString('172.16.255.255'))); $this->assertFalse($cidr->contains(IpAddress::fromString('172.17.0.0'))); $this->assertFalse($cidr->contains(IpAddress::fromString('172.15.255.255'))); } public function testV4Slash0MatchesAllV4(): void { $cidr = Cidr::fromString('0.0.0.0/0'); $this->assertSame(96, $cidr->prefixLength()); $this->assertTrue($cidr->contains(IpAddress::fromString('0.0.0.0'))); $this->assertTrue($cidr->contains(IpAddress::fromString('255.255.255.255'))); $this->assertTrue($cidr->contains(IpAddress::fromString('1.2.3.4'))); // /0 of v4 = ::ffff:0:0/96 → does NOT match pure IPv6 $this->assertFalse($cidr->contains(IpAddress::fromString('2001:db8::1'))); } public function testV6Slash0MatchesEverything(): void { $cidr = Cidr::fromString('::/0'); $this->assertSame(0, $cidr->prefixLength()); $this->assertTrue($cidr->contains(IpAddress::fromString('::1'))); $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::1'))); // Even v4 (which is stored as v4-mapped v6) matches /0 $this->assertTrue($cidr->contains(IpAddress::fromString('1.2.3.4'))); } public function testV6Slash128SingleHost(): void { $cidr = Cidr::fromString('2001:db8::1/128'); $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::1'))); $this->assertFalse($cidr->contains(IpAddress::fromString('2001:db8::2'))); } public function testV6Slash64Containment(): void { $cidr = Cidr::fromString('2001:db8::/64'); $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::1'))); $this->assertTrue($cidr->contains(IpAddress::fromString('2001:db8::ffff:ffff:ffff:ffff'))); $this->assertFalse($cidr->contains(IpAddress::fromString('2001:db9::1'))); } public function testV6CidrDoesNotContainV4(): void { $cidr = Cidr::fromString('2001:db8::/32'); $this->assertFalse($cidr->contains(IpAddress::fromString('1.2.3.4'))); } public function testV4InV4MappedV6Cidr(): void { // ::ffff:1.0.0.0/120 covers v4-mapped 1.0.0.0/24 $cidr = Cidr::fromString('::ffff:1.0.0.0/120'); $this->assertFalse($cidr->isIpv4(), 'CIDR is in v6 syntax even though it covers v4'); $this->assertSame(120, $cidr->prefixLength()); $this->assertTrue($cidr->contains(IpAddress::fromString('1.0.0.5'))); $this->assertTrue($cidr->contains(IpAddress::fromString('1.0.0.255'))); $this->assertFalse($cidr->contains(IpAddress::fromString('1.0.1.0'))); } public function testV4MappedV6AddressInsideV4Cidr(): void { $cidr = Cidr::fromString('1.0.0.0/24'); // The v4-mapped IPv6 form must still be contained because internal storage is unified. $this->assertTrue($cidr->contains(IpAddress::fromString('::ffff:1.0.0.7'))); } public function testNetworkAddressMaskedFromHostBits(): void { $cidr = Cidr::fromString('10.1.2.99/24'); $expected = IpAddress::fromString('10.1.2.0'); $this->assertSame($expected->binary(), $cidr->network()); $this->assertSame('10.1.2.0/24', $cidr->text()); } public function testV6NetworkAddressMaskedFromHostBits(): void { $cidr = Cidr::fromString('2001:db8:abcd:1234::ff/56'); $this->assertSame('2001:db8:abcd:1200::/56', $cidr->text()); } public function testRejectsMissingSlash(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('10.0.0.0'); } public function testRejectsEmpty(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString(''); } public function testRejectsBadIp(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('garbage/8'); } public function testRejectsNegativePrefix(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('10.0.0.0/-1'); } public function testRejectsV4PrefixOver32(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('10.0.0.0/33'); } public function testRejectsV6PrefixOver128(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('2001:db8::/129'); } public function testRejectsNonNumericPrefix(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('10.0.0.0/abc'); } public function testRejectsMultipleSlashes(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('10.0.0.0/24/8'); } public function testRejectsLeadingWhitespace(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString(' 10.0.0.0/24'); } public function testRejectsEmptyPrefix(): void { $this->expectException(InvalidCidrException::class); Cidr::fromString('10.0.0.0/'); } public function testNonByteAlignedV4Prefix(): void { // /20 fixes the first 20 bits = 8 (byte 0) + 8 (byte 1) + 4 (high // nibble of byte 2). Exercises the partial-byte mask path. // 10.16.0.0/20 covers 10.16.0.0 — 10.16.15.255 (4096 addresses). $cidr = Cidr::fromString('10.16.0.0/20'); $this->assertTrue($cidr->contains(IpAddress::fromString('10.16.0.0'))); $this->assertTrue($cidr->contains(IpAddress::fromString('10.16.15.255'))); $this->assertTrue($cidr->contains(IpAddress::fromString('10.16.5.42'))); $this->assertFalse($cidr->contains(IpAddress::fromString('10.16.16.0'))); $this->assertFalse($cidr->contains(IpAddress::fromString('10.15.255.255'))); $this->assertSame('10.16.0.0/20', $cidr->text()); } public function testNonByteAlignedV6Prefix(): void { // /17 fixes 16 bits + 1 high bit of byte 2. 2001:8000::/17 covers // any address whose first 17 bits start with 0x2001 followed by a 1. $cidr = Cidr::fromString('2001:8000::/17'); $this->assertTrue($cidr->contains(IpAddress::fromString('2001:8000::'))); $this->assertTrue($cidr->contains(IpAddress::fromString('2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff'))); $this->assertFalse($cidr->contains(IpAddress::fromString('2001:7fff:ffff::'))); $this->assertFalse($cidr->contains(IpAddress::fromString('2002::'))); } public function testNonByteAlignedHostMaskingClearsBits(): void { // 10.16.5.7/20 should mask the host bits to 10.16.0.0/20 $cidr = Cidr::fromString('10.16.5.7/20'); $this->assertSame('10.16.0.0/20', $cidr->text()); } }