Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## `6.x`

- 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 by passing a
Expand Down
80 changes: 69 additions & 11 deletions docs/03-overview.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Overview

> The `::factory()` static method is now deprecated. See the strict-parsing
> named constructors below.

IP addresses get automatically validated on creation through the static factory
method; if the IP address supplied is invalid an `InvalidIpAddressException`
will be thrown.
Expand Down Expand Up @@ -55,7 +58,7 @@ try {
Each class has methods for determining the version:

- `$ip->getVersion()` returns the IP address version (either `int(4)` or
`int(6)`).
`int(6)`).
- `$ip->isVersion($version)` returns a boolean value on whether the `$ip` object
is the version specified in `$version` (which must be either `int(4)` or
`int(6)`).
Expand All @@ -70,9 +73,11 @@ Each class has methods for determining the version:

## Instantiation

All classes are instantiated using the `factory()` static method. This method
validates the input and converts it into binary. In the case of the `Multi`
class it also packs any version 4 addresses into a version 6 address.
Many instances are constructed for all [helper](./04-helpers.md) and
[type](./07-types.md) methods. Validating the input every time a new instance
is constructed slows things down considerably, so to speed up internal
processes the constructor does not perform any input validation. Because of
this the constructor method has been kept private.

```php
<?php
Expand All @@ -81,15 +86,68 @@ use Darsyn\IP\Version\Multi as IP;
try {
$ip = new IP('127.0.0.1');
} catch (\Error) {
echo 'Cannot create IP using "new"; please use IP::factory() instead.';
echo 'Cannot create IP using "new"; please use IP::fromProtocol() instead.';
}
```

> Many instances are constructed for all [helper](./04-helpers.md) and
> [type](./07-types.md) methods. Validating the input every time a new instance
> is constructed slows things down considerably, so to speed up internal
> processes the constructor does not perform any input validation. Because of
> this the constructor method has been kept private.
> **Deprecated:** `factory()` is deprecated because it accepts raw binary
> sequences as well as protocol notation. Prefer the strict named constructors:
> `fromProtocol()` for protocol notation, or `fromBinary()` for a raw binary
> sequence.

`Darsyn\IP\Contracts\FactoryInterface` provides strict, single-purpose entry points:

- `fromProtocol()` parses protocol notation **only**; a raw binary sequence is
rejected with an `InvalidIpAddressException`.
- `fromBinary()` accepts a raw binary sequence **only**, of exactly the right
length, throwing an `InvalidBinaryException` otherwise.
- `fromHex()` accepts a hexadecimal string (no `0x` prefix, case-insensitive).

```php
<?php
use Darsyn\IP\Version\IPv4;
use Darsyn\IP\Exception;

IPv4::factory('abcd'); // Accepted: the raw bytes become "97.98.99.100".

try {
IPv4::fromProtocol('abcd');
} catch (Exception\InvalidIpAddressException $e) {
echo 'Not valid IP notation, and never treated as raw bytes.';
}

IPv4::fromBinary("\x7f\x00\x00\x01"); // string("127.0.0.1")
IPv4::fromHex('7f000001'); // string("127.0.0.1")
```

Each strict constructor has a non-throwing companion `tryFrom*` that returns
`null` on failure, mirroring the behaviour of PHP enums.

```php
<?php
use Darsyn\IP\Version\IPv4;

if (null === $ip = IPv4::tryFromProtocol($_GET['ip'])) {
echo 'Please supply a valid IPv4 address.';
}
```

Additionally, `FactoryInterface::isValid()` is available for a simple boolean
check on protocol notation:

```php
<?php
use Darsyn\IP\Version\IPv4;

IPv4::isValid('127.0.0.1'); // bool(true)
IPv4::isValid('abcd'); // bool(false)
```

When using the `Multi` class, each of these methods accepts the same optional
embedding strategy as `factory()` does as its final argument.

> **Note:** `InvalidBinaryException` extends `InvalidIpAddressException`, so
> existing `catch` blocks keep working unchanged.

## Return Formats

