diff --git a/CHANGELOG.md b/CHANGELOG.md index caaa23f..d4630f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## `6.x` +- Split embedding strategies into canonical and non-canonical packers via the + new `Strategy\CanonicalEmbeddingInterface` bridge. - Add strict parsing methods to the new `Contracts\FactoryInterface` interface: try/from protocol, binary, hex. - Deprecate `factory()` in favour of the strict named constructors diff --git a/docs/05-strategies.md b/docs/05-strategies.md index 1bf0a85..f959159 100644 --- a/docs/05-strategies.md +++ b/docs/05-strategies.md @@ -24,7 +24,7 @@ Each embedding strategy implements the - Detect whether a version 4 address is embedded into a version 6 address, - Extracting a version 4 address from a version 6 address, and - Packing a version 4 address into a version 6 address according to the given - strategy. + strategy (see [Canonical and Non-Canonical Packing](#canonical-and-non-canonical-packing)). ## Specifying a Strategy @@ -45,6 +45,50 @@ IP::setDefaultEmbeddingStrategy(new Strategy\Compatible); $ip = IP::factory('127.0.0.1', new Strategy\Derived); ``` +## Canonical and Non-Canonical Packing + +Some embedding strategies (6to4-derived, Teredo, and NAT64 with a prefix shorter +than `/96`) carry bits *outside* the embedded version 4 address — a Teredo +address, for example, also carries the tunnel server's address, flags, and the +client's UDP port. The original `pack()` always produces the **canonical** form, +zeroing every such bit; this is the right behaviour when constructing an address +from a bare version 4 address, but it silently discards information when re-packing +an existing version 6 address. + +`Darsyn\IP\Strategy\CanonicalEmbeddingInterface` provides methods (and +deprecates `pack()`): +- `packIntoCanonical(string $ipv4): string` is identical to `pack()`: every bit + outside the embedded version 4 address is normalised/zeroed. +- `packIntoNonCanonical(string $ipv6, string $ipv4): string` replaces only the + embedded version 4 bit positions of `$ipv6`, preserving every other bit. A + `PackingException` is thrown if `$ipv6` is not recognised by the strategy. + +> `CanonicalEmbeddingInterface` is a _temporary scaffolding_ (a bridge interface) +> to maintain backwards compatibility for `EmbeddingStrategyInterface` on the +> `6.x` branch. Both interfaces will be combined back into `EmbeddingStrategyInterface` +> with `pack()` removed on the next major version bump. +> +> Type-hint `EmbeddingStrategyInterface` and use feature detection +> (`instanceof CanonicalEmbeddingInterface`) if you wish to use the new methods +> in custom strategies. + +```php +packIntoCanonical($ipv4)); // string("20010000000000000000000080fffffe") + +// Non-canonical packing embeds the new client but preserves the server, flags, +// and port carried by the original address. +bin2hex($strategy->packIntoNonCanonical($ipv6, $ipv4)); // string("200100004136e378800063bf80fffffe") +``` + ## NAT64 (RFC 6052) Unlike the other strategies, `Nat64` has no public constructor — it is instantiated via named constructors only: diff --git a/src/Strategy/CanonicalEmbeddingInterface.php b/src/Strategy/CanonicalEmbeddingInterface.php new file mode 100644 index 0000000..09692fa --- /dev/null +++ b/src/Strategy/CanonicalEmbeddingInterface.php @@ -0,0 +1,36 @@ +packIntoCanonical($binary); + } + + public function packIntoCanonical(string $ipv4): string + { + if (4 === MbString::getLength($ipv4)) { + return "\0\0\0\0\0\0\0\0\0\0\0\0" . $ipv4; + } + throw new StrategyException\PackingException($ipv4, $this); + } + + public function packIntoNonCanonical(string $ipv6, string $ipv4): string + { + if (!$this->isEmbedded($ipv6)) { + throw new StrategyException\PackingException($ipv6, $this); } - throw new StrategyException\PackingException($binary, $this); + // The prefix (bytes 0-11) + the IPv4 address (bytes 12-15) occupy the + // entire IPv6 address space; defer to canonical. + return $this->packIntoCanonical($ipv4); } } diff --git a/src/Strategy/Composite.php b/src/Strategy/Composite.php index 1cc56c7..bba4415 100644 --- a/src/Strategy/Composite.php +++ b/src/Strategy/Composite.php @@ -17,7 +17,7 @@ * several embedding schemes on input while always producing a single canonical * form on output. */ -class Composite implements EmbeddingStrategyInterface +class Composite implements CanonicalEmbeddingInterface { /** @var EmbeddingStrategyInterface $packer */ private $packer; @@ -54,8 +54,37 @@ public function extract(string $binary): string throw new StrategyException\ExtractionException($binary, $this); } + /** @deprecated Use packIntoCanonical() instead. */ public function pack(string $binary): string { - return $this->packer->pack($binary); + return $this->packIntoCanonical($binary); + } + + public function packIntoCanonical(string $ipv4): string + { + if ($this->packer instanceof CanonicalEmbeddingInterface) { + return $this->packer->packIntoCanonical($ipv4); + } + // Graceful degradation for a userland packer predating the bridge. + /** @phpstan-ignore method.deprecated */ + return $this->packer->pack($ipv4); + } + + /** + * Delegate to the first strategy (in constructor order) that recognises the + * supplied IPv6 address, mirroring extract(). + */ + public function packIntoNonCanonical(string $ipv6, string $ipv4): string + { + foreach ($this->strategies as $strategy) { + if ($strategy->isEmbedded($ipv6)) { + if ($strategy instanceof CanonicalEmbeddingInterface) { + return $strategy->packIntoNonCanonical($ipv6, $ipv4); + } + /** @phpstan-ignore method.deprecated */ + return $strategy->pack($ipv4); + } + } + throw new StrategyException\PackingException($ipv6, $this); } } diff --git a/src/Strategy/Derived.php b/src/Strategy/Derived.php index ef85198..8d25136 100644 --- a/src/Strategy/Derived.php +++ b/src/Strategy/Derived.php @@ -24,7 +24,7 @@ * N.B. Legacy, but not formally deprecated (only 6to4 anycast was deprecated * via RFC 7526). */ -class Derived implements EmbeddingStrategyInterface +class Derived implements CanonicalEmbeddingInterface { public function isEmbedded(string $binary): bool { @@ -46,15 +46,39 @@ public function extract(string $binary): string * in bits 16-47 of a 6to4 address. * The SLA ID and interface ID bits are lost so a direct pass-through * (extract-pack) reconstructs the canonical Derived address - * (`2002:XXXX:XXXX::`), not the original. + * (`2002:XXXX:XXXX::`), not the original. Use `packIntoNonCanonical()` to + * preserve the SLA ID and interface ID bits. + * + * @deprecated Use packIntoCanonical() instead. */ public function pack(string $binary): string { - if (4 === MbString::getLength($binary)) { + return $this->packIntoCanonical($binary); + } + + public function packIntoCanonical(string $ipv4): string + { + if (4 === MbString::getLength($ipv4)) { // Zero the SLA ID (subnet) and interface ID fields. $subnetInterface = "\0\0\0\0\0\0\0\0\0\0"; - return Binary::fromHex('2002') . $binary . $subnetInterface; + return Binary::fromHex('2002') . $ipv4 . $subnetInterface; + } + throw new StrategyException\PackingException($ipv4, $this); + } + + /** + * Replace only the embedded IPv4 address (bits 16-47); the 6to4 prefix + * (bits 0-15) and the SLA ID and interface ID fields (bits 48-127) of the + * supplied IPv6 address pass through unchanged. + */ + public function packIntoNonCanonical(string $ipv6, string $ipv4): string + { + if (!$this->isEmbedded($ipv6)) { + throw new StrategyException\PackingException($ipv6, $this); + } + if (4 !== MbString::getLength($ipv4)) { + throw new StrategyException\PackingException($ipv4, $this); } - throw new StrategyException\PackingException($binary, $this); + return MbString::subString($ipv6, 0, 2) . $ipv4 . MbString::subString($ipv6, 6, 10); } } diff --git a/src/Strategy/EmbeddingStrategyInterface.php b/src/Strategy/EmbeddingStrategyInterface.php index 4b697a1..d77efd6 100644 --- a/src/Strategy/EmbeddingStrategyInterface.php +++ b/src/Strategy/EmbeddingStrategyInterface.php @@ -24,6 +24,10 @@ public function extract(string $binary): string; * Convert the supplied IPv4 binary string into an embedded IPv6 binary * string, according to the implemented embedding strategy. * + * @deprecated Implement `CanonicalEmbeddingInterface` and use + * `packIntoCanonical()` (identical behaviour) or `packIntoNonCanonical()` + * instead. This method will be replaced by the two split packers in the + * next major version. * @throws \Darsyn\IP\Exception\Strategy\PackingException */ public function pack(string $binary): string; diff --git a/src/Strategy/Mapped.php b/src/Strategy/Mapped.php index ff0e56e..411aab6 100644 --- a/src/Strategy/Mapped.php +++ b/src/Strategy/Mapped.php @@ -14,7 +14,7 @@ * * Reserved by protocol, and not globally reachable (as an IPv6 address). */ -class Mapped implements EmbeddingStrategyInterface +class Mapped implements CanonicalEmbeddingInterface { public function isEmbedded(string $binary): bool { @@ -30,11 +30,27 @@ public function extract(string $binary): string throw new StrategyException\ExtractionException($binary, $this); } + /** @deprecated Use packIntoCanonical() instead. */ public function pack(string $binary): string { - if (4 === MbString::getLength($binary)) { - return Binary::fromHex('00000000000000000000ffff') . $binary; + return $this->packIntoCanonical($binary); + } + + public function packIntoCanonical(string $ipv4): string + { + if (4 === MbString::getLength($ipv4)) { + return Binary::fromHex('00000000000000000000ffff') . $ipv4; + } + throw new StrategyException\PackingException($ipv4, $this); + } + + public function packIntoNonCanonical(string $ipv6, string $ipv4): string + { + if (!$this->isEmbedded($ipv6)) { + throw new StrategyException\PackingException($ipv6, $this); } - throw new StrategyException\PackingException($binary, $this); + // The prefix (bytes 0-11) + the IPv4 address (bytes 12-15) occupy the + // entire IPv6 address space; defer to canonical. + return $this->packIntoCanonical($ipv4); } } diff --git a/src/Strategy/Nat64.php b/src/Strategy/Nat64.php index 91bcc2e..9e00393 100644 --- a/src/Strategy/Nat64.php +++ b/src/Strategy/Nat64.php @@ -38,7 +38,7 @@ * within the Well-Known Prefix; the restriction does not apply to * Network-Specific Prefixes. This library deliberately does not enforce it. */ -class Nat64 implements EmbeddingStrategyInterface +class Nat64 implements CanonicalEmbeddingInterface { /** Hex representation of the Well-Known Prefix `64:ff9b::/96` (RFC 6052 § 2.1). */ public const WELL_KNOWN_PREFIX = '0064ff9b000000000000000000000000'; @@ -165,29 +165,66 @@ public function extract(string $binary): string * address; the reserved octet (bits 64 to 71) and the suffix are * zero-filled, so a direct pass-through (extract-pack) of a non-canonical * address reconstructs the canonical form, not the original. + * + * @deprecated Use packIntoCanonical() instead. */ public function pack(string $binary): string + { + return $this->packIntoCanonical($binary); + } + + public function packIntoCanonical(string $ipv4): string { // Note: non-global IPv4 addresses should not be packed into the // Well-Known Prefix (the restriction does not apply to // Network-Specific Prefixes), but is not enforced here. It is down to // the user of this library to know when to use which embedding // strategy. - if (4 !== MbString::getLength($binary)) { - throw new StrategyException\PackingException($binary, $this); + if (4 !== MbString::getLength($ipv4)) { + throw new StrategyException\PackingException($ipv4, $this); } $bytes = \intdiv($this->length, 8); $prefix = MbString::subString($this->prefix, 0, $bytes); if (96 === $this->length) { - return $prefix . $binary; + return $prefix . $ipv4; } // The reserved octet (bits 64 to 71) must be zero (RFC 6052 § 2.2), // and the suffix should be zero; zero-fill both. $split = 8 - $bytes; $withoutSuffix = $prefix - . MbString::subString($binary, 0, $split) + . MbString::subString($ipv4, 0, $split) . "\0" - . MbString::subString($binary, $split); + . MbString::subString($ipv4, $split); return MbString::padString($withoutSuffix, 16, "\0"); } + + /** + * Replace only the embedded IPv4 address; the configured prefix and the + * suffix bits of the supplied IPv6 address pass through unchanged. The + * reserved octet (bits 64 to 71) is NOT preservation space: RFC 6052 § 2.2 + * mandates it be zero, so it stays zero regardless of the supplied address. + */ + public function packIntoNonCanonical(string $ipv6, string $ipv4): string + { + if (!$this->isEmbedded($ipv6)) { + throw new StrategyException\PackingException($ipv6, $this); + } + if (4 !== MbString::getLength($ipv4)) { + throw new StrategyException\PackingException($ipv4, $this); + } + $bytes = \intdiv($this->length, 8); + $prefix = MbString::subString($this->prefix, 0, $bytes); + if (96 === $this->length) { + // No reserved octet and no suffix at /96: identical to canonical. + return $prefix . $ipv4; + } + // The reserved octet (bits 64 to 71) stays zero; the suffix (the bits + // after the embedded address) is taken from the supplied IPv6 address. + $split = 8 - $bytes; + return $prefix + . MbString::subString($ipv4, 0, $split) + . "\0" + . MbString::subString($ipv4, $split) + . MbString::subString($ipv6, $bytes + 5); + } } diff --git a/src/Strategy/Teredo.php b/src/Strategy/Teredo.php index 733e654..10fc4fe 100644 --- a/src/Strategy/Teredo.php +++ b/src/Strategy/Teredo.php @@ -23,7 +23,7 @@ * treat every Teredo address as an embedded IPv4 address, even though only the * canonical form can round-trip through extraction. */ -class Teredo implements EmbeddingStrategyInterface +class Teredo implements CanonicalEmbeddingInterface { private const INVERSE_MASK = "\xff\xff\xff\xff"; @@ -49,15 +49,34 @@ public function extract(string $binary): string * The tunnel server's IPv4 address, the flags, and the obfuscated UDP port * are lost so a direct pass-through (extract-pack) reconstructs the * canonical Teredo address (`2001::XXXX:XXXX`), not the original. + * + * @deprecated Use packIntoCanonical() instead. */ public function pack(string $binary): string { - if (4 === MbString::getLength($binary)) { + return $this->packIntoCanonical($binary); + } + + public function packIntoCanonical(string $ipv4): string + { + if (4 === MbString::getLength($ipv4)) { // Zero the server/flags/port fields. $serverFlagsPort = Binary::fromHex('0000000000000000'); // Re-obfuscate the client IPv4 (XOR 0xFFFFFFFF). - return Binary::fromHex('20010000') . $serverFlagsPort . ($binary ^ self::INVERSE_MASK); + return Binary::fromHex('20010000') . $serverFlagsPort . ($ipv4 ^ self::INVERSE_MASK); + } + throw new StrategyException\PackingException($ipv4, $this); + } + + public function packIntoNonCanonical(string $ipv6, string $ipv4): string + { + if (!$this->isEmbedded($ipv6)) { + throw new StrategyException\PackingException($ipv6, $this); + } + if (4 !== MbString::getLength($ipv4)) { + throw new StrategyException\PackingException($ipv4, $this); } - throw new StrategyException\PackingException($binary, $this); + // Re-obfuscate the client IPv4 (XOR 0xFFFFFFFF with bits 96-127). + return MbString::subString($ipv6, 0, 12) . ($ipv4 ^ self::INVERSE_MASK); } } diff --git a/src/Version/Multi.php b/src/Version/Multi.php index b28ab30..4979eeb 100644 --- a/src/Version/Multi.php +++ b/src/Version/Multi.php @@ -6,6 +6,7 @@ use Darsyn\IP\Exception; use Darsyn\IP\IpInterface; +use Darsyn\IP\Strategy\CanonicalEmbeddingInterface; use Darsyn\IP\Strategy\EmbeddingStrategyInterface; use Darsyn\IP\Strategy\Mapped as MappedEmbeddingStrategy; use Darsyn\IP\Util\Binary; @@ -54,6 +55,16 @@ private static function getDefaultEmbeddingStrategy(): EmbeddingStrategyInterfac return self::$defaultEmbeddingStrategy ?: new MappedEmbeddingStrategy(); } + /** Graceful degradation for a user-defined embedding strategy that doesn't implement the CanonicalEmbeddingInterface. */ + private static function packIntoCanonical(EmbeddingStrategyInterface $strategy, string $binary): string + { + if ($strategy instanceof CanonicalEmbeddingInterface) { + return $strategy->packIntoCanonical($binary); + } + /** @phpstan-ignore method.deprecated */ + return $strategy->pack($binary); + } + /** @deprecated Use fromProtocol() or fromBinary() instead. */ public static function factory(string $ip, ?EmbeddingStrategyInterface $strategy = null): self { @@ -67,7 +78,7 @@ public static function factory(string $ip, ?EmbeddingStrategyInterface $strategy // If the IP address is a binary sequence of 4 bytes, then pack it into // a 16 byte IPv6 binary sequence according to the embedding strategy. if (4 === MbString::getLength($binary)) { - $binary = $strategy->pack($binary); + $binary = self::packIntoCanonical($strategy, $binary); } } catch (Exception\IpException $e) { throw new Exception\InvalidIpAddressException($ip, $e); @@ -89,7 +100,7 @@ public static function fromProtocol(string $ip, ?EmbeddingStrategyInterface $str } $length = MbString::getLength($binary); if (4 === $length) { - $binary = $strategy->pack($binary); + $binary = self::packIntoCanonical($strategy, $binary); } elseif (16 !== $length) { throw new Exception\InvalidBinaryException($binary); } @@ -110,7 +121,7 @@ public static function fromBinary(string $binary, ?EmbeddingStrategyInterface $s $strategy = $strategy ?: self::getDefaultEmbeddingStrategy(); $length = MbString::getLength($binary); if (4 === $length) { - $binary = $strategy->pack($binary); + $binary = self::packIntoCanonical($strategy, $binary); } elseif (16 !== $length) { throw new Exception\InvalidBinaryException($binary); } @@ -199,7 +210,7 @@ public function getNetworkIp(int $cidr): self if ($this->isVersion4WithAppropriateCidr($cidr)) { $v4 = (new IPv4($this->getShortBinary()))->getNetworkIp($cidr)->getBinary(); return new static( - $this->embeddingStrategy->pack($v4), + self::packIntoCanonical($this->embeddingStrategy, $v4), clone $this->embeddingStrategy ); } @@ -214,7 +225,7 @@ public function getBroadcastIp(int $cidr): self if ($this->isVersion4WithAppropriateCidr($cidr)) { $v4 = (new IPv4($this->getShortBinary()))->getBroadcastIp($cidr)->getBinary(); return new static( - $this->embeddingStrategy->pack($v4), + self::packIntoCanonical($this->embeddingStrategy, $v4), clone $this->embeddingStrategy ); } diff --git a/tests/DataProvider/Strategy/Composite.php b/tests/DataProvider/Strategy/Composite.php index 926ab65..9dd0efb 100644 --- a/tests/DataProvider/Strategy/Composite.php +++ b/tests/DataProvider/Strategy/Composite.php @@ -69,4 +69,22 @@ public static function getPackableSequences() { return Mapped::getValidSequences(); } + + /** + * Non-canonical addresses recognised by a sub-strategy of the composite under + * test (6to4 with interface ID set, and Teredo with server/flags/port set); + * packIntoNonCanonical() must delegate to the recognising sub-strategy and + * preserve the non-embedded bits. + * + * @return list + */ + public static function getNonCanonicalDelegationSequences() + { + return [ + // [ non-canonical IPv6 binary, embedded IPv4 binary ]. Re-embedding the + // address's own extracted IPv4 must reproduce the original exactly. + [\pack('H*', '2002c00002010000dead00000000beef'), \pack('H*', 'c0000201')], + [\pack('H*', '200100004136e378800063bf3ffffdd2'), \pack('H*', 'c000022d')], + ]; + } } diff --git a/tests/Strategy/CompatibleTest.php b/tests/Strategy/CompatibleTest.php index 35190e0..48c5cb3 100644 --- a/tests/Strategy/CompatibleTest.php +++ b/tests/Strategy/CompatibleTest.php @@ -13,7 +13,7 @@ class CompatibleTest extends TestCase { - /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy */ + /** @var \Darsyn\IP\Strategy\CanonicalEmbeddingInterface $strategy */ private $strategy; /** @before */ @@ -76,6 +76,7 @@ public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $i } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Compatible::getInvalidIpAddresses() */ @@ -95,6 +96,7 @@ public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $va } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Compatible::getValidSequences() */ @@ -104,4 +106,50 @@ public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv6 { $this->assertSame($ipv6, $this->strategy->pack($ipv4)); } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Compatible::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompatibleDataProvider::class, 'getInvalidIpAddresses')] + public function testPackIntoCanonicalThrowsForStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoCanonical($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Compatible::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompatibleDataProvider::class, 'getValidSequences')] + public function testPackIntoCanonicalProducesCanonicalForm(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->packIntoCanonical($ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Compatible::getInvalidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompatibleDataProvider::class, 'getInvalidSequences')] + public function testPackIntoNonCanonicalThrowsForUnrecognisedIpv6(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoNonCanonical($value, \pack('H*', '7f000001')); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Compatible::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompatibleDataProvider::class, 'getValidSequences')] + public function testPackIntoNonCanonicalEqualsCanonical(string $ipv6, string $ipv4): void + { + $this->assertSame($this->strategy->packIntoCanonical($ipv4), $this->strategy->packIntoNonCanonical($ipv6, $ipv4)); + } } diff --git a/tests/Strategy/CompositeTest.php b/tests/Strategy/CompositeTest.php index 0aac5b8..8fbfd54 100644 --- a/tests/Strategy/CompositeTest.php +++ b/tests/Strategy/CompositeTest.php @@ -15,7 +15,7 @@ class CompositeTest extends TestCase { - /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy */ + /** @var \Darsyn\IP\Strategy\CanonicalEmbeddingInterface $strategy */ private $strategy; /** @before */ @@ -71,6 +71,7 @@ public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $i } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getInvalidIpAddresses() */ @@ -83,6 +84,7 @@ public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $va } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getPackableSequences() */ @@ -92,4 +94,54 @@ public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv6 { $this->assertSame($ipv6, $this->strategy->pack($ipv4)); } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getInvalidIpAddresses')] + public function testPackIntoCanonicalThrowsForStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoCanonical($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getPackableSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getPackableSequences')] + public function testPackIntoCanonicalProducesCanonicalForm(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->packIntoCanonical($ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getInvalidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getInvalidSequences')] + public function testPackIntoNonCanonicalThrowsForUnrecognisedIpv6(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoNonCanonical($value, \pack('H*', '7f000001')); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getNonCanonicalDelegationSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getNonCanonicalDelegationSequences')] + public function testPackIntoNonCanonicalDelegatesToRecognisingStrategy(string $ipv6, string $ipv4): void + { + // Re-embedding the address's own IPv4 reproduces the original (the recognising + // sub-strategy preserves the non-embedded bits). + $this->assertSame($ipv6, $this->strategy->packIntoNonCanonical($ipv6, $ipv4)); + $newV4 = \pack('H*', '08080808'); + $this->assertSame($newV4, $this->strategy->extract($this->strategy->packIntoNonCanonical($ipv6, $newV4))); + } } diff --git a/tests/Strategy/DerivedTest.php b/tests/Strategy/DerivedTest.php index 62522f4..063d760 100644 --- a/tests/Strategy/DerivedTest.php +++ b/tests/Strategy/DerivedTest.php @@ -11,7 +11,7 @@ class DerivedTest extends TestCase { - /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy */ + /** @var \Darsyn\IP\Strategy\CanonicalEmbeddingInterface $strategy */ private $strategy; /** @before */ @@ -67,6 +67,7 @@ public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $i } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getInvalidIpAddresses() */ @@ -79,6 +80,7 @@ public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $va } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getValidSequences() */ @@ -120,6 +122,86 @@ public function testCorrectSequenceExtractedFromNonCanonical6to4(string $ipv6, s public function testNonCanonical6to4CanonicalisedByExtractThenPack(string $ipv6, string $ipv4, string $canonical): void { $this->assertNotSame($canonical, $ipv6); - $this->assertSame($canonical, $this->strategy->pack($this->strategy->extract($ipv6))); + $this->assertSame($canonical, $this->strategy->packIntoCanonical($this->strategy->extract($ipv6))); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getInvalidIpAddresses')] + public function testPackIntoCanonicalThrowsForStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoCanonical($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getValidSequences')] + public function testPackIntoCanonicalProducesCanonicalForm(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->packIntoCanonical($ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getInvalidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getInvalidSequences')] + public function testPackIntoNonCanonicalThrowsForUnrecognisedIpv6(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoNonCanonical($value, \pack('H*', '7f000001')); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getNonCanonicalSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getNonCanonicalSequences')] + public function testPackIntoNonCanonicalReportsInvalidIpv4(string $ipv6, string $ipv4, string $canonical): void + { + $invalid = \pack('H*', '7f0000'); + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + try { + $this->strategy->packIntoNonCanonical($ipv6, $invalid); + } catch (\Darsyn\IP\Exception\Strategy\PackingException $e) { + $this->assertSame($this->strategy, $e->getEmbeddingStrategy()); + $this->assertSame($invalid, $e->getSuppliedBinary()); + throw $e; + } + $this->fail(); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getNonCanonicalSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getNonCanonicalSequences')] + public function testPackIntoNonCanonicalPreservesNonEmbeddedBits(string $ipv6, string $ipv4, string $canonical): void + { + $this->assertSame($ipv6, $this->strategy->packIntoNonCanonical($ipv6, $ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getNonCanonicalSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getNonCanonicalSequences')] + public function testPackIntoNonCanonicalEmbedsNewAddress(string $ipv6, string $ipv4, string $canonical): void + { + $newV4 = \pack('H*', '08080808'); + $result = $this->strategy->packIntoNonCanonical($ipv6, $newV4); + $this->assertSame($newV4, $this->strategy->extract($result)); + $this->assertSame(\substr($ipv6, 6, 10), \substr($result, 6, 10)); } } diff --git a/tests/Strategy/MappedTest.php b/tests/Strategy/MappedTest.php index 1b50a59..c3835cb 100644 --- a/tests/Strategy/MappedTest.php +++ b/tests/Strategy/MappedTest.php @@ -11,7 +11,7 @@ class MappedTest extends TestCase { - /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy */ + /** @var \Darsyn\IP\Strategy\CanonicalEmbeddingInterface $strategy */ private $strategy; /** @before */ @@ -67,6 +67,7 @@ public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $i } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Mapped::getInvalidIpAddresses() */ @@ -79,6 +80,7 @@ public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $va } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Mapped::getValidSequences() */ @@ -88,4 +90,50 @@ public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv6 { $this->assertSame($ipv6, $this->strategy->pack($ipv4)); } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Mapped::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(MappedDataProvider::class, 'getInvalidIpAddresses')] + public function testPackIntoCanonicalThrowsForStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoCanonical($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Mapped::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(MappedDataProvider::class, 'getValidSequences')] + public function testPackIntoCanonicalProducesCanonicalForm(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->packIntoCanonical($ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Mapped::getInvalidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(MappedDataProvider::class, 'getInvalidSequences')] + public function testPackIntoNonCanonicalThrowsForUnrecognisedIpv6(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoNonCanonical($value, \pack('H*', '7f000001')); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Mapped::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(MappedDataProvider::class, 'getValidSequences')] + public function testPackIntoNonCanonicalEqualsCanonical(string $ipv6, string $ipv4): void + { + $this->assertSame($this->strategy->packIntoCanonical($ipv4), $this->strategy->packIntoNonCanonical($ipv6, $ipv4)); + } } diff --git a/tests/Strategy/Nat64Test.php b/tests/Strategy/Nat64Test.php index 559f3ae..b5a85ba 100644 --- a/tests/Strategy/Nat64Test.php +++ b/tests/Strategy/Nat64Test.php @@ -12,7 +12,7 @@ class Nat64Test extends TestCase { - /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy */ + /** @var \Darsyn\IP\Strategy\CanonicalEmbeddingInterface $strategy */ private $strategy; /** @before */ @@ -68,6 +68,7 @@ public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $i } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getInvalidIpAddresses() */ @@ -80,6 +81,7 @@ public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $va } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getValidSequences() */ @@ -101,7 +103,7 @@ public function testSequencesEmbedExtractAndPackWithNetworkSpecificPrefixes(stri $strategy = Nat64::networkSpecific(IPv6::fromProtocol($prefixAddress), $length); $this->assertTrue($strategy->isEmbedded($ipv6)); $this->assertSame($ipv4, $strategy->extract($ipv6)); - $this->assertSame($ipv6, $strategy->pack($ipv4)); + $this->assertSame($ipv6, $strategy->packIntoCanonical($ipv4)); } /** @@ -126,7 +128,7 @@ public function testNonCanonicalSequencesRoundTripToCanonicalFormWithNetworkSpec $strategy = Nat64::networkSpecific(IPv6::fromProtocol($prefixAddress), $length); $this->assertTrue($strategy->isEmbedded($nonCanonical)); $this->assertSame($ipv4, $strategy->extract($nonCanonical)); - $this->assertSame($canonical, $strategy->pack($strategy->extract($nonCanonical))); + $this->assertSame($canonical, $strategy->packIntoCanonical($strategy->extract($nonCanonical))); } /** @@ -177,8 +179,8 @@ public function testWellKnownNamedConstructor(): void $this->assertSame(96, $wellKnown->getPrefixLength()); $this->assertSame('64:ff9b::', IPv6::fromBinary($wellKnown->getPrefix())->getCompactedAddress()); $this->assertSame( - Nat64::networkSpecific(IPv6::fromProtocol('64:ff9b::'), 96)->pack(\pack('H*', 'c0000221')), - $wellKnown->pack(\pack('H*', 'c0000221')) + Nat64::networkSpecific(IPv6::fromProtocol('64:ff9b::'), 96)->packIntoCanonical(\pack('H*', 'c0000221')), + $wellKnown->packIntoCanonical(\pack('H*', 'c0000221')) ); } @@ -203,7 +205,7 @@ public function testSequencesEmbedExtractAndPackWithLocalUsePrefix(string $ipv6, $strategy = Nat64::localUse(); $this->assertTrue($strategy->isEmbedded($ipv6)); $this->assertSame($ipv4, $strategy->extract($ipv6)); - $this->assertSame($ipv6, $strategy->pack($ipv4)); + $this->assertSame($ipv6, $strategy->packIntoCanonical($ipv4)); } /** @@ -228,6 +230,101 @@ public function testNonCanonicalSequencesRoundTripToCanonicalFormWithLocalUsePre $strategy = Nat64::localUse(); $this->assertTrue($strategy->isEmbedded($nonCanonical)); $this->assertSame($ipv4, $strategy->extract($nonCanonical)); - $this->assertSame($canonical, $strategy->pack($strategy->extract($nonCanonical))); + $this->assertSame($canonical, $strategy->packIntoCanonical($strategy->extract($nonCanonical))); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getInvalidIpAddresses')] + public function testPackIntoCanonicalThrowsForStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoCanonical($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getValidSequences')] + public function testPackIntoCanonicalProducesCanonicalForm(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->packIntoCanonical($ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getInvalidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getInvalidSequences')] + public function testPackIntoNonCanonicalThrowsForUnrecognisedIpv6(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoNonCanonical($value, \pack('H*', '7f000001')); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getValidSequences')] + public function testPackIntoNonCanonicalReportsInvalidIpv4(string $ipv6, string $ipv4): void + { + $invalid = \pack('H*', '7f0000'); + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + try { + $this->strategy->packIntoNonCanonical($ipv6, $invalid); + } catch (\Darsyn\IP\Exception\Strategy\PackingException $e) { + $this->assertSame($this->strategy, $e->getEmbeddingStrategy()); + $this->assertSame($invalid, $e->getSuppliedBinary()); + throw $e; + } + $this->fail(); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getNonCanonicalNetworkSpecificSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getNonCanonicalNetworkSpecificSequences')] + public function testPackIntoNonCanonicalPreservesSuffixAndZeroesReservedOctetWithNetworkSpecificPrefixes(string $prefixAddress, int $length, string $nonCanonical, string $ipv4, string $canonical): void + { + $strategy = Nat64::networkSpecific(IPv6::fromProtocol($prefixAddress), $length); + $result = $strategy->packIntoNonCanonical($nonCanonical, $ipv4); + // The embedded IPv4 round-trips. + $this->assertSame($ipv4, $strategy->extract($result)); + $bytes = \intdiv($length, 8); + // The configured prefix is preserved. + $this->assertSame(\substr($nonCanonical, 0, $bytes), \substr($result, 0, $bytes)); + if (96 !== $length) { + // The reserved octet (byte 8) is forced to zero. + $this->assertSame("\0", \substr($result, 8, 1)); + // The suffix (bytes after the embedded address) is preserved from the source. + $this->assertSame(\substr($nonCanonical, $bytes + 5), \substr($result, $bytes + 5)); + } + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getNonCanonicalLocalUseSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getNonCanonicalLocalUseSequences')] + public function testPackIntoNonCanonicalPreservesSuffixAndZeroesReservedOctetWithLocalUsePrefix(string $nonCanonical, string $ipv4, string $canonical): void + { + $strategy = Nat64::localUse(); + $result = $strategy->packIntoNonCanonical($nonCanonical, $ipv4); + $this->assertSame($ipv4, $strategy->extract($result)); + // Local-use prefix is /48 → 6 bytes, reserved octet at byte 8, suffix from byte 11. + $this->assertSame(\substr($nonCanonical, 0, 6), \substr($result, 0, 6)); + $this->assertSame("\0", \substr($result, 8, 1)); + $this->assertSame(\substr($nonCanonical, 11), \substr($result, 11)); } } diff --git a/tests/Strategy/TeredoTest.php b/tests/Strategy/TeredoTest.php index be34190..29a2ddb 100644 --- a/tests/Strategy/TeredoTest.php +++ b/tests/Strategy/TeredoTest.php @@ -11,7 +11,7 @@ class TeredoTest extends TestCase { - /** @var \Darsyn\IP\Strategy\EmbeddingStrategyInterface $strategy */ + /** @var \Darsyn\IP\Strategy\CanonicalEmbeddingInterface $strategy */ private $strategy; /** @before */ @@ -67,6 +67,7 @@ public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $i } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getInvalidIpAddresses() */ @@ -79,6 +80,7 @@ public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $va } /** + * @deprecated Covers the deprecated pack(); see `testPackIntoCanonical*` methods. * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidPackSequences() */ @@ -88,4 +90,84 @@ public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv4 { $this->assertSame($ipv6, $this->strategy->pack($ipv4)); } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getInvalidIpAddresses')] + public function testPackIntoCanonicalThrowsForStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoCanonical($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidPackSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidPackSequences')] + public function testPackIntoCanonicalProducesCanonicalForm(string $ipv4, string $ipv6): void + { + $this->assertSame($ipv6, $this->strategy->packIntoCanonical($ipv4)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getInvalidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getInvalidSequences')] + public function testPackIntoNonCanonicalThrowsForUnrecognisedIpv6(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->packIntoNonCanonical($value, \pack('H*', '7f000001')); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidSequences')] + public function testPackIntoNonCanonicalReportsInvalidIpv4(string $ipv6, string $ipv4): void + { + $invalid = \pack('H*', '7f0000'); + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + try { + $this->strategy->packIntoNonCanonical($ipv6, $invalid); + } catch (\Darsyn\IP\Exception\Strategy\PackingException $e) { + $this->assertSame($this->strategy, $e->getEmbeddingStrategy()); + $this->assertSame($invalid, $e->getSuppliedBinary()); + throw $e; + } + $this->fail(); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidSequences')] + public function testPackIntoNonCanonicalPreservesNonEmbeddedBits(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->packIntoNonCanonical($ipv6, $this->strategy->extract($ipv6))); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidSequences')] + public function testPackIntoNonCanonicalEmbedsNewAddress(string $ipv6, string $ipv4): void + { + $newV4 = \pack('H*', '08080808'); + $result = $this->strategy->packIntoNonCanonical($ipv6, $newV4); + $this->assertSame($newV4, $this->strategy->extract($result)); + $this->assertSame(\substr($ipv6, 0, 12), \substr($result, 0, 12)); + } }