diff --git a/CHANGELOG.md b/CHANGELOG.md index eb24b37..9217b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,84 +1,88 @@ # Darsyn IP -## `6.x` - -- Rename the whole-value output methods to `to*` (conversions), keeping `get*` - for component and property accessors. Keep the `get*` spellings as deprecated - aliases; `getBinary()` is exempt as the accessor of the canonical state. -- Detect embedding strategies via the new `Contracts\StrategyDetectionInterface` - deprecating the old `IpInterface::isEmbedded()` and associated methods. -- Convert IP addresses to and from integers: `fromInteger()`/`toInteger()` via - the new `Contracts\Factory4Interface` (IPv4 and Multi only), plus - arbitrary-precision `fromIntegerString()`/`toIntegerString()` and fixed-width - `toHexString()` on all classes. -- Add arbitrary-precision base-256 <-> base-10 conversion to `Util\Binary`: - `toDecimalString()` and `fromDecimalString()` (GMP fast path when the - extension is loaded, backed by pure-PHP fallback). -- Extend `Contracts\OutputInterface` from `\JsonSerializable`. -- Expand `Contracts\OutputInterface`: `getOctets()`, `getSegments()`, and - canonical `toString()` (deferable method for Stringable equivalent). -- Add address arithmetic methods `next()`, `previous()` and `offset(int $offset)` - to `Contracts\ArithmeticInterface`. Over/underflowing the address space throws - `Exception\OverflowException`. -- 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 - `fromProtocol()`/`fromBinary()`. -- Introduce `@experimental` capability interfaces under `Darsyn\IP\Contracts\`. - Their shape may change before `7.0`, but remains backwards compatible for `6.x` -- Allow overriding the global formatter per call via an optional - `Formatter\ProtocolFormatterInterface` parameter on `toDotAddress()`, - `toCompactedAddress()` and `toProtocolAppropriateAddress()` (deprecated - `get*` aliases accept the same argument, but any other value triggers a - deprecation notice and falls back to the global formatter instead of - throwing a `TypeError`). -- Move CIDR mask generation from the protected `AbstractIP::generateBinaryMask()` - to the public static `Util\Binary::mask()`. -- Performance: compute network masks (`getNetworkIp()` / `getBroadcastIp()`) - and the greatest common CIDR (`getCommonCidr()`) directly on raw bytes. -- Add arithmetic primitives to `Util\Binary`: `increment()`, `decrement()` and - `addIntegerOffset()`. Over/underflowing throws new exception. -- Widen the `$previous` constructor argument on all exception classes from - `?\Exception` to `?\Throwable`. -- Add support for PHP `8.5` in README, and GitHub Action CI workflows. -- Access protected method via Closure in unit tests, instead of reflection, to - fix deprecation warnings in PHP `8.5`. -- Define all data-provider arguments as test method arguments, to fix - deprecation warnings in PHPUnit (cherry-picked from `5.0.2`). -- Drop all PHP versions except floor (`7.4`) and latest (`8.5`) for running - static analysis. -- Type-narrow out-of-range CIDRs to remove unnecessary `@phpstan-ignore` - instructions. -- Bugfix: don't compress single 16-bit zero group (according to RFC 5952 § `4.2.2`). -- Bugfix: stop `MbString::subString()` from swallowing valid `0`. -- Add Bash script for testing GitHub Action workflows locally using Docker. -- Add code style ruleset definition via PHP-CS-Fixer configuration. Apply the - code style to the entire codebase, and enable checking as another CI job in - GitHub Actions. -- Bugfix: classify full `fc00::/7` block as private use. -- Add (or update) community standards: `SECURITY.md`, `CODE_OF_CONDUCT.md` and - `CONTRIBUTING.md` -- Bugfix: detect embedded IPv4 across the entire 6to4 block (`2002::/16`, RFC - 3056 § 2), not just canonical 6to4 addresses with a zeroed 80-bit tail. -- Bugfix: align `isPublicUse()`, `isUnicastGlobal()` and `isDocumentation()` - with the IANA special-purpose address registries, and update RFC - references/citations for all address classification methods. -- Rename `isPublicUse()` to `isGloballyReachable()`, conforming to the official - wording used in the IANA special-purpose address registries ("Public Use" - does not appear in them). Keep `isPublicUse()` as a deprecated alias. -- Add NAT64 (RFC 6052 § 2.2) embedding strategy, with named constructors for - the Well-known Prefix (`64:ff9b::/96`), operator Network-specific Prefixes, - and the RFC 8215 Local-use prefix (`64:ff9b:1::/48`). -- Add `Teredo` embedding strategy for `2001::/32` (according to RFC 4380 § 4). -- Add `Composite` embedding strategy that recognises and extracts version 4 - addresses across several underlying strategies, while packing through a single - canonical strategy. -- Bugfix: classify a NAT64 Well-known Prefix (`64:ff9b::/96`, RFC 6052 § 2.1) - address by the version 4 address it embeds rather than trusting the prefix, so - an embedded non-globally-reachable address is no longer reported as globally - reachable (closes a potential SSRF deny-list bypass). +## `6.1.0` + +> Adds strict parsing, integer conversion, address arithmetic and three new embedding strategies. +> +> The public API is now split into capability interfaces under `Darsyn\IP\Contracts\`. These interfaces are +> `@experimental` (their shape may change before `7.0`, but stay backwards compatible for `6.x`). +> +> Every deprecation in this release is a docblock `@deprecated` only, so use a static analyzer. + +### Parsing + +- Add strict named constructors on the new `Contracts\FactoryInterface` (`fromProtocol()`, `fromBinary()` and + `fromHex()`). Each one has a `tryFrom*` twin that returns `null` instead of throwing an exception. +- `isValid()` reports whether a string parses. +- Add `fromInteger()` and `tryFromInteger()` on the new `Contracts\Factory4Interface` (IPv4 and Multi). +- Arbitrary-precision `fromIntegerString()` and `tryFromIntegerString()` on all classes. +- Fix: `Multi::fromProtocol()` now rejects a binary string of the wrong length instead of passing it through. + +### Output + +- Add `toString()` as the canonical string form. `__toString()` now defers to it. +- `Contracts\OutputInterface` extends `\JsonSerializable` so an address encodes as canonical string. +- Add `getOctets()` on all classes and `getSegments()` on IPv6 and Multi. +- Add `toInteger()` (IPv4 and Multi), `toIntegerString()` and fixed-width `toHexString()` on all classes. +- Rename the whole-value output methods with prefix `to*`. `get*` stays for component and property accessors, and + `getBinary()` (accessor of the internal state). +- Accept an optional `Formatter\ProtocolFormatterInterface` argument on `toDotAddress()`, `toCompactedAddress()` and + `toProtocolAppropriateAddress()` to override the global formatter. + +### Arithmetic + +- Add `next()`, `previous()` and `offset(int $offset)` to `Contracts\ArithmeticInterface`. +- Performance: `getNetworkIp()`, `getBroadcastIp()` and `getCommonCidr()` now work on raw bytes. + +### Embedding strategies + +- Add the `Strategy\Nat64` strategy (RFC 6052 § 2.2) with named constructors for the Well-Known Prefix `64:ff9b::/96`, + operator Network-Specific Prefixes, and the RFC 8215 Local-Use prefix `64:ff9b:1::/48`. +- Add the `Strategy\Teredo` strategy for `2001::/32` (RFC 4380 § 4). +- Add the `Strategy\Composite` strategy. Detects and extracts an embedded address through several strategies, and packs + through one canonical strategy. +- Add `Contracts\StrategyDetectionInterface` on IPv6 and Multi: `isEmbeddedAccordingToStrategy()`, `isNat64WellKnown()`, + `isNat64LocalUse()`, `isTeredo()` and `getEmbeddedIp()` (the inverse of `IPv6::fromEmbedded()`). +- Split packing into canonical and non-canonical forms through the new `Strategy\CanonicalEmbeddingInterface` bridge. + +### Classification + +- Rename `isPublicUse()` to `isGloballyReachable()`, the term used by IANA special-purpose address registries. +- Bugfix: align `isGloballyReachable()`, `isUnicastGlobal()` and `isDocumentation()` with the IANA special-purpose + address registries, and update the RFC citations on all classification methods. +- Bugfix: classify a NAT64 Well-Known Prefix address by the IPv4 address it embeds, not by its prefix. An embedded + non-reachable address is no longer reported as globally reachable (closes an SSRF deny-list bypass issue). +- Bugfix: classify the whole `fc00::/7` block as private use. +- Bugfix: detect an embedded IPv4 address across the whole 6to4 block `2002::/16` (RFC 3056 § 2), not only addresses + with a zeroed 80-bit tail. +- Bugfix: never compress a single 16-bit zero group when formatting an IPv6 address (RFC 5952 § 4.2.2). + +### Deprecated + +- `factory()` (use `fromProtocol()` or `fromBinary()`). +- `isPublicUse()` (use `isGloballyReachable()`). +- `getDotAddress()`, `getCompactedAddress()`, `getExpandedAddress()` and `getProtocolAppropriateAddress()`. + Use the `to*` spelling. +- `IpInterface::isEmbedded()`, `isMapped()`, `isDerived()` and `isCompatible()` (use + `Contracts\StrategyDetectionInterface`). +- `EmbeddingStrategyInterface::pack()`. Implement `Strategy\CanonicalEmbeddingInterface` and use `packIntoCanonical()`. + +### Utilities + +- Add byte-string arithmetic to `Util\Binary` (`increment()`, `decrement()` and `addIntegerOffset()`). +- Add base-256 to base-10 conversion to `Util\Binary` (`toDecimalString()` and `fromDecimalString()`). + Use GMP when the extension is loaded. +- Add `Util\Binary::mask()` and remove the protected `AbstractIP::generateBinaryMask()`. +- Add `Util\MbString::split()` (byte-safe `str_split()`). +- Bugfix: `MbString::subString()` no longer swallows a valid `"0"`. +- Widen the `$previous` argument on all exception classes from `?\Exception` to `?\Throwable`. + +#### Project + +- Add PHP `8.5` to the supported versions and the CI matrix. +- Add a PHP-CS-Fixer ruleset. +- Add test script that uses Docker to mirror the CI workflows. +- Add `SECURITY.md`, `CODE_OF_CONDUCT.md` and `CONTRIBUTING.md`. ## `6.0.0` diff --git a/docs/02-installation.md b/docs/02-installation.md index 689b4ce..8574dc2 100644 --- a/docs/02-installation.md +++ b/docs/02-installation.md @@ -2,8 +2,9 @@ ## System Requirements -- All functionality can be implemented without using new language features, so - this library will support PHP versions `5.6` onwards. +- This library requires PHP `7.1` or above and the `ctype` extension. The + minimum supported PHP version is only raised when a newer version of PHP + needs syntax that older versions cannot parse, never for modernisation. - This library cannot be used on 32-bits systems due to a dependency on the in-built PHP functions `inet_pton` and `inet_ntop`. diff --git a/docs/10-api.md b/docs/10-api.md index 9e66874..6b63160 100644 --- a/docs/10-api.md +++ b/docs/10-api.md @@ -1,65 +1,67 @@ # API Reference -| Contract | Method | Returns | IPv4 | IPv6 | Multi | -|------------------------------|---------------------------------------------------|--------------------------------|------|------|-------| -| `IpInterface` | `factory(string $ip, [$strategy])` _(deprecated)_ | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `isPublicUse()` _(deprecated)_ | `bool` | ✓ | ✓ | ✓ | -| `FactoryInterface` | `fromProtocol(string $ip, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `tryFromProtocol(string $ip, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | -| ↳ | `fromBinary(string $binary, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `tryFromBinary(string $binary, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | -| ↳ | `fromHex(string $hex, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `tryFromHex(string $hex, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | -| ↳ | `fromIntegerString(string $integer, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `isValid(string $ip, [$strategy])` | `bool` | ✓ | ✓ | ✓ | -| `Factory4Interface` | `fromInteger(int $integer, [$strategy])` | Static `IpInterface` | ✓ | | ✓ | -| `OutputInterface` | `getBinary()` | `string` | ✓ | ✓ | ✓ | -| ↳ | `getOctets()` | `list` | ✓ | ✓ | ✓ | -| ↳ | `toIntegerString()` | `string` | ✓ | ✓ | ✓ | -| ↳ | `toHexString()` | `string` | ✓ | ✓ | ✓ | -| ↳ | `toString()` | `string` | ✓ | ✓ | ✓ | -| ↳ | `__toString()` | `string` | ✓ | ✓ | ✓ | -| ↳ (`JsonSerializable`) | `jsonSerialize()` | `string` | ✓ | ✓ | ✓ | -| `Output4Interface` | `toDotAddress([$strategy])` | `string` | ✓ | | ✓ | -| ↳ | `toInteger()` | `int` | ✓ | | ✓ | -| `Output6Interface` | `toCompactedAddress([$strategy])` | `string` | | ✓ | ✓ | -| ↳ | `toExpandedAddress()` | `string` | | ✓ | ✓ | -| ↳ | `getSegments()` | `list` | | ✓ | ✓ | -| `VersionIdentityInterface` | `getVersion()` | `int` | ✓ | ✓ | ✓ | -| ↳ | `isVersion(int $version)` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isVersion4()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isVersion6()` | `bool` | ✓ | ✓ | ✓ | -| `ComparisonInterface` | `inRange(IpInterface $ip, int $cidr)` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `getCommonCidr(IpInterface $ip)` | `int` | ✓ | ✓ | ✓ | -| ↳ | `equals(IpInterface $ip)` | `bool` | ✓ | ✓ | ✓ | -| `StrategyDetectionInterface` | `isEmbeddedAccordingToStrategy($strategy)` | `bool` | | ✓ | ✓ | -| ↳ | `getEmbeddedIp([$strategy])` | `IPv4` | | ✓ | ✓ | -| ↳ | `isMapped()` | `bool` | | ✓ | ✓ | -| ↳ | `isDerived()` | `bool` | | ✓ | ✓ | -| ↳ | `isCompatible()` | `bool` | | ✓ | ✓ | -| ↳ | `isNat64WellKnown()` | `bool` | | ✓ | ✓ | -| ↳ | `isNat64LocalUse()` | `bool` | | ✓ | ✓ | -| ↳ | `isTeredo()` | `bool` | | ✓ | ✓ | -| `ClassificationInterface` | `isLinkLocal()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isLoopback()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isMulticast()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isPrivateUse()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isUnspecified()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isBenchmarking()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isDocumentation()` | `bool` | ✓ | ✓ | ✓ | -| ↳ | `isGloballyReachable()` | `bool` | ✓ | ✓ | ✓ | -| `Classification4Interface` | `isBroadcast()` | `bool` | ✓ | | ✓ | -| ↳ | `isShared()` | `bool` | ✓ | | ✓ | -| ↳ | `isFutureReserved()` | `bool` | ✓ | | ✓ | -| `Classification6Interface` | `getMulticastScope()` | `?int` | | ✓ | ✓ | -| ↳ | `isUniqueLocal()` | `bool` | | ✓ | ✓ | -| ↳ | `isUnicast()` | `bool` | | ✓ | ✓ | -| ↳ | `isUnicastGlobal()` | `bool` | | ✓ | ✓ | -| `ArithmeticInterface` | `getNetworkIp(int $cidr)` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `getBroadcastIp(int $cidr)` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `next()` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `previous()` | Static `IpInterface` | ✓ | ✓ | ✓ | -| ↳ | `offset(int $offset)` | Static `IpInterface` | ✓ | ✓ | ✓ | -| `MultiVersionInterface` | `setDefaultEmbeddingStrategy($strategy)` | `void` | | | ✓ | -| ↳ | `toProtocolAppropriateAddress([$strategy])` | `string` | | | ✓ | -| ↳ | `isEmbedded()` | `bool` | | | ✓ | +| Contract | Method | Returns | IPv4 | IPv6 | Multi | +|------------------------------|------------------------------------------------------|--------------------------------|------|------|-------| +| `IpInterface` | `factory(string $ip, [$strategy])` _(deprecated)_ | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `isPublicUse()` _(deprecated)_ | `bool` | ✓ | ✓ | ✓ | +| `FactoryInterface` | `fromProtocol(string $ip, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `tryFromProtocol(string $ip, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | +| ↳ | `fromBinary(string $binary, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `tryFromBinary(string $binary, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | +| ↳ | `fromHex(string $hex, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `tryFromHex(string $hex, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | +| ↳ | `fromIntegerString(string $integer, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `tryFromIntegerString(string $integer, [$strategy])` | Static `IpInterface` or `null` | ✓ | ✓ | ✓ | +| ↳ | `isValid(string $ip, [$strategy])` | `bool` | ✓ | ✓ | ✓ | +| `Factory4Interface` | `fromInteger(int $integer, [$strategy])` | Static `IpInterface` | ✓ | | ✓ | +| ↳ | `tryFromInteger(int $integer, [$strategy])` | Static `IpInterface` or `null` | ✓ | | ✓ | +| `OutputInterface` | `getBinary()` | `string` | ✓ | ✓ | ✓ | +| ↳ | `getOctets()` | `list` | ✓ | ✓ | ✓ | +| ↳ | `toIntegerString()` | `string` | ✓ | ✓ | ✓ | +| ↳ | `toHexString()` | `string` | ✓ | ✓ | ✓ | +| ↳ | `toString()` | `string` | ✓ | ✓ | ✓ | +| ↳ | `__toString()` | `string` | ✓ | ✓ | ✓ | +| ↳ (`JsonSerializable`) | `jsonSerialize()` | `string` | ✓ | ✓ | ✓ | +| `Output4Interface` | `toDotAddress([$formatter])` | `string` | ✓ | | ✓ | +| ↳ | `toInteger()` | `int` | ✓ | | ✓ | +| `Output6Interface` | `toCompactedAddress([$formatter])` | `string` | | ✓ | ✓ | +| ↳ | `toExpandedAddress()` | `string` | | ✓ | ✓ | +| ↳ | `getSegments()` | `list` | | ✓ | ✓ | +| `VersionIdentityInterface` | `getVersion()` | `int` | ✓ | ✓ | ✓ | +| ↳ | `isVersion(int $version)` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isVersion4()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isVersion6()` | `bool` | ✓ | ✓ | ✓ | +| `ComparisonInterface` | `inRange(IpInterface $ip, int $cidr)` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `getCommonCidr(IpInterface $ip)` | `int` | ✓ | ✓ | ✓ | +| ↳ | `equals(IpInterface $ip)` | `bool` | ✓ | ✓ | ✓ | +| `StrategyDetectionInterface` | `isEmbeddedAccordingToStrategy($strategy)` | `bool` | | ✓ | ✓ | +| ↳ | `getEmbeddedIp([$strategy])` | `IPv4` | | ✓ | ✓ | +| ↳ | `isMapped()` | `bool` | | ✓ | ✓ | +| ↳ | `isDerived()` | `bool` | | ✓ | ✓ | +| ↳ | `isCompatible()` | `bool` | | ✓ | ✓ | +| ↳ | `isNat64WellKnown()` | `bool` | | ✓ | ✓ | +| ↳ | `isNat64LocalUse()` | `bool` | | ✓ | ✓ | +| ↳ | `isTeredo()` | `bool` | | ✓ | ✓ | +| `ClassificationInterface` | `isLinkLocal()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isLoopback()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isMulticast()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isPrivateUse()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isUnspecified()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isBenchmarking()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isDocumentation()` | `bool` | ✓ | ✓ | ✓ | +| ↳ | `isGloballyReachable()` | `bool` | ✓ | ✓ | ✓ | +| `Classification4Interface` | `isBroadcast()` | `bool` | ✓ | | ✓ | +| ↳ | `isShared()` | `bool` | ✓ | | ✓ | +| ↳ | `isFutureReserved()` | `bool` | ✓ | | ✓ | +| `Classification6Interface` | `getMulticastScope()` | `?int` | | ✓ | ✓ | +| ↳ | `isUniqueLocal()` | `bool` | | ✓ | ✓ | +| ↳ | `isUnicast()` | `bool` | | ✓ | ✓ | +| ↳ | `isUnicastGlobal()` | `bool` | | ✓ | ✓ | +| `ArithmeticInterface` | `getNetworkIp(int $cidr)` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `getBroadcastIp(int $cidr)` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `next()` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `previous()` | Static `IpInterface` | ✓ | ✓ | ✓ | +| ↳ | `offset(int $offset)` | Static `IpInterface` | ✓ | ✓ | ✓ | +| `MultiVersionInterface` | `setDefaultEmbeddingStrategy($strategy)` | `void` | | | ✓ | +| ↳ | `toProtocolAppropriateAddress([$formatter])` | `string` | | | ✓ | +| ↳ | `isEmbedded()` | `bool` | | | ✓ |