Expand Down Expand Up @@ -173,7 +231,7 @@ $ip->getProtocolAppropriateAddress(); // string("127.0.0.1")

`getBinary()` returns the 16 byte (4 bytes if using `IPv4`) binary string of the
IP address. This will most likely contain non-printable characters, so is not
appropriate for displaying.
appropriate for displaying.

```php
<?php
Expand Down
64 changes: 64 additions & 0 deletions src/Contracts/FactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Darsyn\IP\Contracts;

/**
* @experimental
*/
interface FactoryInterface
{
/**
* Create a New IP From Protocol Notation
*
* Strictly parses an IP address from its protocol notation only. Unlike
* factory(), a raw binary sequence is NOT accepted; user-supplied strings
* never reach the binary path (avoiding an SSRF/validation footgun).
*
* @throws \Darsyn\IP\Exception\InvalidIpAddressException
* @throws \Darsyn\IP\Exception\WrongVersionException
* @return static
*/
public static function fromProtocol(string $ip);

/**
* Create a New IP From Protocol Notation, or Null on Failure
*
* @return static|null
*/
public static function tryFromProtocol(string $ip);

/**
* Create a New IP From a Raw Binary Sequence
*
* @throws \Darsyn\IP\Exception\InvalidBinaryException
* @return static
*/
public static function fromBinary(string $binary);

/**
* Create a New IP From a Raw Binary Sequence, or Null on Failure
*
* @return static|null
*/
public static function tryFromBinary(string $binary);

/**
* Create a New IP From a Hexadecimal String
*
* @throws \Darsyn\IP\Exception\InvalidIpAddressException
* @return static
*/
public static function fromHex(string $hex);

/**
* Create a New IP From a Hexadecimal String, or Null on Failure
*
* @return static|null
*/
public static function tryFromHex(string $hex);

/** Whether the supplied string is valid IP protocol notation. */
public static function isValid(string $ip): bool;
}
7 changes: 7 additions & 0 deletions src/Exception/InvalidBinaryException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace Darsyn\IP\Exception;

class InvalidBinaryException extends InvalidIpAddressException {}
1 change: 1 addition & 0 deletions src/IpInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface IpInterface extends ArithmeticInterface, ClassificationInterface, Comp
* @throws \Darsyn\IP\Exception\InvalidIpAddressException
* @throws \Darsyn\IP\Exception\WrongVersionException
* @return static
* @deprecated Use fromProtocol() for protocol notation, or fromBinary() for a raw binary sequence.
*/
public static function factory(string $ip);

Expand Down
4 changes: 2 additions & 2 deletions src/Strategy/Nat64.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public static function wellKnown(): self
}

/**
* Named constructor for a Network-Specific Prefix from an operator's own
* unicast space, eg `Nat64::networkSpecific(IPv6::factory('2001:db8:122:344::'), 64)`.
* Named constructor for a Network-Specific Prefix from an operator's own unicast
* space, eg `Nat64::networkSpecific(IPv6::fromProtocol('2001:db8:122:344::'), 64)`.
*
* @throws \InvalidArgumentException
*/
Expand Down
71 changes: 71 additions & 0 deletions src/Version/IPv4.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
*/
class IPv4 extends AbstractIP implements Version4Interface
{
/** @deprecated Use fromProtocol() or fromBinary() instead. */
public static function factory(string $ip)
{
try {
Expand All @@ -48,6 +49,76 @@ public static function factory(string $ip)
return new static($binary);
}

public static function fromProtocol(string $ip)
{
try {
$binary = self::getProtocolFormatter()->pton($ip);
} catch (Exception\IpException $e) {
throw new Exception\InvalidIpAddressException($ip, $e);
}
// pton() returns a raw 4/16-byte string verbatim as a permissive
// fallback (the behaviour the old factory relies on); strict protocol
// parsing must reject anything not actually parsed from protocol notation.
// TODO: This _really_ needs to change before the next major version bump.
if ($binary === $ip) {
throw new Exception\InvalidIpAddressException($ip);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if (4 !== MbString::getLength($binary)) {
throw new Exception\WrongVersionException(4, 6, $ip);
}
return new static($binary);
}

public static function tryFromProtocol(string $ip)
{
try {
return static::fromProtocol($ip);
} catch (Exception\InvalidIpAddressException $e) {
return null;
}
}

public static function fromBinary(string $binary)
{
if (4 !== MbString::getLength($binary)) {
throw new Exception\InvalidBinaryException($binary);
}
return new static($binary);
}

public static function tryFromBinary(string $binary)
{
try {
return static::fromBinary($binary);
} catch (Exception\InvalidIpAddressException $e) {
return null;
}
}
Comment thread
zanbaldwin marked this conversation as resolved.

public static function fromHex(string $hex)
{
try {
$binary = Binary::fromHex($hex);
} catch (\InvalidArgumentException $e) {
throw new Exception\InvalidIpAddressException($hex, $e);
}
return static::fromBinary($binary);
}

public static function tryFromHex(string $hex)
{
try {
return static::fromHex($hex);
} catch (Exception\InvalidIpAddressException $e) {
return null;
}
}

public static function isValid(string $ip): bool
{
return null !== static::tryFromProtocol($ip);
}

public function getDotAddress(/* ?ProtocolFormatterInterface $formatter = null */): string
{
try {
Expand Down
74 changes: 73 additions & 1 deletion src/Version/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*/
class IPv6 extends AbstractIP implements Version6Interface
{
/** @deprecated Use fromProtocol() or fromBinary() instead. */
public static function factory(string $ip)
{
try {
Expand All @@ -46,14 +47,85 @@ public static function factory(string $ip)
return new static($binary);
}

public static function fromProtocol(string $ip)
{
try {
$binary = self::getProtocolFormatter()->pton($ip);
} catch (Exception\IpException $e) {
throw new Exception\InvalidIpAddressException($ip, $e);
}
// (see rant in IPv4::fromProtocol).
if ($binary === $ip) {
throw new Exception\InvalidIpAddressException($ip);
}
if (16 !== MbString::getLength($binary)) {
throw new Exception\WrongVersionException(6, 4, $ip);
}
return new static($binary);
}

public static function tryFromProtocol(string $ip)
{
try {
return static::fromProtocol($ip);
} catch (Exception\InvalidIpAddressException $e) {
return null;
}
}

public static function fromBinary(string $binary)
{
if (16 !== MbString::getLength($binary)) {
throw new Exception\InvalidBinaryException($binary);
}
return new static($binary);
}

public static function tryFromBinary(string $binary)
{
try {
return static::fromBinary($binary);
} catch (Exception\InvalidIpAddressException $e) {
return null;
}
}

public static function fromHex(string $hex)
{
try {
$binary = Binary::fromHex($hex);
} catch (\InvalidArgumentException $e) {
throw new Exception\InvalidIpAddressException($hex, $e);
}
return static::fromBinary($binary);
}

public static function tryFromHex(string $hex)
{
try {
return static::fromHex($hex);
} catch (Exception\InvalidIpAddressException $e) {
return null;
}
}

public static function isValid(string $ip): bool
{
return null !== static::tryFromProtocol($ip);
}

/**
* @throws \Darsyn\IP\Exception\InvalidIpAddressException
* @throws \Darsyn\IP\Exception\WrongVersionException
* @return static
* TODO: Deprecate method after I've decided on the whole
* canonical/non-canonical packing situation. Will replace with
* IPv6::fromEmbed(Version4Interface, ?EmbeddingStrategyInterface).
*/
public static function fromEmbedded(string $ip, ?EmbeddingStrategyInterface $strategy = null)
{
return new static(Multi::factory($ip, $strategy)->getBinary());
$multi = Multi::tryFromProtocol($ip, $strategy) ?? Multi::fromBinary($ip, $strategy);
return new static($multi->getBinary());
}
Comment thread
zanbaldwin marked this conversation as resolved.

public function getExpandedAddress(): string
Expand Down
Loading