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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## `6.x`

- 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`.
Expand Down
39 changes: 35 additions & 4 deletions docs/03-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,43 @@ $ip = IP::factory('80.111.111.112');
$ip->getBinary(); // string("Poop")
```

### Octets

`getOctets()` returns the individual bytes of the binary string as an array of
integers between `0` and `255` (four entries for an IPv4 address, sixteen for
IPv6). Calling it on an instance of `Multi` that contains a version 4 address
returns the four octets of the embedded IPv4 address.

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

$ip = IP::fromProtocol('127.0.0.1');
$ip->getOctets(); // array(127, 0, 0, 1)
```

### Segments

`getSegments()` returns the eight 16-bit segments (hextets) of an IPv6 address
as an array of integers between `0` and `65535`. It is only available for the
`IPv6` and `Multi` classes; calling it on an instance of `Multi` that contains a
version 4 address will result in a `WrongVersionException` being thrown.

```php
<?php
use Darsyn\IP\Version\IPv6 as IP;

$ip = IP::fromProtocol('2001:db8::1');
$ip->getSegments(); // array(8193, 3512, 0, 0, 0, 0, 0, 1)
```

## String Casting

Previous versions of this documentation specified that string casting for IP
objects was enabled to get the binary string, but that was unfortunately untrue.
Now, string casting is enabled for all version classes and the `__toString()`
method is promised in `Darsyn\IP\IpInterface`:
The canonical method for casting to a string is `toString()`.
`\Stringable` is a PHP 8 feature and not available on all supported PHP versions,
so `__toString()` is implemented independently (deferring to `toString()`). The
returned string is in protocol-appropriate notation and can be re-parsed via
`fromProtocol()`.

- String casting the `IPv4` class is the equivalent of `$ip->getDotAddress()`.
- String casting the `IPv6` class is the equivalent of
Expand Down
3 changes: 3 additions & 0 deletions docs/10-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
|---------------------------------------|----------------------|------|------|-------|
| `factory(string $ip, [$strategy])` | Static `IpInterface` | ✓ | ✓ | ✓ |
| `getBinary()` | `string` | ✓ | ✓ | ✓ |
| `getOctets()` | `list<int>` | ✓ | ✓ | ✓ |
| `toString()` | `string` | ✓ | ✓ | ✓ |
| `equals(IpInterface $ip)` | `bool` | ✓ | ✓ | ✓ |
| `getVersion()` | `int` | ✓ | ✓ | ✓ |
| `isVersion(int $version)` | `bool` | ✓ | ✓ | ✓ |
Expand Down Expand Up @@ -35,6 +37,7 @@
| `getCompactedAddress()` | `string` | | ✓ | ✓ |
| `getExpandedAddress()` | `string` | | ✓ | ✓ |
| `getCompactedAddress()` | `string` | | ✓ | ✓ |
| `getSegments()` | `list<int>` | | ✓ | ✓ |
| `getMulticastScope()` | `?int` | | ✓ | ✓ |
| `isUniqueLocal()` | `bool` | | ✓ | ✓ |
| `isUnicast()` | `bool` | | ✓ | ✓ |
Expand Down
9 changes: 9 additions & 0 deletions src/AbstractIP.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ final public function getBinary(): string
return $this->ip;
}

public function getOctets(): array
{
$octets = [];
foreach (MbString::split($this->getBinary()) as $byte) {
$octets[] = \ord($byte);
}
return $octets;
}

public function equals(IpInterface $ip): bool
{
return $this->getBinary() === $ip->getBinary();
Expand Down
8 changes: 8 additions & 0 deletions src/Contracts/Output6Interface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,12 @@ public function getCompactedAddress(): string;
* @throws \Darsyn\IP\Exception\IpException
*/
public function getExpandedAddress(): string;

/**
* Get the IP address as an array of the eight 16-bit segments (hextets).
*
* @throws \Darsyn\IP\Exception\WrongVersionException for multi-embedded IPv4 addresses
* @return list<int<0, 65535>>
*/
public function getSegments(): array;
}
16 changes: 16 additions & 0 deletions src/Contracts/OutputInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ interface OutputInterface
/** Get Binary Representation */
public function getBinary(): string;

/**
* Get the IP address as an array of octets; the individual bytes of the
* address (four for an IPv4 address, sixteen for IPv6).
*
* @return list<int<0, 255>>
*/
public function getOctets(): array;

/**
* Get the IP address as a string in its protocol-appropriate notation.
*
* This is the canonical string form regardless of version, re-parseable via
* the fromProtocol() named constructor.
*/
public function toString(): string;

/** Implement string casting for IP objects. */
public function __toString(): string;
}
2 changes: 1 addition & 1 deletion src/Formatter/ConsistentFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function ntop(string $binary): string
private function ntopVersion6(string $binary): string
{
$hex = Binary::toHex($binary);
$parts = \str_split($hex, 4);
$parts = MbString::split($hex, 4);
$zeroes = \array_map(static function ($part) {
return '0000' === $part;
}, $parts);
Expand Down
4 changes: 2 additions & 2 deletions src/Util/Binary.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static function fromHumanReadable(string $asciiBinarySequence): string
}
return '' === $asciiBinarySequence ? '' : static::fromHex(\implode('', \array_map(static function ($byteRepresentation) {
return MbString::padString(\dechex((int) \bindec($byteRepresentation)), 2, '0', \STR_PAD_LEFT);
}, \function_exists('mb_str_split') ? \mb_str_split($asciiBinarySequence, 8, '8bit') : \str_split($asciiBinarySequence, 8))));
}, MbString::split($asciiBinarySequence, 8))));
}

/** @throws \InvalidArgumentException */
Expand All @@ -46,7 +46,7 @@ public static function toHumanReadable(string $binary): string
$hex = static::toHex($binary);
return \implode('', \array_map(static function ($character) {
return MbString::padString(\decbin((int) \hexdec($character)), 8, '0', \STR_PAD_LEFT);
}, \function_exists('mb_str_split') ? \mb_str_split($hex, 2, '8bit') : \str_split($hex, 2)));
}, MbString::split($hex, 2)));
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/Util/MbString.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ public static function subString(string $str, int $start, ?int $length = null):
: \substr($str, $start) . '';
}

/**
* Split a string into fixed-length byte chunks (the mbstring-overload-safe
* equivalent of `str_split()`).
*
* @param int<1, max> $length
* @return list<string>
*/
public static function split(string $str, int $length = 1): array
{
if ('' === $str) {
// str_split of an empty string returns `['']` on PHP 7.1-7.3, and `[]` on PHP 7.4
return [];
}
return \function_exists('\\mb_str_split')
? \mb_str_split($str, $length, '8bit')
: \str_split($str, $length);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/**
* PHP doesn't have a function for multibyte string padding. This should suffice in case
* PHP's internal string functions have been overloaded by the mbstring extension.
Expand Down
7 changes: 6 additions & 1 deletion src/Version/IPv4.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,13 @@ public function isFutureReserved(): bool
&& $this->inRange(new self(Binary::fromHex('f0000000')), 4);
}

public function __toString(): string
public function toString(): string
{
return $this->getDotAddress();
}

public function __toString(): string
{
return $this->toString();
}
}
16 changes: 15 additions & 1 deletion src/Version/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ public function getCompactedAddress(/* ?ProtocolFormatterInterface $formatter =
}
}

public function getSegments(): array
{
$segments = [];
foreach (MbString::split($this->getBinary(), 2) as $word) {
$segments[] = (\ord($word[0]) << 8) + \ord($word[1]);
}
return $segments;
}
Comment thread
zanbaldwin marked this conversation as resolved.

public function getVersion(): int
{
return 6;
Expand Down Expand Up @@ -294,8 +303,13 @@ private function isIetfProtocolAssignment(): bool
&& !$this->inRange(new self(Binary::fromHex('20010030000000000000000000000000')), 28);
}

public function __toString(): string
public function toString(): string
{
return $this->getCompactedAddress();
}

public function __toString(): string
{
return $this->toString();
}
}
22 changes: 21 additions & 1 deletion src/Version/Multi.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ public function getDotAddress(/* ?ProtocolFormatterInterface $formatter = null *
throw new Exception\WrongVersionException(4, 6, (string) $this);
}

public function getOctets(): array
{
return $this->isEmbedded()
? (new IPv4($this->getShortBinary()))->getOctets()
: parent::getOctets();
}

public function getSegments(): array
{
if ($this->isEmbedded()) {
throw new Exception\WrongVersionException(6, 4, (string) $this);
}
return parent::getSegments();
}

public function getVersion(): int
{
return $this->isEmbedded() ? 4 : 6;
Expand Down Expand Up @@ -424,8 +439,13 @@ private function isVersion4CompatibleWithCurrentStrategy(IpInterface $ip): bool
return $this->isVersion4() && $ip->isVersion4() && $this->embeddingStrategy->isEmbedded($ip->getBinary());
}

public function __toString(): string
public function toString(): string
{
return $this->getProtocolAppropriateAddress();
}

public function __toString(): string
{
return $this->toString();
}
}
11 changes: 11 additions & 0 deletions tests/DataProvider/IPv4.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ public static function getValidIpAddresses()
return \array_merge(self::getValidBinarySequences(), self::getValidProtocolIpAddresses());
}

/** @return list<array{string, list<int<0, 255>>}> */
public static function getOctetAddresses()
{
return [
['119.14.113.44', [119, 14, 113, 44]],
['192.168.1.254', [192, 168, 1, 254]],
['0.0.0.0', [0, 0, 0, 0]],
['255.255.255.255', [255, 255, 255, 255]],
];
}

/** @return list<array{string}> */
public static function getInvalidIpAddresses()
{
Expand Down
20 changes: 20 additions & 0 deletions tests/DataProvider/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ public static function getValidIpAddresses()
return \array_merge(self::getValidBinarySequences(), self::getValidProtocolIpAddresses());
}

/** @return list<array{string, list<int<0, 255>>}> */
public static function getOctetAddresses()
{
return [
['2001:db8::a60:8a2e:370:7334', [32, 1, 13, 184, 0, 0, 0, 0, 10, 96, 138, 46, 3, 112, 115, 52]],
['::', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
['ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]],
];
}

/** @return list<array{string, list<int<0, 65535>>}> */
public static function getSegmentAddresses()
{
return [
['2001:db8::a60:8a2e:370:7334', [8193, 3512, 0, 0, 2656, 35374, 880, 29492]],
['::', [0, 0, 0, 0, 0, 0, 0, 0]],
['ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', [65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535]],
];
}

/** @return list<array{string}> */
public static function getInvalidIpAddresses()
{
Expand Down
22 changes: 22 additions & 0 deletions tests/DataProvider/Multi.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ public static function getValidIpAddresses()
return \array_merge(self::getValidBinarySequences(), self::getValidProtocolIpAddresses());
}

/** @return list<array{string, list<int<0, 255>>}> */
public static function getOctetAddresses()
{
return [
// Embedded (Mapped) addresses report the four octets of the embedded IPv4 address.
['119.14.113.44', [119, 14, 113, 44]],
['12.34.56.78', [12, 34, 56, 78]],
// Non-embedded addresses report all sixteen octets.
['2001:db8::a60:8a2e:370:7334', [32, 1, 13, 184, 0, 0, 0, 0, 10, 96, 138, 46, 3, 112, 115, 52]],
['::1', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]],
];
}

/** @return list<array{string, list<int<0, 65535>>}> */
public static function getSegmentAddresses()
{
return [
['2001:db8::a60:8a2e:370:7334', [8193, 3512, 0, 0, 2656, 35374, 880, 29492]],
['::1', [0, 0, 0, 0, 0, 0, 0, 1]],
];
}

/** @return list<array{string, string, string, string, string}> */
public static function getValidIpVersion4Addresses()
{
Expand Down
27 changes: 27 additions & 0 deletions tests/Version/IPv4Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,33 @@ public function testStringCasting(string $value, string $expectedHex, string $ex
$this->assertSame($expectedDot, (string) $ip);
}

/**
* @test
* @dataProvider \Darsyn\IP\Tests\DataProvider\IPv4::getValidProtocolIpAddresses()
*/
#[PHPUnit\Test]
#[PHPUnit\DataProviderExternal(IPv4DataProvider::class, 'getValidProtocolIpAddresses')]
public function testToStringReturnsCanonicalNotation(string $value, string $expectedHex, string $expectedDot): void
{
$ip = IP::fromProtocol($value);
$this->assertSame($expectedDot, $ip->toString());
$this->assertSame((string) $ip, $ip->toString());
$this->assertSame($ip->getBinary(), IP::fromProtocol($ip->toString())->getBinary());
}

/**
* @test
* @dataProvider \Darsyn\IP\Tests\DataProvider\IPv4::getOctetAddresses()
* @param list<int<0, 255>> $expectedOctets
*/
#[PHPUnit\Test]
#[PHPUnit\DataProviderExternal(IPv4DataProvider::class, 'getOctetAddresses')]
public function testGetOctets(string $value, array $expectedOctets): void
{
$ip = IP::fromProtocol($value);
$this->assertSame($expectedOctets, $ip->getOctets());
}

/** @test */
#[PHPUnit\Test]
public function testPerCallFormatterOverridesGlobal(): void
Expand Down
Loading