├── shim
├── override.attribute.php
└── deprecated.attribute.php
├── .gitignore
├── src
├── Address.php
├── Network.php
├── IPAddress.php
├── NetworkAddress.php
├── CIDRAddress.php
├── IPv4
│ ├── CIDRv4Address.php
│ ├── IPv4Address.php
│ └── IPv4Network.php
├── IPv6
│ ├── CIDRv6Address.php
│ ├── IPv6Address.php
│ └── IPv6Network.php
├── Assert.php
└── MACAddress.php
├── infection.json.dist
├── phpunit.xml.dist
├── psalm.xml
├── .github
├── actions
│ └── composer
│ │ └── composer
│ │ ├── install
│ │ ├── run.sh
│ │ └── action.yaml
│ │ └── determine-cache-directory
│ │ └── action.yaml
└── workflows
│ └── ci.yml
├── CHANGELOG.md
├── tests
├── IPAddressTest.php
├── CIDRAddressTest.php
├── NetworkAddressTest.php
├── IPv4
│ ├── IPv4NetworkTest.php
│ ├── CIDRv4AddressTest.php
│ └── IPv4AddressTest.php
├── IPv6
│ ├── CIDRv6AddressTest.php
│ ├── IPv6AddressTest.php
│ └── IPv6NetworkTest.php
└── MACAddressTest.php
├── CONTRIBUTING.md
├── phpcs.xml.dist
├── composer.json
├── README.md
└── LICENSE.md
/shim/override.attribute.php:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests
10 |
11 |
12 |
13 |
14 |
15 | ./src
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.github/actions/composer/composer/install/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | dependencies="${COMPOSER_INSTALL_DEPENDENCIES}"
4 |
5 | if [[ ${dependencies} == "lowest" ]]; then
6 | composer update --no-interaction --no-progress --prefer-lowest
7 |
8 | exit $?
9 | fi
10 |
11 | if [[ ${dependencies} == "locked" ]]; then
12 | composer install --no-interaction --no-progress
13 |
14 | exit $?
15 | fi
16 |
17 | if [[ ${dependencies} == "highest" ]]; then
18 | composer update --no-interaction --no-progress
19 |
20 | exit $?
21 | fi
22 |
23 | echo "::error::The value for the \"dependencies\" input needs to be one of \"lowest\", \"locked\"', \"highest\"' - got \"${dependencies}\" instead."
24 |
25 | exit 1
26 |
--------------------------------------------------------------------------------
/src/IPAddress.php:
--------------------------------------------------------------------------------
1 | > $GITHUB_ENV"
17 |
--------------------------------------------------------------------------------
/src/NetworkAddress.php:
--------------------------------------------------------------------------------
1 | prefixLength;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/IPAddressTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidArgumentException::class);
19 | IPAddress::fromString('hello world');
20 | }
21 |
22 | public static function provideFromStringTestData(): iterable
23 | {
24 | return [
25 | 'ip v4' => ['127.0.0.1', IPv4Address::class],
26 | 'ip v6' => ['fe80::a65:78:0:22', IPv6Address::class],
27 | ];
28 | }
29 |
30 | #[DataProvider('provideFromStringTestData')]
31 | public function testShouldConstructFromString(
32 | string $address,
33 | string $expectedClass,
34 | string|null $expectedToString = null,
35 | ): void {
36 | $result = IPAddress::fromString($address);
37 |
38 | self::assertInstanceOf($expectedClass, $result);
39 | self::assertSame($expectedToString ?? $address, $result->toString());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/CIDRAddressTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidArgumentException::class);
19 | CIDRAddress::fromString('hello world');
20 | }
21 |
22 | public static function provideFromStringTestData(): iterable
23 | {
24 | return [
25 | 'cidr v4' => ['192.168.0.0/24', CIDRv4Address::class],
26 | 'cidr v6' => ['fe80::/64', CIDRv6Address::class],
27 | ];
28 | }
29 |
30 | #[DataProvider('provideFromStringTestData')]
31 | public function testShouldConstructFromString(
32 | string $address,
33 | string $expectedClass,
34 | string|null $expectedToString = null,
35 | ): void {
36 | $result = CIDRAddress::fromString($address);
37 |
38 | self::assertInstanceOf($expectedClass, $result);
39 | self::assertSame($expectedToString ?? $address, $result->toString());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Contributions in any form are welcome.
4 |
5 | ## Reporting bugs
6 |
7 | If you'd like to report a bug you may open an issue on github.
8 |
9 | ## Contributing Code
10 |
11 | When contributing code, you are doing this under the terms of the
12 | [GNU Lesser General Public License Version 3](http://www.gnu.org/licenses/lgpl-3.0.html) (LGPL) and you
13 | agree that your contribution will fall under this license as well. You also agree that you
14 | will not contribute code that is copyright of a third party or infringes the LGPL of this project.
15 |
16 | To contribute code, fork this repository on github, make your changes and open a pull request.
17 | When your change fixes a bug, open the pull request against the `master` branch. For all other changes
18 | the pull request must be targeted to the `develop` branch.
19 |
20 | Make sure the following requirements are met:
21 |
22 | - The changes must pass CI which includes:
23 | - Unit tests
24 | - Infection tests
25 | - Static analysis
26 | - Coding style checks
27 | - Your changes are covered by tests.
28 | - Update the changelog (CHANGELOG.md) accordingly. Changes that do not
29 | affect library users (i.e. CI config) may be omitted.
30 | - When you submit a feature describe what it does and what
31 | the use case is. State an example for better understanding
32 | if necessary.
33 | - Provide documentation for new features
34 |
35 | Please note that we may decide to reject your contribution, especially when
36 | these requirements are not met.
37 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | src
14 | tests
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | tests/*
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "luka/network-address-types",
3 | "description": "Provides typed value objects for network addresses (IP v4/v6 and MAC)",
4 | "type": "library",
5 | "license": "LGPL-3.0",
6 | "authors": [
7 | {
8 | "name": "Axel Helmert",
9 | "email": "axel.helmert@luka.de"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-4": {
14 | "LUKA\\Network\\": "src/"
15 | }
16 | },
17 | "autoload-dev": {
18 | "psr-4": {
19 | "LUKATest\\Network\\": "tests/"
20 | },
21 | "classmap": [
22 | "shim/override.attribute.php",
23 | "shim/deprecated.attribute.php"
24 | ]
25 | },
26 | "require": {
27 | "php": "8.2 - 8.4",
28 | "ext-gmp": "*",
29 | "ext-json": "*"
30 | },
31 | "require-dev": {
32 | "doctrine/coding-standard": "^13.0.1",
33 | "phpunit/phpunit": "^10.5",
34 | "vimeo/psalm": "^6"
35 | },
36 | "scripts": {
37 | "test": "phpunit --testdox",
38 | "analyse": "psalm",
39 | "cs-fix": "phpcbf",
40 | "cs-check": [
41 | "mkdir -p .build/php_codesniffer/",
42 | "phpcs"
43 | ],
44 | "infection-test": "infection --min-msi=84 --min-covered-msi=86",
45 | "check": [
46 | "@test",
47 | "@analyse",
48 | "@cs-check"
49 | ]
50 | },
51 | "config": {
52 | "sort-packages": true,
53 | "allow-plugins": {
54 | "dealerdirect/phpcodesniffer-composer-installer": true
55 | }
56 | },
57 | "extra": {
58 | "branch-alias": {
59 | "dev-master": "1.2.x-dev",
60 | "dev-develop": "1.3.x-dev"
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/NetworkAddressTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidArgumentException::class);
22 | NetworkAddress::fromString('hello world');
23 | }
24 |
25 | /** @return iterable */
26 | public static function provideFromStringTestData(): iterable
27 | {
28 | return [
29 | 'ip v4' => ['127.0.0.1', IPv4Address::class],
30 | 'ip v6' => ['fe80::a65:78:0:22', IPv6Address::class],
31 | 'cidr v4' => ['192.168.0.0/24', CIDRv4Address::class],
32 | 'cidr v6' => ['fe80::/64', CIDRv6Address::class],
33 | 'mac (colon)' => ['28:e5:65:78:00:22', MACAddress::class],
34 | 'mac (dash)' => ['28-e5-65-78-00-22', MACAddress::class, '28:e5:65:78:00:22'],
35 | 'mac (no delim)' => ['28e565780022', MACAddress::class, '28:e5:65:78:00:22'],
36 | ];
37 | }
38 |
39 | /**
40 | * @param class-string $expectedClass
41 | *
42 | * @return void
43 | */
44 | #[DataProvider('provideFromStringTestData')]
45 | public function testShouldConstructFromString(
46 | string $address,
47 | string $expectedClass,
48 | string|null $expectedToString = null,
49 | ): void {
50 | $result = NetworkAddress::fromString($address);
51 |
52 | self::assertInstanceOf($expectedClass, $result);
53 | self::assertSame($expectedToString ?? $address, $result->toString());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/IPv4/CIDRv4Address.php:
--------------------------------------------------------------------------------
1 | address;
51 | }
52 |
53 | #[Override]
54 | public function toNetwork(): IPv4Network
55 | {
56 | return new IPv4Network($this);
57 | }
58 |
59 | #[Override]
60 | public function toString(): string
61 | {
62 | return sprintf('%s/%d', $this->address->toString(), $this->prefixLength);
63 | }
64 |
65 | #[Override]
66 | public function equals(Address $other): bool
67 | {
68 | return $other instanceof self
69 | && $this->address->equals($other->address)
70 | && $this->prefixLength === $other->prefixLength;
71 | }
72 |
73 | #[Override]
74 | public function jsonSerialize(): string
75 | {
76 | return $this->toString();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/IPv6/CIDRv6Address.php:
--------------------------------------------------------------------------------
1 | address->toString(), $this->prefixLength);
51 | }
52 |
53 | #[Override]
54 | public function equals(Address $other): bool
55 | {
56 | return $other instanceof self
57 | && $this->address->equals($other->address)
58 | && $this->prefixLength === $other->prefixLength;
59 | }
60 |
61 | #[Override]
62 | public function toAddress(): IPv6Address
63 | {
64 | return $this->address;
65 | }
66 |
67 | #[Override]
68 | public function toNetwork(): IPv6Network
69 | {
70 | return new IPv6Network($this);
71 | }
72 |
73 | #[Override]
74 | public function jsonSerialize(): string
75 | {
76 | return $this->toString();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/IPv4/IPv4Address.php:
--------------------------------------------------------------------------------
1 | address);
57 | }
58 |
59 | #[Override]
60 | public function equals(Address $other): bool
61 | {
62 | return $other instanceof self
63 | && $this->address === $other->address;
64 | }
65 |
66 | public function isNull(): bool
67 | {
68 | return $this->address === 0;
69 | }
70 |
71 | public function toInt(): int
72 | {
73 | return $this->address;
74 | }
75 |
76 | #[Override]
77 | public function toByteString(): string
78 | {
79 | return pack('N', $this->address);
80 | }
81 |
82 | #[Override]
83 | public function jsonSerialize(): string
84 | {
85 | return $this->toString();
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/IPv4/IPv4Network.php:
--------------------------------------------------------------------------------
1 | getPrefixLength();
22 | $this->netmask = -1 << 32 - $prefixLength & 0xffffffff;
23 | $this->cidr = new CIDRv4Address(
24 | new IPv4Address(
25 | $cidr->toAddress()->toInt() & $this->netmask,
26 | ),
27 | $prefixLength,
28 | );
29 | }
30 |
31 | #[Override]
32 | public function toString(): string
33 | {
34 | return $this->cidr->toString();
35 | }
36 |
37 | #[Override]
38 | public function equals(Address $other): bool
39 | {
40 | return $other instanceof self
41 | && $this->cidr->equals($other->cidr);
42 | }
43 |
44 | #[Override]
45 | public function getRangeMinAddress(): CIDRv4Address
46 | {
47 | $prefix = $this->cidr->getPrefixLength();
48 |
49 | return $prefix > 31
50 | ? $this->cidr
51 | : new CIDRv4Address(
52 | new IPv4Address($this->cidr->toAddress()->toInt() + 1),
53 | $prefix,
54 | );
55 | }
56 |
57 | #[Override]
58 | public function getRangeMaxAddress(): CIDRv4Address
59 | {
60 | $prefix = $this->cidr->getPrefixLength();
61 |
62 | if ($prefix > 30) {
63 | return $prefix === 31 ? $this->getRangeMinAddress() : $this->cidr;
64 | }
65 |
66 | $max = 0xfffffffe & ~$this->netmask;
67 |
68 | return new CIDRv4Address(
69 | new IPv4Address($this->cidr->toAddress()->toInt() | $max),
70 | $prefix,
71 | );
72 | }
73 |
74 | #[Override]
75 | public function containsAddress(IPAddress $address): bool
76 | {
77 | return $address instanceof IPv4Address
78 | && $this->cidr->toAddress()->toInt() === ($address->toInt() & $this->netmask);
79 | }
80 |
81 | #[Override]
82 | public function toCidrAddress(): CIDRv4Address
83 | {
84 | return $this->cidr;
85 | }
86 |
87 | public function toBroadcastAddress(): CIDRv4Address
88 | {
89 | return new CIDRv4Address(
90 | new IPv4Address(
91 | $this->cidr->toAddress()->toInt() | (0xffffffff & ~$this->netmask),
92 | ),
93 | $this->cidr->getPrefixLength(),
94 | );
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/IPv6/IPv6Address.php:
--------------------------------------------------------------------------------
1 | address, 1, GMP_MSW_FIRST),
71 | 16,
72 | "\x00",
73 | STR_PAD_LEFT,
74 | );
75 | }
76 |
77 | #[Override]
78 | public function toString(): string
79 | {
80 | if ($this->isNull()) {
81 | return '::';
82 | }
83 |
84 | $formatted = inet_ntop($this->toByteString());
85 | assert($formatted !== false);
86 |
87 | return $formatted;
88 | }
89 |
90 | #[Override]
91 | public function equals(Address $other): bool
92 | {
93 | return $other instanceof self
94 | && gmp_cmp($this->address, $other->address) === 0;
95 | }
96 |
97 | public function isNull(): bool
98 | {
99 | return gmp_cmp($this->address, '0') === 0;
100 | }
101 |
102 | public function toNumber(): GMP
103 | {
104 | return $this->address;
105 | }
106 |
107 | #[Override]
108 | public function jsonSerialize(): string
109 | {
110 | return $this->toString();
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/IPv4/IPv4NetworkTest.php:
--------------------------------------------------------------------------------
1 | toString());
24 | self::assertEquals($expected, $subject->toCidrAddress()->toString());
25 | }
26 |
27 | public function testShouldContainAddressesWithinTheSamePrefix(): void
28 | {
29 | self::assertTrue(
30 | (new IPv4Network(CIDRv4Address::fromString('88.154.76.23/24')))
31 | ->containsAddress(IPv4Address::fromString('88.154.76.23')),
32 | );
33 | }
34 |
35 | public static function provideRangeTestData(): iterable
36 | {
37 | $data = [
38 | '88.154.76.23/24' => ['88.154.76.1/24', '88.154.76.254/24'],
39 | '88.154.76.0/24' => ['88.154.76.1/24', '88.154.76.254/24'],
40 | '88.154.76.23/8' => ['88.0.0.1/8', '88.255.255.254/8'],
41 | '88.154.76.23/32' => ['88.154.76.23/32', '88.154.76.23/32'],
42 | '88.154.76.23/31' => ['88.154.76.23/31', '88.154.76.23/31'],
43 | ];
44 |
45 | foreach ($data as $input => $range) {
46 | yield $input => [$input, ...$range];
47 | }
48 | }
49 |
50 | #[DataProvider('provideRangeTestData')]
51 | public function testShouldProvideCorrectAddressRange(string $input, string $min, string $max): void
52 | {
53 | $subject = new IPv4Network(CIDRv4Address::fromString($input));
54 |
55 | self::assertSame($min, $subject->getRangeMinAddress()->toString());
56 | self::assertSame($max, $subject->getRangeMaxAddress()->toString());
57 | }
58 |
59 | public function testShouldCompareEquality(): void
60 | {
61 | $first = new IPv4Network(CIDRv4Address::fromString('127.0.0.1/8'));
62 | $second = new IPv4Network(CIDRv4Address::fromString('127.0.0.1/8'));
63 |
64 | self::assertNotSame($first, $second);
65 | self::assertTrue($first->equals($second));
66 | }
67 |
68 | public static function provideInequalityTestData(): iterable
69 | {
70 | return [
71 | 'different network' => ['127.0.0.1/8', new IPv4Network(CIDRv4Address::fromString('127.0.0.1/16'))],
72 | 'different type' => ['127.0.0.1/8', new IPv6Network(CIDRv6Address::fromString('::1/10'))],
73 | 'cidr' => ['127.0.0.1/8', CIDRv4Address::fromString('127.0.0.1/8')],
74 | ];
75 | }
76 |
77 | #[DataProvider('provideInequalityTestData')]
78 | public function testShouldNotMatchInequality(string $subject, Address $other): void
79 | {
80 | self::assertFalse((new IPv4Network(CIDRv4Address::fromString($subject)))->equals($other));
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/IPv6/IPv6Network.php:
--------------------------------------------------------------------------------
1 | getPrefixLength();
33 | $this->netmask = gmp_init(str_pad(str_repeat('1', $prefix), 128, '0', STR_PAD_RIGHT), 2);
34 | $this->cidr = new CIDRv6Address(
35 | new IPv6Address(
36 | gmp_and(
37 | $cidr->toAddress()->toNumber(),
38 | $this->netmask,
39 | ),
40 | ),
41 | $prefix,
42 | );
43 | }
44 |
45 | #[Override]
46 | public function toString(): string
47 | {
48 | return $this->cidr->toString();
49 | }
50 |
51 | #[Override]
52 | public function equals(Address $other): bool
53 | {
54 | return $other instanceof self
55 | && $this->cidr->equals($other->cidr)
56 | && gmp_cmp($this->netmask, $other->netmask) === 0;
57 | }
58 |
59 | #[Override]
60 | public function getRangeMinAddress(): CIDRv6Address
61 | {
62 | $prefix = $this->cidr->getPrefixLength();
63 |
64 | return $prefix === 128
65 | ? $this->cidr
66 | : new CIDRv6Address(
67 | new IPv6Address(
68 | gmp_add(
69 | $this->cidr->toAddress()->toNumber(),
70 | gmp_init('1', 2),
71 | ),
72 | ),
73 | $prefix,
74 | );
75 | }
76 |
77 | #[Override]
78 | public function getRangeMaxAddress(): CIDRAddress
79 | {
80 | $prefix = $this->cidr->getPrefixLength();
81 |
82 | return $prefix === 128
83 | ? $this->cidr
84 | : new CIDRv6Address(
85 | new IPv6Address(
86 | gmp_add(
87 | $this->cidr->toAddress()->toNumber(),
88 | gmp_init(str_repeat('1', 128 - $prefix), 2),
89 | ),
90 | ),
91 | $prefix,
92 | );
93 | }
94 |
95 | #[Override]
96 | public function containsAddress(IPAddress $address): bool
97 | {
98 | return $address instanceof IPv6Address
99 | && gmp_cmp(
100 | gmp_and(
101 | $address->toNumber(),
102 | $this->netmask,
103 | ),
104 | $this->cidr->toAddress()->toNumber(),
105 | ) === 0;
106 | }
107 |
108 | #[Override]
109 | public function toCidrAddress(): CIDRAddress
110 | {
111 | return $this->cidr;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Assert.php:
--------------------------------------------------------------------------------
1 | = $min && $value <= $max) {
51 | return;
52 | }
53 |
54 | self::throwInvalidArgument($message ?? 'Expected %d to be in range of %d - %d.', $value, $min, $max);
55 | }
56 |
57 | /** @psalm-pure */
58 | public static function contains(string $value, string $substring, string|null $message = null): void
59 | {
60 | if (strpos($value, $substring) !== false) {
61 | return;
62 | }
63 |
64 | self::throwInvalidArgument($message ?? 'Expected "%s" to contain "%s".', $value, $substring);
65 | }
66 |
67 | /**
68 | * @psalm-pure
69 | * @psalm-assert numeric $value
70 | */
71 | public static function integerish(string|int $value, string|null $message = null): void
72 | {
73 | // phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators
74 | if (is_numeric($value) && $value == (int)$value) {
75 | return;
76 | }
77 |
78 | // phpcs:enable
79 | self::throwInvalidArgument($message ?? 'Expected "%s" to be an integer-like value', $value);
80 | }
81 |
82 | /** @psalm-pure */
83 | public static function lessThanEq(int $value, int $max, string|null $message = null): void
84 | {
85 | if ($value <= $max) {
86 | return;
87 | }
88 |
89 | self::throwInvalidArgument($message ?? 'Expected %d to be less than %d', $value, $max);
90 | }
91 |
92 | /**
93 | * @param non-empty-string $regex
94 | *
95 | * @psalm-pure
96 | */
97 | public static function regex(string $value, string $regex, string|null $message = null): void
98 | {
99 | if (preg_match($regex, $value)) {
100 | return;
101 | }
102 |
103 | self::throwInvalidArgument($message ?? 'Expected "%s" to match "%s".', $value, $regex);
104 | }
105 |
106 | /** @psalm-pure */
107 | private static function throwInvalidArgument(string $message, float|int|string ...$args): void
108 | {
109 | throw new InvalidArgumentException(
110 | vsprintf($message, $args),
111 | );
112 | }
113 |
114 | /** @psalm-pure */
115 | private static function typeStringFor(mixed $value): string
116 | {
117 | return is_object($value) ? $value::class : gettype($value);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tests/IPv4/CIDRv4AddressTest.php:
--------------------------------------------------------------------------------
1 | [$ip];
31 | }
32 | }
33 |
34 | public static function provideInvalidValues(): iterable
35 | {
36 | $list = [
37 | 'a.7.s.s',
38 | 'localhost',
39 | '127.0.0/66',
40 | '10.0.5.256/24',
41 | '10.0.256.2',
42 | '86.134.56.253/32.5',
43 | '86.134.56.253/32/5',
44 | '',
45 | '10.0.5.153/128',
46 | ];
47 |
48 | foreach ($list as $ip) {
49 | yield 'value: ' . $ip => [$ip];
50 | }
51 | }
52 |
53 | #[DataProvider('provideValidValues')]
54 | public function testShouldAcceptValidAddresses(string $fixture): void
55 | {
56 | $subject = CIDRv4Address::fromString($fixture);
57 | self::assertSame($fixture, $subject->toString());
58 | }
59 |
60 | #[DataProvider('provideInvalidValues')]
61 | public function testShouldThrowOnInvalidAddresses(string $fixture): void
62 | {
63 | $this->expectException(InvalidArgumentException::class);
64 | CIDRv4Address::fromString($fixture);
65 | }
66 |
67 | public function testShouldBeConvertibleToIpAddress(): void
68 | {
69 | self::assertTrue(
70 | CIDRv4Address::fromString('128.6.119.56/24')
71 | ->toAddress()
72 | ->equals(IPv4Address::fromString('128.6.119.56')),
73 | );
74 | }
75 |
76 | public function testShouldBeConvertibleToNetwork(): void
77 | {
78 | $subject = CIDRv4Address::fromString('128.6.119.56/24');
79 | self::assertTrue($subject->toNetwork()->equals(new IPv4Network($subject)));
80 | }
81 |
82 | public function testShouldMatchEquality(): void
83 | {
84 | $subject = CIDRv4Address::fromString('128.6.119.56/24');
85 | $other = CIDRv4Address::fromString('128.6.119.56/24');
86 |
87 | self::assertNotSame($other, $subject);
88 | self::assertTrue($subject->equals($other));
89 | }
90 |
91 | public static function provideUnequalAddresses(): iterable
92 | {
93 | return [
94 | 'different prefix' => ['128.6.119.56/24', CIDRv4Address::fromString('128.6.119.56/25')],
95 | 'different address' => ['128.6.119.56/24', CIDRv4Address::fromString('128.6.119.57/24')],
96 | 'different type (MAC)' => ['128.6.119.56/24', MACAddress::generateRandomAddress()],
97 | 'different type (IP v6)' => ['127.0.0.1/8', IPv6Address::fromString('::1')],
98 | ];
99 | }
100 |
101 | #[DataProvider('provideUnequalAddresses')]
102 | public function testShouldNotMatchInequality(string $address, Address $other): void
103 | {
104 | $subject = CIDRv4Address::fromString($address);
105 |
106 | self::assertNotSame($other, $subject);
107 | self::assertFalse($subject->equals($other));
108 | }
109 |
110 | public function testShouldExposePrefixLength(): void
111 | {
112 | $subject = CIDRv4Address::fromString('10.0.0.0/12');
113 |
114 | self::assertSame(12, $subject->getPrefixLength());
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/tests/IPv6/CIDRv6AddressTest.php:
--------------------------------------------------------------------------------
1 | ['2004:6fe8::/64', '2004:6fe8::/64'],
24 | '2004:6fe8::4/64' => ['2004:6fe8::4/64', '2004:6fe8::4/64'],
25 | '::1/8' => ['::1/8', '::1/8'],
26 | ];
27 | }
28 |
29 | #[DataProvider('provideValidStringInput')]
30 | public function testShouldConstructFromString(string $input, string $expected): void
31 | {
32 | $subject = CIDRv6Address::fromString($input);
33 | self::assertSame($expected, $subject->toString());
34 | }
35 |
36 | public static function provideInvalidStringInput(): iterable
37 | {
38 | return [
39 | 'empty string' => [''],
40 | 'missing length' => ['200f:65::'],
41 | 'empty length' => ['200f:65::/'],
42 | 'prefix to large' => ['200f:65::/129'],
43 | 'non-numeric length' => ['200f:65::/f'],
44 | 'empty address' => ['/64'],
45 | ];
46 | }
47 |
48 | #[DataProvider('provideInvalidStringInput')]
49 | public function testShouldThrowInvalidArgumentOnInvalidStringInput(string $input): void
50 | {
51 | $this->expectException(InvalidArgumentException::class);
52 | CIDRv6Address::fromString($input);
53 | }
54 |
55 | public function testShouldBeConvertableToIpAddress(): void
56 | {
57 | self::assertSame(
58 | '2004:6fe8::4',
59 | CIDRv6Address::fromString('2004:6fe8::4/64')
60 | ->toAddress()
61 | ->toString(),
62 | );
63 | }
64 |
65 | public function testShouldMatchEquality(): void
66 | {
67 | $subject = CIDRv6Address::fromString('2004:6fe8::4/64');
68 | $other = CIDRv6Address::fromString('2004:6fe8::4/64');
69 |
70 | self::assertNotSame($subject, $other);
71 | self::assertTrue($subject->equals($other));
72 | }
73 |
74 | public static function provideUnequalAddresses(): iterable
75 | {
76 | return [
77 | 'different prefix' => ['2004:6fe8::4/64', CIDRv6Address::fromString('2004:6fe8::4/65')],
78 | 'different address' => ['2004:6fe8::4/64', CIDRv6Address::fromString('2004:6fe8::5/64')],
79 | 'different type (MAC)' => ['2004:6fe8::4/64', MACAddress::generateRandomAddress()],
80 | 'different type (IP v6)' => ['::1/8', IPv6Address::fromString('::1')],
81 | ];
82 | }
83 |
84 | #[DataProvider('provideUnequalAddresses')]
85 | public function testShouldNotMatchInequality(string $input, Address $other): void
86 | {
87 | $subject = CIDRv6Address::fromString($input);
88 |
89 | self::assertFalse($subject->equals($other));
90 | }
91 |
92 | public function testShouldBeConvertibleToNetwork(): void
93 | {
94 | $subject = CIDRv6Address::fromString('2004:6fe8::4/64');
95 |
96 | self::assertSame(
97 | '2004:6fe8::/64',
98 | $subject->toNetwork()->toString(),
99 | );
100 | }
101 |
102 | public function testShouldBeJsonSerializable(): void
103 | {
104 | $subject = CIDRv6Address::fromString('2004:6fe8::4/64');
105 | $json = json_encode($subject);
106 |
107 | self::assertSame(
108 | $subject->toString(),
109 | CIDRv6Address::fromString(json_decode($json))->toString(),
110 | );
111 | }
112 |
113 | public function testShouldExposePrefixLength(): void
114 | {
115 | $subject = CIDRv6Address::fromString('2004:6fe8::/82');
116 | self::assertSame(82, $subject->getPrefixLength());
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/MACAddress.php:
--------------------------------------------------------------------------------
1 | vendorId = bin2hex(substr($binary, 0, 3));
50 | }
51 |
52 | /**
53 | * @psalm-return self
54 | *
55 | * @psalm-pure
56 | */
57 | #[Override]
58 | public static function fromString(string $address): self
59 | {
60 | Assert::regex($address, self::MAC_ADDRESS_FORMAT, 'Invalid mac address: "%s"');
61 |
62 | return new self(gmp_init(strtr($address, [':' => '', '-' => '']), 16));
63 | }
64 |
65 | /**
66 | * Generate a random mac address with a prefix
67 | *
68 | * The prefix is omitted or empty, this method will ensure that the generated
69 | * Address is flagged as a local one.
70 | *
71 | * @param string $prefix Prefix as hex number, 3 bytes at maximum.
72 | */
73 | public static function generateRandomAddress(string $prefix = ''): self
74 | {
75 | if ($prefix === '') {
76 | return new MACAddress(
77 | gmp_or(
78 | gmp_init(bin2hex(random_bytes(6)), 16),
79 | gmp_init('020000000000', 16),
80 | ),
81 | );
82 | }
83 |
84 | Assert::regex($prefix, '/^([a-f0-9]{2}){0,3}$/i', 'Invalid mac address prefix: "%s"');
85 |
86 | $prefixLength = strlen($prefix);
87 | $randomLength = 6 - (int)($prefixLength / 2);
88 |
89 | assert($randomLength > 0);
90 |
91 | return new MACAddress(
92 | gmp_add(
93 | gmp_init($prefix . str_repeat('0', 12 - $prefixLength), 16),
94 | gmp_init(bin2hex(random_bytes($randomLength)), 16),
95 | ),
96 | );
97 | }
98 |
99 | #[Override]
100 | public function toString(): string
101 | {
102 | $hex = str_pad(gmp_strval($this->address, 16), 12, '0', STR_PAD_LEFT);
103 | $segments = [];
104 |
105 | for ($offset = 0; $offset < 12; $offset += 2) {
106 | $segments[] = substr($hex, $offset, 2);
107 | }
108 |
109 | return implode(':', $segments);
110 | }
111 |
112 | public function toByteString(): string
113 | {
114 | return gmp_export($this->address, 1, GMP_MSW_FIRST);
115 | }
116 |
117 | #[Override]
118 | public function equals(Address $other): bool
119 | {
120 | return $other instanceof self
121 | && gmp_cmp($this->address, $other->address) === 0;
122 | }
123 |
124 | #[Override]
125 | public function jsonSerialize(): string
126 | {
127 | return $this->toString();
128 | }
129 |
130 | public function getVendorID(): string
131 | {
132 | return $this->vendorId;
133 | }
134 |
135 | public function isLocal(): bool
136 | {
137 | return gmp_testbit($this->address, 41);
138 | }
139 |
140 | public function isBroadCast(): bool
141 | {
142 | return gmp_cmp($this->address, gmp_init('ffffffffffff', 16)) === 0;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/tests/IPv4/IPv4AddressTest.php:
--------------------------------------------------------------------------------
1 | [$ip];
32 | }
33 | }
34 |
35 | public static function provideInvalidIps(): iterable
36 | {
37 | $list = [
38 | 'a.7.s.s',
39 | 'localhost',
40 | '127.0.0',
41 | '10.0.5.256',
42 | '10.0.256.2',
43 | '10.256.0.2',
44 | '256.0.0.2',
45 | '10.896.0.2',
46 | '86.1340.56.263',
47 | '',
48 | '127001',
49 | ];
50 |
51 | foreach ($list as $ip) {
52 | yield 'value: ' . $ip => [$ip];
53 | }
54 | }
55 |
56 | #[DataProvider('provideValidIps')]
57 | public function testShouldAcceptValidAddresses(string $fixture): void
58 | {
59 | $subject = IPv4Address::fromString($fixture);
60 | self::assertSame($fixture, $subject->toString());
61 | }
62 |
63 | #[DataProvider('provideInvalidIps')]
64 | public function testShouldThrowOnInvalidAddresses(string $fixture): void
65 | {
66 | $this->expectException(InvalidArgumentException::class);
67 | IPv4Address::fromString($fixture);
68 | }
69 |
70 | public function testShouldCompareEquality(): void
71 | {
72 | $first = IPv4Address::fromString('192.168.55.7');
73 | $second = IPv4Address::fromString('192.168.55.7');
74 |
75 | self::assertNotSame($first, $second);
76 | self::assertTrue($first->equals($second));
77 | }
78 |
79 | public static function provideInequalityTestData(): iterable
80 | {
81 | return [
82 | 'different address' => ['127.0.0.1', IPv4Address::fromString('127.0.0.2')],
83 | 'different type' => ['127.0.0.1', IPv6Address::fromString('::1')],
84 | 'cidr' => ['127.0.0.1', CIDRv4Address::fromString('127.0.0.1/8')],
85 | ];
86 | }
87 |
88 | #[DataProvider('provideInequalityTestData')]
89 | public function testShouldNotMatchInequality(string $subject, Address $other): void
90 | {
91 | self::assertFalse(IPv4Address::fromString($subject)->equals($other));
92 | }
93 |
94 | public function testShouldConvertToInteger(): void
95 | {
96 | $int = IPv4Address::fromString('127.0.0.1')->toInt();
97 |
98 | self::assertSame(0x7f000001, $int);
99 | self::assertSame(
100 | '127.0.0.1',
101 | (new IPv4Address($int))->toString(),
102 | );
103 | }
104 |
105 | public function testShouldExportBinaryValue(): void
106 | {
107 | $expected = '255.0.0.5';
108 | $bytes = IPv4Address::fromString($expected)->toByteString();
109 |
110 | self::assertNotSame($expected, $bytes);
111 | self::assertSame($expected, IPv4Address::fromByteString($bytes)->toString());
112 | }
113 |
114 | public function testShouldIdentifyNullAddress(): void
115 | {
116 | self::assertTrue(IPv4Address::fromString('0.0.0.0')->isNull());
117 | self::assertFalse(IPv4Address::fromString('127.0.0.1')->isNull());
118 | }
119 |
120 | public function testShouldBeJsonSerializable(): void
121 | {
122 | $subject = IPv4Address::fromString('192.168.0.33');
123 | $json = json_encode($subject);
124 |
125 | self::assertTrue(
126 | $subject->equals(
127 | IPv4Address::fromString(json_decode($json)),
128 | ),
129 | );
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/IPv6/IPv6AddressTest.php:
--------------------------------------------------------------------------------
1 | ['::1', '::1'],
27 | '::' => ['::', '::'],
28 | 'a6f4:56::' => ['a6f4:56::', 'a6f4:56::'],
29 | '0:0:0:0:0:0:0:1' => ['0:0:0:0:0:0:0:1', '::1'],
30 | 'fe80::a65:78:0:22' => ['fe80::a65:78:0:22', 'fe80::a65:78:0:22'],
31 | ];
32 | }
33 |
34 | #[DataProvider('provideValidAddressStrings')]
35 | public function testShouldConstructFromString(string $input, string $expected): void
36 | {
37 | $subject = IPv6Address::fromString($input);
38 | self::assertSame($expected, $subject->toString());
39 | }
40 |
41 | public static function provideInvalidAddressStrings(): iterable
42 | {
43 | return [
44 | 'multiple zero omits' => ['fe80::7::22'],
45 | 'too many segments' => ['fe80:0:0:0:0:0:0:0:1'],
46 | 'non-hex digits' => ['fe80::z6:1'],
47 | 'segment too large' => ['fe80::f0893:1'],
48 | ];
49 | }
50 |
51 | #[DataProvider('provideInvalidAddressStrings')]
52 | public function testShouldThrowInvalidArgumentOnInvalidAddressStringInput(string $input): void
53 | {
54 | $this->expectException(InvalidArgumentException::class);
55 | IPv6Address::fromString($input);
56 | }
57 |
58 | public function testShouldExportCorrectByteString(): void
59 | {
60 | self::assertSame(
61 | str_pad("\x01", 16, "\x00", STR_PAD_LEFT),
62 | IPv6Address::fromString('::1')->toByteString(),
63 | );
64 | }
65 |
66 | public function testShouldRestoreFromBinary(): void
67 | {
68 | self::assertSame(
69 | '::1',
70 | IPv6Address::fromBinary("\x01")->toString(),
71 | );
72 | }
73 |
74 | public function testShouldMatchEquality(): void
75 | {
76 | $subject = IPv6Address::fromString('fe80::a65:78:0:22');
77 | $other = IPv6Address::fromString('fe80::a65:78:0:22');
78 |
79 | self::assertNotSame($subject, $other);
80 | self::assertTrue($subject->equals($other));
81 | }
82 |
83 | public static function provideInequalityTestData(): iterable
84 | {
85 | return [
86 | 'different address' => ['fe80::a65:78:0:22', IPv6Address::fromString('fe80::a65:78:0:21')],
87 | 'different type' => ['::1', IPv4Address::fromString('127.0.0.1')],
88 | 'cidr' => ['::1', CIDRv6Address::fromString('::1/64')],
89 | ];
90 | }
91 |
92 | #[DataProvider('provideInequalityTestData')]
93 | public function testShouldNotMatchInequality(string $subject, Address $other): void
94 | {
95 | self::assertFalse(IPv6Address::fromString($subject)->equals($other));
96 | }
97 |
98 | public function testShouldExportNumericValue(): void
99 | {
100 | $subject = IPv6Address::fromString('fe80::a65:78:0:22');
101 | $number = $subject->toNumber();
102 |
103 | self::assertTrue($subject->equals(new IPv6Address($number)));
104 | }
105 |
106 | public function testShouldIdentifyNullAddress(): void
107 | {
108 | self::assertTrue(IPv6Address::fromString('::')->isNull());
109 | self::assertFalse(IPv6Address::fromString('fe80::a65:78:0:22')->isNull());
110 | }
111 |
112 | public function testShouldBeJsonSerializable(): void
113 | {
114 | $subject = IPv6Address::fromString('fe80::a65:78:0:22');
115 | $json = json_encode($subject);
116 |
117 | self::assertSame(
118 | $subject->toString(),
119 | IPv6Address::fromString(json_decode($json))->toString(),
120 | );
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/tests/MACAddressTest.php:
--------------------------------------------------------------------------------
1 | isLocal());
22 | self::assertFalse(MACAddress::fromString('dc:0e:a1:6e:08:c2')->isLocal());
23 | }
24 |
25 | public function testShouldExportTheVendorId(): void
26 | {
27 | self::assertSame('dc0ea1', MACAddress::fromString('dc:0e:a1:6e:08:c2')->getVendorID());
28 | }
29 |
30 | public static function provideInvalidStringInput(): iterable
31 | {
32 | return [
33 | 'empty string' => [''],
34 | 'too many bytes' => ['02:75:00:56:4f:a8:c2'],
35 | 'invalid characters' => ['02:75:00:z6:4f:a8'],
36 | 'arbitary string' => ['hello world'],
37 | ];
38 | }
39 |
40 | #[DataProvider('provideInvalidStringInput')]
41 | public function testShouldThrowWhenConstructedFromInvalidStringAddress(string $input): void
42 | {
43 | $this->expectException(InvalidArgumentException::class);
44 | MACAddress::fromString($input);
45 | }
46 |
47 | public function testShouldStringifyColonSeparatedNotation(): void
48 | {
49 | self::assertSame('dc:0e:a1:6e:08:c2', MACAddress::fromString('dc0ea16e08c2')->toString());
50 | self::assertSame('dc:0e:a1:6e:08:c2', MACAddress::fromString('dc-0e-a1-6e-08-c2')->toString());
51 | }
52 |
53 | public function testShouldIdentifyBroadcastAddress(): void
54 | {
55 | self::assertTrue(MACAddress::fromString('ff:ff:ff:ff:ff:ff')->isBroadCast());
56 | self::assertFalse(MACAddress::fromString('dc:0e:a1:6e:08:c2')->isBroadCast());
57 | }
58 |
59 | public function testShouldBeJsonSerializable(): void
60 | {
61 | $subject = MACAddress::generateRandomAddress();
62 | $json = json_encode($subject);
63 |
64 | self::assertSame(
65 | $subject->toString(),
66 | MACAddress::fromString(json_decode($json))->toString(),
67 | );
68 | }
69 |
70 | public function testShouldGenerateRandomAddresses(): void
71 | {
72 | $first = MACAddress::generateRandomAddress();
73 | $second = MACAddress::generateRandomAddress();
74 |
75 | self::assertNotSame(
76 | $first->toString(),
77 | $second->toString(),
78 | );
79 | }
80 |
81 | public function testShouldGenerateRandomAddressesWithPrefix(): void
82 | {
83 | $prefix = '200000';
84 | $first = MACAddress::generateRandomAddress($prefix);
85 | $second = MACAddress::generateRandomAddress($prefix);
86 |
87 | self::assertNotSame(
88 | $first->toString(),
89 | $second->toString(),
90 | );
91 |
92 | self::assertStringStartsWith('20:00:00', $first->toString());
93 | self::assertStringStartsWith('20:00:00', $second->toString());
94 | }
95 |
96 | public function testShouldExportCorrectByteString(): void
97 | {
98 | self::assertSame(
99 | "\x20\x00\x00\x00\x00\x01",
100 | MACAddress::fromString('20:00:00:00:00:01')->toByteString(),
101 | );
102 | }
103 |
104 | public function testShouldCompareEquality(): void
105 | {
106 | $first = MACAddress::fromString('20:00:00:00:00:01');
107 | $second = MACAddress::fromString('20:00:00:00:00:01');
108 |
109 | self::assertNotSame($first, $second);
110 | self::assertTrue($first->equals($second));
111 | }
112 |
113 | public static function provideInequalityTestData(): iterable
114 | {
115 | return [
116 | 'different address' => ['20:00:00:00:00:01', MACAddress::fromString('20:00:00:00:00:02')],
117 | 'different type' => ['20:00:00:00:00:01', IPv6Address::fromString('::1')],
118 | ];
119 | }
120 |
121 | #[DataProvider('provideInequalityTestData')]
122 | public function testShouldNotMatchInequality(string $subject, Address $other): void
123 | {
124 | self::assertFalse(MACAddress::fromString($subject)->equals($other));
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/tests/IPv6/IPv6NetworkTest.php:
--------------------------------------------------------------------------------
1 | ['2004:6fe8::/64', '2004:6fe8::/64'],
24 | '2004:6fe8::4/64' => ['2004:6fe8::4/64', '2004:6fe8::/64'],
25 | 'ff02::1:2/8' => ['ff02::1:2/8', 'ff00::/8'],
26 | '2004:6fe8::4/0' => ['2004:6fe8::4/0', '::/0'],
27 | '2004:6fe8::4/128' => ['2004:6fe8::4/128', '2004:6fe8::4/128'],
28 | ];
29 | }
30 |
31 | #[DataProvider('provideNormalizePrefixTestData')]
32 | public function testShouldNormalizeCidrPrefix(string $input, string $expected): void
33 | {
34 | $subject = new IPv6Network(CIDRv6Address::fromString($input));
35 | self::assertSame($expected, $subject->toString());
36 | self::assertSame($expected, $subject->toCidrAddress()->toString());
37 | }
38 |
39 | public static function provideRangeTestData(): iterable
40 | {
41 | $data = [
42 | '200e:cafe:5::/120' => ['200e:cafe:5::1/120', '200e:cafe:5::ff/120'],
43 | '200e:cafe:5::15/120' => ['200e:cafe:5::1/120', '200e:cafe:5::ff/120'],
44 | '200e:cafe:5::1/128' => ['200e:cafe:5::1/128', '200e:cafe:5::1/128'],
45 | '200e:cafe:5::1/127' => ['200e:cafe:5::1/127', '200e:cafe:5::1/127'],
46 | '200e:cafe:5::1/126' => ['200e:cafe:5::1/126', '200e:cafe:5::3/126'],
47 | ];
48 |
49 | foreach ($data as $input => $range) {
50 | yield $input => [$input, ...$range];
51 | }
52 | }
53 |
54 | #[DataProvider('provideRangeTestData')]
55 | public function testShouldProvideNetworkRange(string $input, string $min, string $max): void
56 | {
57 | $subject = new IPv6Network(CIDRv6Address::fromString($input));
58 |
59 | self::assertSame($min, $subject->getRangeMinAddress()->toString());
60 | self::assertSame($max, $subject->getRangeMaxAddress()->toString());
61 | }
62 |
63 | public static function provideContainsAddressTestData(): iterable
64 | {
65 | return [
66 | '200e:cafe:5::/120' => ['200e:cafe:5::/120', '200e:cafe:5::15'],
67 | '200e:cafe:5::/64' => ['200e:cafe:5::/64', '200e:cafe:5::56a4:7f54'],
68 | ];
69 | }
70 |
71 | #[DataProvider('provideContainsAddressTestData')]
72 | public function testShouldContainMatchingAddress(string $network, string $address): void
73 | {
74 | $subject = new IPv6Network(CIDRv6Address::fromString($network));
75 |
76 | self::assertTrue($subject->containsAddress(IPv6Address::fromString($address)));
77 | }
78 |
79 | public static function provideNotContainsAddressTestData(): iterable
80 | {
81 | return [
82 | 'not in prefix' => ['200e:cafe:5::/120', IPv6Address::fromString('200e:cafe:5::115')],
83 | 'different address type' => ['200e:cafe:5::/64', IPv4Address::fromString('129.0.0.1')],
84 | ];
85 | }
86 |
87 | #[DataProvider('provideNotContainsAddressTestData')]
88 | public function testShouldNotContainNonMatchingAddress(string $network, IPAddress $address): void
89 | {
90 | $subject = new IPv6Network(CIDRv6Address::fromString($network));
91 |
92 | self::assertFalse($subject->containsAddress($address));
93 | }
94 |
95 | public function testShouldCompareEquality(): void
96 | {
97 | $first = new IPv6Network(CIDRv6Address::fromString('::1/10'));
98 | $second = new IPv6Network(CIDRv6Address::fromString('::1/10'));
99 |
100 | self::assertNotSame($first, $second);
101 | self::assertTrue($first->equals($second));
102 | }
103 |
104 | /** @return iterable */
105 | public static function provideInequalityTestData(): iterable
106 | {
107 | return [
108 | 'different network' => ['::1/10', new IPv6Network(CIDRv6Address::fromString('::1/8'))],
109 | 'different type' => ['::1/10', new IPv4Network(CIDRv4Address::fromString('127.0.0.1/8'))],
110 | 'cidr' => ['::1/10', CIDRv6Address::fromString('::1/10')],
111 | ];
112 | }
113 |
114 | #[DataProvider('provideInequalityTestData')]
115 | public function testShouldNotMatchInequality(string $subject, Address $other): void
116 | {
117 | self::assertFalse((new IPv6Network(CIDRv6Address::fromString($subject)))->equals($other));
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Network Address Types for PHP
2 |
3 | [](https://codeclimate.com/github/lukanetconsult/network-address-types/test_coverage)
4 | [](https://codeclimate.com/github/lukanetconsult/network-address-types/maintainability)
5 | [](https://infection.github.io)
6 |
7 | This library provides types to handle the most common network addresses.
8 | The following address types are supported:
9 |
10 | - MAC addresses in the following forms:
11 | - Colon separated (`00:00:00:00:00:00`)
12 | - Dash separated (`00-00-00-00-00-00`)
13 | - Without separator (`000000000000`)
14 | - IPv4 (ex: `127.0.0.1`)
15 | - IPv6 (ex: `::1`)
16 | - CIDR addresses for IPv4 and IPv6 (ex: `129.0.0.1/12`, `ff80:e::/64`)
17 |
18 | ## Installation
19 |
20 | Installation can be performed with composer:
21 |
22 | ```bash
23 | composer require luka/network-address-types
24 | ```
25 |
26 | ### Requirements
27 |
28 | - PHP
29 | - 7.4 (until version 1.1.0)
30 | - 8.0
31 | - 8.1
32 | - 8.2
33 | - `ext-gmp` for handling IPv6 calculations
34 | - `ext-json` as it supports json_encode by implementing `JSONSerializable`
35 |
36 | # Getting started
37 |
38 | ```php
39 | use LUKA\Network\NetworkAddress;
40 |
41 | $address = NetworkAddress::fromString('127.0.0.1');
42 | assert($address instanceof \LUKA\Network\IPv4\IPv4Address);
43 | $address = NetworkAddress::fromString('127.0.0.1/8');
44 | assert($address instanceof \LUKA\Network\IPv4\CIDRv4Address);
45 | $address = NetworkAddress::fromString('::1');
46 | assert($address instanceof \LUKA\Network\IPv6\IPv6Address);
47 | $address = NetworkAddress::fromString('ff80::1/64');
48 | assert($address instanceof \LUKA\Network\IPv6\CIDRv6Address);
49 | $address = NetworkAddress::fromString('84:34:ff:ff:ff:ff');
50 | assert($address instanceof \LUKA\Network\MACAddress);
51 | ```
52 |
53 | # Serialization
54 |
55 | All types can be constructed from strings and can therefore be converted
56 | to strings as well with the `toString()` method.
57 |
58 | ```php
59 | use LUKA\Network\NetworkAddress;
60 |
61 | assert('::1' === NetworkAddress::fromString('::1')->toString());
62 | ```
63 |
64 | When converting addresses to strings, they will be normalized for the corresponding type:
65 |
66 | ```php
67 | use LUKA\Network\NetworkAddress;
68 |
69 | assert('::1' === NetworkAddress::fromString('0:0:0:0::1')->toString());
70 | assert('00:00:00:00:00:00' === NetworkAddress::fromString('00-00-00-00-00-00')->toString());
71 | ```
72 |
73 | ## JSON
74 |
75 | All address types implement the `JSONSerializable` interface, and can therefore be used with
76 | `json_encode()` directly.
77 |
78 | ## Binary
79 |
80 | IP and MAC addresses can also be converted to the corresponding byte sequence. (for example to storing them in
81 | a `BINARY` database field).
82 |
83 | They can also be constructed from this byte sequence with the static `fromByteString()` method of the corresponding
84 | class.
85 |
86 | # Address Comparison
87 |
88 | ## Compare equality
89 |
90 | Each address implements an `equals()` method to compare it to other
91 | network addresses.
92 |
93 | Addresses are considered equal when they are from the same type and contain
94 | the same value:
95 |
96 | ```php
97 | use LUKA\Network\NetworkAddress;
98 |
99 | assert(
100 | true === NetworkAddress::fromString('::1')
101 | ->equals(NetworkAddress::fromString('::1'))
102 | );
103 |
104 | // Value mismatch:
105 | assert(
106 | false === NetworkAddress::fromString('::1')
107 | ->equals(NetworkAddress::fromString('::2'))
108 | );
109 |
110 | // Type mismatch (different IP version):
111 | assert(
112 | false === NetworkAddress::fromString('::1')
113 | ->equals(NetworkAddress::fromString('127.0.0.1'))
114 | );
115 |
116 | // Type mismatch (cidr vs non-cidr)
117 | assert(
118 | false === NetworkAddress::fromString('192.168.0.5')
119 | ->equals(NetworkAddress::fromString('192.168.0.5/24'))
120 | );
121 | ```
122 |
123 | ## Comparing addresses to networks
124 |
125 | CIDR addresses allow to obtain the corresponding network to the denoted address.
126 | With this network you can check if an IP address is within this network.
127 |
128 | ```php
129 | use LUKA\Network\NetworkAddress;
130 |
131 | $cidr = NetworkAddress::fromString('192.168.0.7/8');
132 | $network = $cidr->toNetwork();
133 |
134 | assert(true === $network->containsAddress(NetworkAddress::fromString('192.168.0.1')));
135 | assert(true === $network->containsAddress(NetworkAddress::fromString('192.45.0.2')));
136 | assert(false === $network->containsAddress(NetworkAddress::fromString('127.10.0.1')));
137 | assert(false === $network->containsAddress(NetworkAddress::fromString('ff80::5')));
138 | ```
139 |
140 | This will work for IPv6 as well:
141 |
142 | ```php
143 | use LUKA\Network\NetworkAddress;
144 |
145 | $cidr = NetworkAddress::fromString('ff80::/64');
146 | $network = $cidr->toNetwork();
147 |
148 | assert(true === $network->containsAddress(NetworkAddress::fromString('ff80::1')));
149 | assert(true === $network->containsAddress(NetworkAddress::fromString('ff80::10:e5:7')));
150 | assert(false === $network->containsAddress(NetworkAddress::fromString('ff80:e::1')));
151 | assert(false === $network->containsAddress(NetworkAddress::fromString('127.0.0.1')));
152 | ```
153 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ### GNU LESSER GENERAL PUBLIC LICENSE
2 |
3 | Version 3, 29 June 2007
4 |
5 | Copyright (C) 2007 Free Software Foundation, Inc.
6 |
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this
9 | license document, but changing it is not allowed.
10 |
11 | This version of the GNU Lesser General Public License incorporates the
12 | terms and conditions of version 3 of the GNU General Public License,
13 | supplemented by the additional permissions listed below.
14 |
15 | #### 0. Additional Definitions.
16 |
17 | As used herein, "this License" refers to version 3 of the GNU Lesser
18 | General Public License, and the "GNU GPL" refers to version 3 of the
19 | GNU General Public License.
20 |
21 | "The Library" refers to a covered work governed by this License, other
22 | than an Application or a Combined Work as defined below.
23 |
24 | An "Application" is any work that makes use of an interface provided
25 | by the Library, but which is not otherwise based on the Library.
26 | Defining a subclass of a class defined by the Library is deemed a mode
27 | of using an interface provided by the Library.
28 |
29 | A "Combined Work" is a work produced by combining or linking an
30 | Application with the Library. The particular version of the Library
31 | with which the Combined Work was made is also called the "Linked
32 | Version".
33 |
34 | The "Minimal Corresponding Source" for a Combined Work means the
35 | Corresponding Source for the Combined Work, excluding any source code
36 | for portions of the Combined Work that, considered in isolation, are
37 | based on the Application, and not on the Linked Version.
38 |
39 | The "Corresponding Application Code" for a Combined Work means the
40 | object code and/or source code for the Application, including any data
41 | and utility programs needed for reproducing the Combined Work from the
42 | Application, but excluding the System Libraries of the Combined Work.
43 |
44 | #### 1. Exception to Section 3 of the GNU GPL.
45 |
46 | You may convey a covered work under sections 3 and 4 of this License
47 | without being bound by section 3 of the GNU GPL.
48 |
49 | #### 2. Conveying Modified Versions.
50 |
51 | If you modify a copy of the Library, and, in your modifications, a
52 | facility refers to a function or data to be supplied by an Application
53 | that uses the facility (other than as an argument passed when the
54 | facility is invoked), then you may convey a copy of the modified
55 | version:
56 |
57 | - a) under this License, provided that you make a good faith effort
58 | to ensure that, in the event an Application does not supply the
59 | function or data, the facility still operates, and performs
60 | whatever part of its purpose remains meaningful, or
61 | - b) under the GNU GPL, with none of the additional permissions of
62 | this License applicable to that copy.
63 |
64 | #### 3. Object Code Incorporating Material from Library Header Files.
65 |
66 | The object code form of an Application may incorporate material from a
67 | header file that is part of the Library. You may convey such object
68 | code under terms of your choice, provided that, if the incorporated
69 | material is not limited to numerical parameters, data structure
70 | layouts and accessors, or small macros, inline functions and templates
71 | (ten or fewer lines in length), you do both of the following:
72 |
73 | - a) Give prominent notice with each copy of the object code that
74 | the Library is used in it and that the Library and its use are
75 | covered by this License.
76 | - b) Accompany the object code with a copy of the GNU GPL and this
77 | license document.
78 |
79 | #### 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that, taken
82 | together, effectively do not restrict modification of the portions of
83 | the Library contained in the Combined Work and reverse engineering for
84 | debugging such modifications, if you also do each of the following:
85 |
86 | - a) Give prominent notice with each copy of the Combined Work that
87 | the Library is used in it and that the Library and its use are
88 | covered by this License.
89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this
90 | license document.
91 | - c) For a Combined Work that displays copyright notices during
92 | execution, include the copyright notice for the Library among
93 | these notices, as well as a reference directing the user to the
94 | copies of the GNU GPL and this license document.
95 | - d) Do one of the following:
96 | - 0) Convey the Minimal Corresponding Source under the terms of
97 | this License, and the Corresponding Application Code in a form
98 | suitable for, and under terms that permit, the user to
99 | recombine or relink the Application with a modified version of
100 | the Linked Version to produce a modified Combined Work, in the
101 | manner specified by section 6 of the GNU GPL for conveying
102 | Corresponding Source.
103 | - 1) Use a suitable shared library mechanism for linking with
104 | the Library. A suitable mechanism is one that (a) uses at run
105 | time a copy of the Library already present on the user's
106 | computer system, and (b) will operate properly with a modified
107 | version of the Library that is interface-compatible with the
108 | Linked Version.
109 | - e) Provide Installation Information, but only if you would
110 | otherwise be required to provide such information under section 6
111 | of the GNU GPL, and only to the extent that such information is
112 | necessary to install and execute a modified version of the
113 | Combined Work produced by recombining or relinking the Application
114 | with a modified version of the Linked Version. (If you use option
115 | 4d0, the Installation Information must accompany the Minimal
116 | Corresponding Source and Corresponding Application Code. If you
117 | use option 4d1, you must provide the Installation Information in
118 | the manner specified by section 6 of the GNU GPL for conveying
119 | Corresponding Source.)
120 |
121 | #### 5. Combined Libraries.
122 |
123 | You may place library facilities that are a work based on the Library
124 | side by side in a single library together with other library
125 | facilities that are not Applications and are not covered by this
126 | License, and convey such a combined library under terms of your
127 | choice, if you do both of the following:
128 |
129 | - a) Accompany the combined library with a copy of the same work
130 | based on the Library, uncombined with any other library
131 | facilities, conveyed under the terms of this License.
132 | - b) Give prominent notice with the combined library that part of it
133 | is a work based on the Library, and explaining where to find the
134 | accompanying uncombined form of the same work.
135 |
136 | #### 6. Revised Versions of the GNU Lesser General Public License.
137 |
138 | The Free Software Foundation may publish revised and/or new versions
139 | of the GNU Lesser General Public License from time to time. Such new
140 | versions will be similar in spirit to the present version, but may
141 | differ in detail to address new problems or concerns.
142 |
143 | Each version is given a distinguishing version number. If the Library
144 | as you received it specifies that a certain numbered version of the
145 | GNU Lesser General Public License "or any later version" applies to
146 | it, you have the option of following the terms and conditions either
147 | of that published version or of any later version published by the
148 | Free Software Foundation. If the Library as you received it does not
149 | specify a version number of the GNU Lesser General Public License, you
150 | may choose any version of the GNU Lesser General Public License ever
151 | published by the Free Software Foundation.
152 |
153 | If the Library as you received it specifies that a proxy can decide
154 | whether future versions of the GNU Lesser General Public License shall
155 | apply, that proxy's public statement of acceptance of any version is
156 | permanent authorization for you to choose that version for the
157 | Library.
158 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/actions
2 |
3 | name: "CI"
4 |
5 | on:
6 | pull_request: null
7 | push:
8 | branches:
9 | - "master"
10 |
11 | env:
12 | PHP_EXTENSIONS: "dom filter gmp json libxml mbstring pcre phar reflection simplexml spl tokenizer xml xmlwriter"
13 |
14 | jobs:
15 | coding-standards:
16 | name: "Coding Standards"
17 | runs-on: "ubuntu-latest"
18 |
19 | strategy:
20 | matrix:
21 | php-version:
22 | - "8.4"
23 |
24 | dependencies:
25 | - "locked"
26 |
27 | steps:
28 | - name: "Checkout"
29 | uses: "actions/checkout@v2.3.4"
30 |
31 | - name: "Install PHP with extensions"
32 | uses: "shivammathur/setup-php@2.9.0"
33 | with:
34 | coverage: "none"
35 | extensions: "${{ env.PHP_EXTENSIONS }}"
36 | php-version: "${{ matrix.php-version }}"
37 |
38 | - name: "Validate composer.json and composer.lock"
39 | run: "composer validate"
40 |
41 | - name: "Determine composer cache directory"
42 | uses: "./.github/actions/composer/composer/determine-cache-directory"
43 |
44 | - name: "Cache dependencies installed with composer"
45 | uses: "actions/cache@v4.2.3"
46 | with:
47 | path: "${{ env.COMPOSER_CACHE_DIR }}"
48 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}"
49 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-"
50 |
51 | - name: "Install ${{ matrix.dependencies }} dependencies with composer"
52 | uses: "./.github/actions/composer/composer/install"
53 | with:
54 | dependencies: "${{ matrix.dependencies }}"
55 |
56 | - name: "Create cache directory for squizlabs/php_codesniffer"
57 | run: "mkdir -p .build/php_codesniffer"
58 |
59 | - name: "Cache cache directory for squizlabs/php_codesniffer"
60 | uses: "actions/cache@v4.2.3"
61 | with:
62 | path: ".build/php_codesniffer"
63 | key: "php-${{ matrix.php-version }}-php_codesniffer-${{ github.sha }}"
64 | restore-keys: "php-${{ matrix.php-version }}-php_codesniffer-"
65 |
66 | - name: "Run squizlabs/php_codesniffer"
67 | run: "vendor/bin/phpcs"
68 |
69 | static-code-analysis:
70 | name: "Static Code Analysis"
71 |
72 | runs-on: "ubuntu-latest"
73 |
74 | strategy:
75 | matrix:
76 | php-version:
77 | - "8.4"
78 |
79 | dependencies:
80 | - "locked"
81 |
82 | steps:
83 | - name: "Checkout"
84 | uses: "actions/checkout@v2.3.4"
85 |
86 | - name: "Install PHP with extensions"
87 | uses: "shivammathur/setup-php@2.9.0"
88 | with:
89 | coverage: "none"
90 | extensions: "${{ env.PHP_EXTENSIONS }}"
91 | php-version: "${{ matrix.php-version }}"
92 |
93 | - name: "Determine composer cache directory"
94 | uses: "./.github/actions/composer/composer/determine-cache-directory"
95 |
96 | - name: "Cache dependencies installed with composer"
97 | uses: "actions/cache@v4.2.3"
98 | with:
99 | path: "${{ env.COMPOSER_CACHE_DIR }}"
100 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}"
101 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-"
102 |
103 | - name: "Install ${{ matrix.dependencies }} dependencies with composer"
104 | uses: "./.github/actions/composer/composer/install"
105 | with:
106 | dependencies: "${{ matrix.dependencies }}"
107 |
108 | - name: "Create cache directory for vimeo/psalm"
109 | run: "mkdir -p .build/psalm"
110 |
111 | - name: "Cache cache directory for vimeo/psalm"
112 | uses: "actions/cache@v4.2.3"
113 | with:
114 | path: ".build/psalm"
115 | key: "php-${{ matrix.php-version }}-psalm-${{ github.sha }}"
116 | restore-keys: "php-${{ matrix.php-version }}-psalm-"
117 |
118 | - name: "Run vimeo/psalm"
119 | run: "vendor/bin/psalm --config=psalm.xml --diff --show-info=false --stats --threads=4"
120 |
121 | tests:
122 | name: "Tests"
123 |
124 | runs-on: "ubuntu-latest"
125 |
126 | env:
127 | COLLECT_COVERAGE_VERSION: "8.4"
128 |
129 | strategy:
130 | matrix:
131 | php-version:
132 | - "8.2"
133 | - "8.3"
134 | - "8.4"
135 |
136 | dependencies:
137 | - "lowest"
138 | - "highest"
139 |
140 | include:
141 | - php-version: "8.4"
142 | dependencies: "locked"
143 |
144 | steps:
145 | - name: "Checkout"
146 | uses: "actions/checkout@v2.3.4"
147 |
148 | - name: "Install PHP with extensions"
149 | uses: "shivammathur/setup-php@2.9.0"
150 | if: "!(matrix.php-version == env.COLLECT_COVERAGE_VERSION && matrix.dependencies == 'locked')"
151 | with:
152 | coverage: "none"
153 | extensions: "${{ env.PHP_EXTENSIONS }}"
154 | php-version: "${{ matrix.php-version }}"
155 |
156 | - name: "Install PHP with extensions and coverage"
157 | uses: "shivammathur/setup-php@2.9.0"
158 | if: "matrix.php-version == env.COLLECT_COVERAGE_VERSION && matrix.dependencies == 'locked'"
159 | with:
160 | coverage: "xdebug"
161 | extensions: "${{ env.PHP_EXTENSIONS }}"
162 | php-version: "${{ matrix.php-version }}"
163 |
164 | - name: "Set up problem matchers for phpunit/phpunit"
165 | run: "echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\""
166 |
167 | - name: "Determine composer cache directory"
168 | uses: "./.github/actions/composer/composer/determine-cache-directory"
169 |
170 | - name: "Cache dependencies installed with composer"
171 | uses: "actions/cache@v4.2.3"
172 | with:
173 | path: "${{ env.COMPOSER_CACHE_DIR }}"
174 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}"
175 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-"
176 |
177 | - name: "Install ${{ matrix.dependencies }} dependencies with composer"
178 | uses: "./.github/actions/composer/composer/install"
179 | with:
180 | dependencies: "${{ matrix.dependencies }}"
181 |
182 | - name: "PHPUnit"
183 | if: "!(matrix.php-version == env.COLLECT_COVERAGE_VERSION && matrix.dependencies == 'locked')"
184 | run: "vendor/bin/phpunit --configuration=phpunit.xml.dist"
185 |
186 | - name: "PHPUnit with code coverage"
187 | if: "matrix.php-version == env.COLLECT_COVERAGE_VERSION && matrix.dependencies == 'locked'"
188 | run: "vendor/bin/phpunit --configuration=phpunit.xml.dist --coverage-clover=.build/phpunit/logs/clover.xml"
189 |
190 | - name: "Send code coverage report to codeclimate.com"
191 | if: "matrix.php-version == env.COLLECT_COVERAGE_VERSION && matrix.dependencies == 'locked'"
192 | env:
193 | CC_TEST_REPORTER_ID: "${{ secrets.CC_TEST_REPORTER_ID }}"
194 | uses: "paambaati/codeclimate-action@v9.0.0"
195 | continue-on-error: true
196 | with:
197 | coverageLocations: "${{ github.workspace }}/.build/phpunit/logs/clover.xml"
198 |
199 | mutation-tests:
200 | name: "Mutation Tests"
201 |
202 | runs-on: "ubuntu-latest"
203 |
204 | strategy:
205 | matrix:
206 | php-version:
207 | - "8.4"
208 |
209 | dependencies:
210 | - "locked"
211 |
212 | steps:
213 | - name: "Checkout"
214 | uses: "actions/checkout@v2.3.4"
215 |
216 | - name: "Install PHP with extensions"
217 | uses: "shivammathur/setup-php@2.9.0"
218 | with:
219 | coverage: "xdebug"
220 | extensions: "${{ env.PHP_EXTENSIONS }}"
221 | php-version: "${{ matrix.php-version }}"
222 | tools: "infection"
223 |
224 | - name: "Determine composer cache directory"
225 | uses: "./.github/actions/composer/composer/determine-cache-directory"
226 |
227 | - name: "Cache dependencies installed with composer"
228 | uses: "actions/cache@v4.2.3"
229 | with:
230 | path: "${{ env.COMPOSER_CACHE_DIR }}"
231 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }}"
232 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-"
233 |
234 | - name: "Install ${{ matrix.dependencies }} dependencies with composer"
235 | uses: "./.github/actions/composer/composer/install"
236 | with:
237 | dependencies: "${{ matrix.dependencies }}"
238 |
239 | - name: "Run mutation tests with Xdebug and infection/infection"
240 | run: "infection --configuration=infection.json.dist"
241 | env:
242 | STRYKER_DASHBOARD_API_KEY: "${{ secrets.STRYKER_DASHBOARD_API_KEY }}"
243 |
--------------------------------------------------------------------------------