├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── classes └── Leth │ └── IPAddress │ ├── IP │ ├── Address.php │ ├── NetworkAddress.php │ └── NetworkAddressIterator.php │ ├── IPv4 │ ├── Address.php │ └── NetworkAddress.php │ └── IPv6 │ ├── Address.php │ └── NetworkAddress.php ├── composer.json ├── lgpl-3.0.txt ├── phpstan.neon ├── phpunit.xml.dist └── tests ├── IPAddressTest.php ├── IPNetworkAddressTest.php ├── IPv4AddressTest.php ├── IPv4NetworkAddressTest.php ├── IPv6AddressTest.php ├── IPv6NetworkAddressTest.php └── bootstrap.php /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | strategy: 8 | max-parallel: 3 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | php: ["8.1", "8.2", "8.3"] 12 | math_biginteger_mode: [INTERNAL, GMP, BCMATH] 13 | include: 14 | - os: ubuntu-latest 15 | phpstan: yes 16 | 17 | name: "PHP ${{ matrix.php }} (bigint mode: ${{ matrix.math_biginteger_mode }}) (OS: ${{ matrix.os }})" 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php }} 26 | extensions: xdebug, dom, gmp, bcmath 27 | coverage: xdebug 28 | env: 29 | fail-fast: true 30 | 31 | - name: Validate composer.json and composer.lock 32 | run: composer validate 33 | 34 | - name: Install dependencies 35 | run: composer install --prefer-dist --no-progress --no-suggest 36 | 37 | - name: Run test suite 38 | run: composer run-script test 39 | env: 40 | MATH_BIGINTEGER_MODE: ${{ matrix.math_biginteger_mode }} 41 | 42 | - if: ${{ matrix.phpstan }} 43 | uses: php-actions/phpstan@v3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | composer.phar 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.0] - 2019-02-15 4 | 5 | ### Added 6 | 7 | - PHP 8.1 support [@mrcnpdlk](https://github.com/mrcnpdlk). 8 | 9 | ### Changed 10 | 11 | - PHP 8.1 is now the minimum required version [@mrcnpdlk](https://github.com/mrcnpdlk). 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | lgpl-3.0.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-IPAddress 2 | 3 | A set of utility classes for working with IP addresses in PHP. 4 | Supports both IPv4 and IPv6 schemes. 5 | 6 | ### Requirements 7 | 8 | * PHP version 5.3.0 or greater. 9 | * The [PEAR](http://pear.php.net/) [Math_BigInteger](http://pear.php.net/package/Math_BigInteger/) class 10 | 11 | Required for add & subtract operations on IPv6 addresses, and finding IPs in IPv6 address blocks. 12 | 13 | ## Examples 14 | 15 | ```php 16 | get_network_address() . "\n"; 45 | // Prints '192.168.0.255' 46 | echo $net_addr->get_broadcast_address() . "\n"; 47 | // Prints '255.255.255.0' 48 | echo $net_addr->get_subnet_mask() . "\n"; 49 | 50 | /** 51 | * For each address of the specified network. 52 | */ 53 | $network = IPv4\NetworkAddress::factory('192.168.0.0/24'); 54 | foreach ($network as $ip) { 55 | // $ip is instance of IPv4\Address with value: 56 | // 192.168.0.0 57 | // 192.168.0.1 58 | // ... 59 | } 60 | 61 | $network = IPv4\NetworkAddress::factory('192.168.0.0/24'); 62 | // Prints '256' 63 | echo count($network); 64 | 65 | /** 66 | * Merge adjacent NetworkAddress blocks into larger blocks 67 | */ 68 | $small = array( 69 | IPv4\NetworkAddress::factory('192.168.0.0/24'), 70 | IPv4\NetworkAddress::factory('192.168.1.0/24') 71 | ); 72 | $merged = IP\NetworkAddress::merge($small); 73 | // Prints '1' 74 | echo count($merged); 75 | // Prints '1' 76 | echo $merged[0] == IP\NetworkAddress::factory('192.168.0.0/23'); 77 | 78 | /** 79 | * Get specified octet from IP 80 | */ 81 | $ipv4 = IP\Address::factory('192.168.1.102'); 82 | // Prints '102' 83 | echo $ipv4->get_octet(-1); 84 | // Prints '168' 85 | echo $ipv4[1]; 86 | 87 | $ipv6 = IP\Address::factory('2490::fa'); 88 | // Prints '250' 89 | echo $ipv6->get_octet(-1); 90 | // Prints '0' 91 | echo $ipv6[5]; 92 | ``` 93 | 94 | ## Test Cases 95 | 96 | To run the test cases, the following commands will do the trick: 97 | 98 | * No-frills tests: 99 | 100 | phpunit -c phpunit.xml.dist 101 | 102 | * Generate code coverage reports into './coverage/': 103 | 104 | phpunit -c phpunit.xml.dist --coverage-html coverage 105 | 106 | * With colours and verbose output: 107 | 108 | phpunit -c phpunit.xml.dist --colors --verbose 109 | 110 | * All together: 111 | 112 | phpunit -c phpunit.xml.dist --coverage-html coverage --colors --verbose 113 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IP/Address.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IP; 21 | use \Leth\IPAddress\IP, \Leth\IPAddress\IPv4, \Leth\IPAddress\IPv6; 22 | use ReturnTypeWillChange; 23 | 24 | /** 25 | * An abstract representation of an IP Address. 26 | * 27 | * @author Marcus Cobden 28 | */ 29 | abstract class Address implements \ArrayAccess 30 | { 31 | public const IP_VERSION = -1; 32 | public const FORMAT_FULL = 0; 33 | public const FORMAT_COMPACT = 1; 34 | 35 | /** 36 | * Internal representation of the address. Format may vary. 37 | * @var numeric 38 | */ 39 | protected $address; 40 | 41 | /** 42 | * Create an IP address object from the supplied address. 43 | * 44 | * @param IP\Address|int|string|\Math_BigInteger $address The address to represent. 45 | * 46 | * @return \Leth\IPAddress\IP\Address|\Leth\IPAddress\IPv4\Address|\Leth\IPAddress\IPv6\Address An instance of a subclass of IP\Address; either IPv4\Address or IPv6\Address 47 | */ 48 | public static function factory(IP\Address|int|string|\Math_BigInteger $address): IP\Address|Ipv4\Address|IPv6\Address 49 | { 50 | if ($address instanceof self) 51 | { 52 | return $address; 53 | } 54 | elseif (is_int($address) || (is_string($address) && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))) 55 | { 56 | return IPv4\Address::factory($address); 57 | } 58 | elseif ($address instanceof \Math_BigInteger || (is_string($address) && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) 59 | { 60 | return IPv6\Address::factory($address); 61 | } 62 | else 63 | { 64 | throw new \InvalidArgumentException('Unable to guess IP address type from \''.$address.'\'.'); 65 | } 66 | } 67 | 68 | /** 69 | * Compare 2 IP Address objects. 70 | * 71 | * This method is a wrapper for the compare_to method and is useful in callback situations, e.g. 72 | * usort($addresses, array('IP\Address', 'compare')); 73 | * 74 | * @param IP\Address $a The left hand side of the comparison. 75 | * @param IP\Address $b The right hand side of the comparison. 76 | * @return int The result of the comparison. 77 | */ 78 | public static function compare(IP\Address $a, IP\Address $b): int 79 | { 80 | return $a->compare_to($b); 81 | } 82 | 83 | /** 84 | * Create a new IP Address object. 85 | * 86 | * @param int|string $address The address to represent. 87 | */ 88 | protected function __construct(int|string $address) 89 | { 90 | $this->address = $address; 91 | } 92 | 93 | /** 94 | * Add the given value to this address. 95 | * 96 | * @param integer|\Math_BigInteger $value 97 | * @return IP\Address An address representing the result of the operation. 98 | */ 99 | abstract public function add($value): Address; 100 | 101 | /** 102 | * Subtract the given value from this address. 103 | * 104 | * @param integer|\Math_BigInteger $value 105 | * @return IP\Address An address representing the result of the operation. 106 | */ 107 | abstract public function subtract($value): Address; 108 | 109 | /** 110 | * Compute the bitwise AND of this address and another. 111 | * 112 | * @param IP\Address $other The other operand. 113 | * @return IP\Address An address representing the result of the operation. 114 | */ 115 | abstract public function bitwise_and(IP\Address $other); 116 | 117 | /** 118 | * Compute the bitwise OR of this address and another. 119 | * 120 | * @param IP\Address $other The other operand. 121 | * @return IP\Address An address representing the result of the operation. 122 | */ 123 | abstract public function bitwise_or(IP\Address $other): Address; 124 | 125 | /** 126 | * Compute the bitwise XOR (Exclusive OR) of this address and another. 127 | * 128 | * @param IP\Address $other The other operand. 129 | * @return IP\Address An address representing the result of the operation. 130 | */ 131 | abstract public function bitwise_xor(IP\Address $other): Address; 132 | 133 | /** 134 | * Compute the bitwise NOT of this address. 135 | * 136 | * @return IP\Address An address representing the result of the operation. 137 | */ 138 | abstract public function bitwise_not(): Address; 139 | 140 | /** 141 | * Compare this IP Address with another. 142 | * 143 | * @param IP\Address $other The instance to compare to. 144 | * @return int The result of the comparison. 145 | */ 146 | abstract public function compare_to(IP\Address $other): int; 147 | 148 | /** 149 | * Convert this object to a string representation 150 | * 151 | * @return string This IP address expressed as a string. 152 | */ 153 | public function __toString(): string 154 | { 155 | return $this->format(self::FORMAT_COMPACT); 156 | } 157 | 158 | /** 159 | * Return the string representation of the address 160 | * 161 | * @return string This IP address expressed as a string. 162 | */ 163 | abstract public function format(int $mode): string; 164 | 165 | /** 166 | * Check that this instance and the supplied instance are of the same class. 167 | * 168 | * @param IP\Address $other The object to check. 169 | * @throws \InvalidArgumentException if objects are of the same class. 170 | */ 171 | protected function check_types(IP\Address $other): void 172 | { 173 | if (get_class($this) !== get_class($other)) { 174 | throw new \InvalidArgumentException('Incompatible types.'); 175 | } 176 | } 177 | 178 | /** 179 | * Get the specified octet from this address. 180 | * 181 | * @param integer $number 182 | * 183 | * @return ?integer An octet value the result of the operation. 184 | */ 185 | public function get_octet(int $number): ?int 186 | { 187 | $address = unpack("C*", $this->address); 188 | $index = (($number >= 0) ? $number : count($address) + $number); 189 | $index++; 190 | 191 | return $address[$index] ?? null; 192 | } 193 | 194 | /** 195 | * Whether octet index in allowed range 196 | * 197 | * @param integer $offset 198 | * @return boolean 199 | */ 200 | public function offsetExists($offset): bool 201 | { 202 | return ($this->get_octet($offset) !== NULL); 203 | } 204 | 205 | /** 206 | * Get the octet value from index 207 | * 208 | * @param integer $offset 209 | * 210 | * @return integer|null 211 | */ 212 | public function offsetGet($offset): ?int 213 | { 214 | return $this->get_octet($offset); 215 | } 216 | 217 | /** 218 | * Operation unsupported 219 | * 220 | * @param integer $offset 221 | * @param mixed $value 222 | * @throws \LogicException 223 | */ 224 | #[ReturnTypeWillChange] 225 | public function offsetSet($offset, $value): mixed 226 | { 227 | throw new \LogicException('Operation unsupported'); 228 | } 229 | 230 | /** 231 | * Operation unsupported 232 | * 233 | * @param integer $offset 234 | * @throws \LogicException 235 | */ 236 | #[ReturnTypeWillChange] 237 | public function offsetUnset($offset): void 238 | { 239 | throw new \LogicException('Operation unsupported'); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IP/NetworkAddress.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IP; 21 | use \Leth\IPAddress\IP, \Leth\IPAddress\IPv4, \Leth\IPAddress\IPv6; 22 | use ReturnTypeWillChange; 23 | 24 | /** 25 | * An abstract representation of an IP Address in a given network 26 | * 27 | * @package default 28 | * @author Marcus Cobden 29 | */ 30 | abstract class NetworkAddress implements \IteratorAggregate, \Countable 31 | { 32 | public const IP_VERSION = -1; 33 | public const MAX_SUBNET = -1; 34 | 35 | /** 36 | * The IP Address 37 | * 38 | * @var IP\Address 39 | */ 40 | protected IP\Address $address; 41 | 42 | /** 43 | * The CIDR number 44 | * 45 | * @var int 46 | */ 47 | protected int $cidr; 48 | 49 | /** 50 | * Generates the subnet mask for a given CIDR 51 | * 52 | * @param int $cidr The CIDR number 53 | * @return IP\Address An IP address representing the mask. 54 | */ 55 | public static function generate_subnet_mask(int $cidr): Address 56 | { 57 | throw new \LogicException(__METHOD__.' not implemented in subclass of '.__CLASS__); 58 | } 59 | 60 | /** 61 | * Gets the Global subnet mask for this IP Protocol 62 | * 63 | * @return IP\Address An IP Address representing the mask. 64 | * @author Marcus Cobden 65 | */ 66 | public static function get_global_netmask(): Address 67 | { 68 | throw new \LogicException(__METHOD__.' not implemented in subclass of '.__CLASS__); 69 | } 70 | 71 | /** 72 | * Creates an IP\NetworkAddress for the supplied string 73 | * 74 | * @param \Leth\IPAddress\IP\NetworkAddress|\Leth\IPAddress\IP\Address|string $address IP Network Address string. 75 | * @param int|string|null $cidr Optional CIDR number. If not supplied It is assumed to be part of the address string 76 | * 77 | * @return \Leth\IPAddress\IPv4\NetworkAddress|\Leth\IPAddress\IP\NetworkAddress|\Leth\IPAddress\IPv6\NetworkAddress 78 | */ 79 | public static function factory(NetworkAddress|Address|string $address, int|string|null $cidr = NULL): IPv4\NetworkAddress|NetworkAddress|IPv6\NetworkAddress 80 | { 81 | if ($address instanceof self) 82 | { 83 | if ($cidr !== NULL && $cidr !== $address->cidr) 84 | { 85 | $class = get_class($address); 86 | return new $class($address->address, $cidr); 87 | } 88 | return $address; 89 | } 90 | 91 | if(is_string($address)){ 92 | $parts = explode('/', $address, 2); 93 | if (count($parts) === 2) { 94 | if ($cidr === NULL) 95 | // Parse CIDR from $address variable because $cidr is null 96 | { 97 | [$address, $cidr] = $parts; 98 | } 99 | else 100 | // Ignore CIDR into $address variable 101 | { 102 | [$address] = $parts; 103 | } 104 | } 105 | } 106 | 107 | if (is_string($cidr)) 108 | { 109 | if ( ! ctype_digit($cidr)) { 110 | throw new \InvalidArgumentException("Malformed CIDR suffix '$cidr'."); 111 | } 112 | 113 | $cidr = (int)$cidr; 114 | } 115 | 116 | if ( ! $address instanceof IP\Address) 117 | { 118 | $address = IP\Address::factory($address); 119 | } 120 | 121 | if ($address instanceof IPv4\Address) { 122 | return new IPv4\NetworkAddress($address, $cidr); 123 | } 124 | elseif ($address instanceof IPv6\Address) { 125 | return new IPv6\NetworkAddress($address, $cidr); 126 | } 127 | else { 128 | throw new \InvalidArgumentException('Unsupported IP Address type \'' . get_class($address) . '\'.'); 129 | } 130 | } 131 | 132 | /** 133 | * Compare 2 IP Network Address objects. 134 | * 135 | * This method is a wrapper for the compare_to method and is useful in callback situations, e.g. 136 | * usort($addresses, array('IP\NetworkAddress', 'compare')); 137 | * 138 | * @param \Leth\IPAddress\IP\NetworkAddress $a The left hand side of the comparison. 139 | * @param \Leth\IPAddress\IP\NetworkAddress $b The right hand side of the comparison. 140 | * 141 | * @return int The result of the comparison. 142 | */ 143 | public static function compare(IP\NetworkAddress $a, IP\NetworkAddress $b): int 144 | { 145 | return $a->compare_to($b); 146 | } 147 | 148 | /** 149 | * Merge adjacent network blocks 150 | * 151 | * Ajacent blocks can only be merged if they belong to the same parent block 152 | * 153 | * @param array $network_addresses NetworkAddresses to merge 154 | * @return array NetworkAddresses remaining after merging 155 | */ 156 | public static function merge(array $network_addresses): array 157 | { 158 | $net_addr_index = array(); 159 | foreach ($network_addresses as $net_addr) { 160 | // Ensure sure we're only dealing with network identifiers 161 | $net_addr = $net_addr->get_network_identifier(); 162 | $net_addr_index[$net_addr::IP_VERSION][$net_addr->cidr][] = $net_addr; 163 | } 164 | // We're done with this structure now 165 | unset($network_addresses); 166 | 167 | $out = array(); 168 | foreach ($net_addr_index as $version => $cidr_addrs) 169 | { 170 | $max = $version === 4 ? IPv4\NetworkAddress::MAX_SUBNET : IPv6\NetworkAddress::MAX_SUBNET; 171 | // smallest networks first (largest cidr) 172 | // We have to loop by index because we modify the array while we iterate 173 | for ($cidr = $max; $cidr > 0; $cidr--) 174 | { 175 | if (! array_key_exists($cidr, $cidr_addrs)) 176 | continue; 177 | $net_addrs = $cidr_addrs[$cidr]; 178 | if (count($net_addrs) === 1) 179 | { 180 | $out[] = $net_addrs[0]; 181 | continue; 182 | } 183 | usort($net_addrs, array(__CLASS__, 'compare')); 184 | 185 | $last_added = NULL; 186 | $a = $b = NULL; 187 | for ($i = 0; $i < count($net_addrs) - 1; $i++) { 188 | $a = $net_addrs[$i]; 189 | $b = $net_addrs[$i + 1]; 190 | if ($a->compare_to($b) === 0) 191 | continue; 192 | $parent = $a->get_parent(); 193 | if ($parent->compare_to($b->get_parent()) === 0) 194 | { 195 | $cidr_addrs[$parent->cidr][] = $parent; 196 | $last_added = $b; 197 | } 198 | elseif($a !== $last_added) 199 | { 200 | $out[] = $a; 201 | $last_added = $a; 202 | } 203 | } 204 | if ($last_added === NULL || ($last_added !== $b && $last_added->compare_to($b) !== 0)) 205 | $out[] = $b; 206 | // We're done with these, remove them to allow GC 207 | unset($cidr_addrs[$cidr]); 208 | } 209 | } 210 | return $out; 211 | } 212 | 213 | /** 214 | * Construct an IP\NetworkAddress. 215 | * 216 | * @param \Leth\IPAddress\IP\Address $address The IP Address of the host 217 | * @param mixed|null $cidr The CIDR size of the network 218 | */ 219 | protected function __construct(IP\Address $address, mixed $cidr) 220 | { 221 | // Default CIDR equal single host 222 | if ($cidr === NULL) { 223 | $cidr = static::MAX_SUBNET; 224 | } 225 | if ( ! is_int($cidr) || $cidr < 0 || $cidr > static::MAX_SUBNET) { 226 | throw new \InvalidArgumentException("Invalid CIDR '.$cidr'.Invalid type or out of range for class " . get_class($this) . "."); 227 | } 228 | 229 | $this->address = $address; 230 | $this->cidr = $cidr; 231 | } 232 | 233 | public function get_address(): Address 234 | { 235 | return $this->address; 236 | } 237 | 238 | public function get_cidr(): int 239 | { 240 | return $this->cidr; 241 | } 242 | 243 | /** 244 | * Get the NetworkAddress immediately enclosing this one 245 | * 246 | * @return \Leth\IPAddress\IPv4\NetworkAddress|\Leth\IPAddress\IP\NetworkAddress|\Leth\IPAddress\IPv6\NetworkAddress|null 247 | */ 248 | public function get_parent(): IPv4\NetworkAddress|\Leth\IPAddress\IP\NetworkAddress|IPv6\NetworkAddress|null 249 | { 250 | if ($this->cidr === 0) { 251 | return null; 252 | } 253 | $parent_cidr = $this->cidr - 1; 254 | $parent_addr = $this->address->bitwise_and(static::generate_subnet_mask($parent_cidr)); 255 | return static::factory($parent_addr, $parent_cidr); 256 | } 257 | 258 | /** 259 | * Calculates the first address in this subnet. 260 | * 261 | * @return IP\Address 262 | */ 263 | public function get_network_start(): Address 264 | { 265 | return $this->address->bitwise_and($this->get_subnet_mask()); 266 | } 267 | 268 | /** 269 | * Calculates the last address in this subnet. 270 | * 271 | * @return IP\Address 272 | */ 273 | public function get_network_end(): Address 274 | { 275 | return $this->get_subnet_mask()->bitwise_not()->bitwise_or($this->address); 276 | } 277 | 278 | /** 279 | * Calculates the number of address in this subnet. 280 | * 281 | * @return integer 282 | */ 283 | public function get_NetworkAddress_count(): int 284 | { 285 | return 2 ** (static::MAX_SUBNET - $this->cidr); 286 | } 287 | 288 | public function get_address_in_network(int|\Math_BigInteger $offset, bool $from_start = NULL): Address 289 | { 290 | $positive = false; 291 | if (is_int($offset)) 292 | { 293 | $positive = ($offset >= 0); 294 | } 295 | elseif ($offset instanceOf \Math_BigInteger) 296 | { 297 | $positive = ($offset->compare(new \Math_BigInteger(0)) >= 0); 298 | } 299 | if ($from_start === NULL) 300 | { 301 | $from_start = $positive; 302 | } 303 | else 304 | { 305 | $from_start = ($from_start === TRUE); 306 | } 307 | 308 | if ($from_start) 309 | { 310 | $point = $this->get_network_start(); 311 | } 312 | else 313 | { 314 | $point = $this->get_network_end(); 315 | } 316 | 317 | if ( ! $positive) 318 | { 319 | if (is_int($offset)) 320 | { 321 | $offset = (int)abs($offset); 322 | } 323 | elseif ($offset instanceOf \Math_BigInteger) 324 | { 325 | $offset = $offset->abs(); 326 | } 327 | } 328 | 329 | if ($positive && $from_start) { 330 | return $point->add($offset); 331 | } 332 | else { 333 | return $point->subtract($offset); 334 | } 335 | } 336 | 337 | /** 338 | * Checks whether this is a Network Identifier 339 | * 340 | * @return boolean 341 | */ 342 | public function is_network_identifier(): bool 343 | { 344 | return $this->address->compare_to($this->get_network_start()) === 0; 345 | } 346 | 347 | /** 348 | * Get the Network Identifier for the network this address is in. 349 | * 350 | * @return IP\NetworkAddress 351 | */ 352 | public function get_network_identifier(): NetworkAddress 353 | { 354 | $classname = get_class($this); 355 | return new $classname($this->get_network_start(), $this->cidr); 356 | } 357 | 358 | /** 359 | * Get the subnet mask for this network 360 | * 361 | * @return IP\Address 362 | */ 363 | public function get_subnet_mask(): Address 364 | { 365 | return static::generate_subnet_mask($this->cidr); 366 | } 367 | 368 | /** 369 | * Calculates whether two subnets share any portion of their address space. 370 | * 371 | * @param \Leth\IPAddress\IP\NetworkAddress $other The other subnet to compare to. 372 | * 373 | * @return bool 374 | */ 375 | public function shares_subnet_space(IP\NetworkAddress $other): bool 376 | { 377 | $this->check_types($other); 378 | 379 | $first = $this; 380 | 381 | if ($this->cidr > $other->cidr) 382 | { 383 | [$first, $other] = array($other, $first); 384 | } 385 | 386 | return 387 | ($first->get_network_start()->compare_to($other->get_network_start()) <= 0) 388 | && 389 | ($first->get_network_end() ->compare_to($other->get_network_end() ) >= 0); 390 | } 391 | 392 | /** 393 | * Checks whether this subnet encloses the supplied subnet. 394 | * 395 | * @param \Leth\IPAddress\IP\NetworkAddress $other Subnet to test against. 396 | * 397 | * @return boolean 398 | */ 399 | public function encloses_subnet(IP\NetworkAddress $other): bool 400 | { 401 | $this->check_types($other); 402 | 403 | if ($this->cidr > $other->cidr) 404 | return FALSE; 405 | 406 | return $this->shares_subnet_space($other); 407 | } 408 | 409 | /** 410 | * Checks whether the supplied IP fits within this subnet. 411 | * 412 | * @param IP\Address $ip IP to test against. 413 | * @return boolean 414 | */ 415 | public function encloses_address(IP\Address $ip) 416 | { 417 | $this->check_IP_version($ip); 418 | 419 | return 420 | ($this->get_network_start()->compare_to($ip) <= 0) 421 | AND 422 | ($this->get_network_end() ->compare_to($ip) >= 0); 423 | } 424 | 425 | /** 426 | * Check that this and the argument are of the same type. 427 | * 428 | * @param IP\NetworkAddress $other The object to check. 429 | * @return void 430 | * @throws \InvalidArgumentException If they are not of the same type. 431 | */ 432 | protected function check_types($other) 433 | { 434 | if (get_class($this) != get_class($other)) 435 | throw new \InvalidArgumentException('Incompatible types.'); 436 | } 437 | 438 | /** 439 | * Check that this and the argument are of the same IP protocol version 440 | * 441 | * @param IP\Address $other 442 | * @return void 443 | * @throws \InvalidArgumentException If they are not of the same type. 444 | */ 445 | protected function check_IP_version(IP\Address $other): void 446 | { 447 | if ($other::IP_VERSION !== static::IP_VERSION) { 448 | throw new \InvalidArgumentException("Incompatible types ('" . get_class($this) . "' and '" . get_class($other) . "')."); 449 | } 450 | } 451 | 452 | /** 453 | * Compare this instance to another IP\NetworkAddress 454 | * 455 | * @param IP\NetworkAddress $other The instance to compare to 456 | * @return integer 457 | */ 458 | public function compare_to(IP\NetworkAddress $other) 459 | { 460 | $cmp = $this->address->compare_to($other->address); 461 | 462 | if ($cmp == 0) 463 | { 464 | $cmp = $this->cidr - $other->cidr; 465 | } 466 | 467 | return $cmp; 468 | } 469 | 470 | /** 471 | * Provides a string representation of this object 472 | * 473 | * @return string 474 | */ 475 | public function __toString() 476 | { 477 | return $this->address.'/'.$this->cidr; 478 | } 479 | 480 | /** 481 | * Find a block of a given size within the smallest network address among the blocks given 482 | * 483 | * @param array $blocks An array of network addresses to search in. 484 | * @param integer $block_size The desired network block size 485 | * 486 | * @return array(IP\NetworkAddress found, IP\NetworkAddress within), or array(NULL, NULL) if none found. 487 | */ 488 | public static function get_block_in_smallest(array $blocks, int $block_size): array 489 | { 490 | $smallest = NULL; 491 | $smallest_cidr = 0; 492 | 493 | foreach ($blocks as $block) 494 | { 495 | $cidr = $block->get_cidr(); 496 | if ($cidr === $block_size) 497 | { 498 | return array($block, $block); 499 | } 500 | elseif ($cidr > $block_size) 501 | { 502 | continue; 503 | } 504 | elseif ($cidr > $smallest_cidr) 505 | { 506 | $smallest = $block; 507 | $smallest_cidr = $block->get_cidr(); 508 | } 509 | } 510 | 511 | if ($smallest) { 512 | return [static::factory($smallest, $block_size), $smallest]; 513 | } else { 514 | return [null, null]; 515 | } 516 | } 517 | 518 | /** 519 | * Find the portion of this network address block that does not overlap with the given blocks. 520 | * 521 | * @param array $excluding An array of network addresses to exclude, each of type IP\NetworkAddress 522 | * @return array 523 | */ 524 | public function excluding(array $excluding): array 525 | { 526 | $candidates = array($this); 527 | foreach ($excluding as $exclude) 528 | { 529 | $stack = $candidates; 530 | $candidates = array(); 531 | 532 | while ( ! empty($stack)) 533 | { 534 | $candidate = array_shift($stack); 535 | 536 | // Null == ok, TRUE == split, FALSE == excluded 537 | $split = NULL; 538 | if ($candidate->shares_subnet_space($exclude)) 539 | { 540 | $split = ($candidate->cidr < $exclude->cidr); 541 | } 542 | if ($split === TRUE) 543 | { 544 | $stack = array_merge($candidate->split(), $stack); 545 | } 546 | elseif ($split === NULL) 547 | { 548 | $candidates[] = $candidate; 549 | } 550 | } 551 | 552 | if (empty($candidates)) 553 | break; 554 | } 555 | return $candidates; 556 | } 557 | 558 | /** 559 | * Split the network address to create 2^n network addresses. 560 | * 561 | * @param int $times The number of times to split the network address 562 | * 563 | * @return array 564 | */ 565 | public function split(int $times = 1): array 566 | { 567 | if (0 === $times) 568 | return array($this); 569 | 570 | $new_cidr = $this->cidr + $times; 571 | $shift = static::MAX_SUBNET - $new_cidr; 572 | if ($shift < 0) 573 | throw new \InvalidArgumentException('Cannot split beyond smallest subnet size'); 574 | 575 | $one = new \Math_BigInteger(1); 576 | $offset = $one->bitwise_leftShift($shift); 577 | 578 | $out = array(); 579 | $pos = $this->address; 580 | for ($i=0; $i < (2 ** $times); $i++) 581 | { 582 | $out[] = static::factory($pos, $new_cidr); 583 | $pos = $pos->add($offset); 584 | } 585 | 586 | return $out; 587 | } 588 | 589 | /** 590 | * Get iterator for this network 591 | * Implement \IteratorAggregate 592 | * 593 | * @return NetworkAddressIterator 594 | */ 595 | #[ReturnTypeWillChange] 596 | public function getIterator(): NetworkAddressIterator 597 | { 598 | return new NetworkAddressIterator($this); 599 | } 600 | 601 | /** 602 | * Get array of addresses in this network 603 | * 604 | * Warning: May use a lot of memory if used with large networks. 605 | * Consider using an iterator and the count() method instead. 606 | * @return array 607 | */ 608 | public function toArray() 609 | { 610 | return iterator_to_array($this, false); 611 | } 612 | 613 | /** 614 | * Get count addresses in this network 615 | * Implement \Countable 616 | * 617 | * @return integer 618 | */ 619 | public function count(): int 620 | { 621 | return $this->get_NetworkAddress_count(); 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IP/NetworkAddressIterator.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IP; 21 | use \Leth\IPAddress\IP; 22 | 23 | class NetworkAddressIterator implements \Iterator 24 | { 25 | /** 26 | * The network of iterator 27 | * 28 | * @var IP\NetworkAddress 29 | */ 30 | protected $network; 31 | 32 | /** 33 | * The position of iterator 34 | * 35 | * @var IP\Address 36 | */ 37 | protected $position; 38 | 39 | public function __construct(NetworkAddress $network) 40 | { 41 | $this->network = $network; 42 | $this->rewind(); 43 | } 44 | 45 | /** 46 | * Set the pointer of iterator to a first network address 47 | * Implement \Iterator 48 | * 49 | * @return void 50 | */ 51 | public function rewind(): void 52 | { 53 | $this->position = $this->network->get_network_start(); 54 | } 55 | 56 | /** 57 | * Get the value from iterator 58 | * Implement \Iterator 59 | * 60 | * @return IP\Address 61 | */ 62 | public function current(): IP\Address 63 | { 64 | return $this->position; 65 | } 66 | 67 | /** 68 | * Get the key from iterator 69 | * Implement \Iterator 70 | * 71 | * @return string 72 | */ 73 | public function key(): string 74 | { 75 | return $this->position->__toString(); 76 | } 77 | 78 | /** 79 | * Move the pointer of iterator to a next network address 80 | * Implement \Iterator 81 | * 82 | * @return void 83 | */ 84 | public function next(): void 85 | { 86 | $this->position = $this->position->add(1); 87 | } 88 | 89 | /** 90 | * Next network address is valid 91 | * Implement \Iterator 92 | * 93 | * @return boolean 94 | */ 95 | public function valid(): bool 96 | { 97 | return ($this->position->compare_to($this->network->get_network_end()) <= 0); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IPv4/Address.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IPv4; 21 | use \Leth\IPAddress\IP, \Leth\IPAddress\IPv4, \Leth\IPAddress\IPv6; 22 | 23 | class Address extends IP\Address 24 | { 25 | public const IP_VERSION = 4; 26 | public const MAX_IP = '255.255.255.255'; 27 | public const FORMAT_INTEGER = 3; 28 | 29 | public static function factory(IP\Address|int|string|\Math_BigInteger $address): IPv4\Address 30 | { 31 | if ($address instanceof self) 32 | { 33 | return $address; 34 | } 35 | elseif (is_string($address)) 36 | { 37 | $tmp = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); 38 | if ($tmp === FALSE) { 39 | throw new \InvalidArgumentException("'$address' is not a valid IPv4 Address"); 40 | } 41 | 42 | $address = static::_pack(ip2long($address)); 43 | } 44 | elseif ($address instanceOf \Math_BigInteger) 45 | { 46 | if ($address->compare(new \Math_BigInteger(pack('N', ip2long(static::MAX_IP)), 256)) > 0) { 47 | throw new \InvalidArgumentException("IP value out of range."); 48 | } 49 | 50 | $address = str_pad($address->toBytes(), 4, chr(0), STR_PAD_LEFT); 51 | } 52 | elseif (is_int($address)) 53 | { 54 | $address = static::_pack($address); 55 | } 56 | else 57 | { 58 | throw new \InvalidArgumentException("Unsupported argument type."); 59 | } 60 | 61 | return new IPv4\Address($address); 62 | } 63 | 64 | protected function __construct($address) 65 | { 66 | parent::__construct($address); 67 | } 68 | 69 | protected static function _pack(int $address): string 70 | { 71 | return pack('N', $address); 72 | } 73 | 74 | protected static function _unpack(string $address): int 75 | { 76 | $out = unpack('N', $address); 77 | return $out[1]; 78 | } 79 | 80 | public function add($value): Address 81 | { 82 | if ($value instanceof \Math_BigInteger) 83 | { 84 | $value = (int)(string)$value; 85 | } 86 | return new IPv4\Address(static::_pack(static::_unpack($this->address) + $value)); 87 | } 88 | 89 | public function subtract($value): Address 90 | { 91 | if ($value instanceof \Math_BigInteger) 92 | { 93 | $value = (int)(string)$value; 94 | } 95 | return new IPv4\Address(static::_pack(static::_unpack($this->address) - $value)); 96 | } 97 | 98 | /** 99 | * Calculates the Bitwise & (AND) of a given IP address. 100 | * @param IPv4\Address $other is the ip to be compared against 101 | */ 102 | public function bitwise_and(IP\Address $other): Address 103 | { 104 | $this->check_types($other); 105 | return new IPv4\Address($this->address & $other->address); 106 | } 107 | 108 | /** 109 | * Calculates the Bitwise | (OR) of a given IP address. 110 | * @param IP\Address $other is the ip to be compared against 111 | * @return IPv4\Address 112 | */ 113 | public function bitwise_or(IP\Address $other): IPv4\Address 114 | { 115 | $this->check_types($other); 116 | return new IPv4\Address($this->address | $other->address); 117 | } 118 | 119 | /** 120 | * Calculates the Bitwise ^ (XOR) of a given IP address. 121 | * @param IP\Address $other is the ip to be compared against 122 | * @return IPv4\Address 123 | */ 124 | public function bitwise_xor(IP\Address $other): IPv4\Address 125 | { 126 | $this->check_types($other); 127 | return new IPv4\Address($this->address ^ $other->address); 128 | } 129 | 130 | /** 131 | * Calculates the Bitwise ~ (NOT) of a given IP address. 132 | * @return IPv4\Address 133 | */ 134 | public function bitwise_not(): Address 135 | { 136 | return new IPv4\Address(~ $this->address); 137 | } 138 | 139 | /** 140 | * Creates a IPv6 address object representing the 'IPv4-Mapped' IPv6 address of this object 141 | * 142 | * @return IPv6\Address 143 | */ 144 | public function as_IPv6_address(): IPv6\Address 145 | { 146 | list( , $address) = unpack('H*', $this->address); 147 | $address = join(':', str_split($address, 4)); 148 | $address = '::ffff:'.$address; 149 | 150 | return IPv6\Address::factory($address); 151 | } 152 | 153 | public function compare_to(IP\Address $other): int 154 | { 155 | $this->check_types($other); 156 | 157 | if ($this->address < $other->address) { 158 | return -1; 159 | } 160 | elseif ($this->address > $other->address) { 161 | return 1; 162 | } 163 | else { 164 | return 0; 165 | } 166 | } 167 | 168 | public function format(int $mode): string 169 | { 170 | $address = static::_unpack($this->address); 171 | switch ($mode) { 172 | case static::FORMAT_INTEGER: 173 | return sprintf('%u', $address); 174 | case IP\Address::FORMAT_COMPACT: 175 | return long2ip($address); 176 | case IP\Address::FORMAT_FULL: 177 | $parts = explode('.', long2ip($address)); 178 | foreach ($parts as $i => $octet) { 179 | $parts[$i] = str_pad($octet, 3, '0', STR_PAD_LEFT); 180 | } 181 | return implode('.', $parts); 182 | default: 183 | throw new \InvalidArgumentException('Unsupported format mode: '.$mode); 184 | } 185 | } 186 | 187 | 188 | } 189 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IPv4/NetworkAddress.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IPv4; 21 | use \Leth\IPAddress\IPv4; 22 | 23 | class NetworkAddress extends \Leth\IPAddress\IP\NetworkAddress 24 | { 25 | public const IP_VERSION = 4; 26 | public const MAX_SUBNET = 32; 27 | 28 | public static function generate_subnet_mask(int $cidr): IPv4\Address 29 | { 30 | $mask = 0; 31 | // left shift operates over arch-specific integer sizes, 32 | // so we have to special case 32 bit shifts 33 | if ($cidr > 0) 34 | { 35 | $mask = (~$mask) << (static::MAX_SUBNET - $cidr); 36 | } 37 | 38 | return IPv4\Address::factory(implode('.', unpack('C4', pack('N', $mask)))); 39 | } 40 | 41 | /** 42 | * Gets the Global subnet mask for this IP Protocol 43 | * @return IPv4\Address An IP Address representing the mask. 44 | * 45 | * @author Marcus Cobden 46 | */ 47 | public static function get_global_netmask(): Address 48 | { 49 | return static::generate_subnet_mask(static::MAX_SUBNET); 50 | } 51 | 52 | /** 53 | * Calculates the Network Address for this address (IPv4) or the first ip of the subnet (IPv6) 54 | * 55 | */ 56 | public function get_NetworkAddress(): \Leth\IPAddress\IP\Address 57 | { 58 | return $this->get_network_start(); 59 | } 60 | 61 | public function get_network_class(): string 62 | { 63 | if ($this->cidr > 24) 64 | { 65 | return '1/'. (2 ** ($this->cidr - 24)) .' C'; 66 | } 67 | elseif ($this->cidr > 16) 68 | { 69 | return (2 ** (24 - $this->cidr)) .' C'; 70 | 71 | } 72 | elseif ($this->cidr > 8) 73 | { 74 | return (2 ** (16 - $this->cidr)) .' B'; 75 | } 76 | else 77 | { 78 | return (2 ** (8 - $this->cidr)) .' A'; 79 | } 80 | } 81 | 82 | /** 83 | * Calculates the Broadcast Address for this address. 84 | * 85 | */ 86 | public function get_broadcast_address(): \Leth\IPAddress\IP\Address 87 | { 88 | return $this->get_network_end(); 89 | } 90 | 91 | // TODO Check this 92 | // public function as_IPv6\NetworkAddress() 93 | // { 94 | // $address = $this->address->as_IPv6\address(); 95 | // $cidr = (IPv6\NetworkAddress::MAX_SUBNET - IPv4\NetworkAddress::MAX_SUBNET) + $this->cidr; 96 | // return new IPv6\NetworkAddress($address, $cidr); 97 | // } 98 | } 99 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IPv6/Address.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IPv6; 21 | use \Leth\IPAddress\IP, \Leth\IPAddress\IPv6, \Leth\IPAddress\IPv4; 22 | 23 | class Address extends IP\Address 24 | { 25 | public const IP_VERSION = 6; 26 | public const FORMAT_ABBREVIATED = 2; 27 | public const FORMAT_MAPPED_IPV4 = 3; 28 | // format mapped v4 if possible, else compact 29 | public const FORMAT_MAY_MAPPED_COMPACT = 4; 30 | 31 | public static function factory(IP\Address|int|string|\Math_BigInteger $address): IPv6\Address 32 | { 33 | if ($address instanceof self) 34 | { 35 | return $address; 36 | } 37 | elseif (is_string($address)) 38 | { 39 | if ( ! filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) 40 | { 41 | throw new \InvalidArgumentException("'$address' is not a valid IPv6 Address."); 42 | } 43 | $address = inet_pton($address); 44 | } 45 | elseif ($address instanceOf \Math_BigInteger) 46 | { 47 | // Do nothing 48 | } 49 | elseif (is_int($address)) 50 | { 51 | $address = new \Math_BigInteger($address); 52 | } 53 | else 54 | { 55 | throw new \InvalidArgumentException('Unsupported argument type.'); 56 | } 57 | 58 | return new IPv6\Address($address); 59 | } 60 | 61 | /** 62 | * This makes an IPv6 address fully qualified. It replaces :: with appropriate 0000 blocks, and 63 | * pads out all dropped 0s 64 | * 65 | * IE: 2001:630:d0:: becomes 2001:0630:00d0:0000:0000:0000:0000:0000 66 | * 67 | * @param string $address IPv6 address to be padded 68 | * @return string A fully padded string IPv6 address 69 | */ 70 | public static function pad(string $address): string 71 | { 72 | $parts = explode(':', $address); 73 | $count = count($parts); 74 | 75 | $hextets = array(); 76 | foreach ($parts as $i => $part) 77 | { 78 | if (isset($part[3])) // not need pad 79 | { 80 | $hextets[] = $part; 81 | } 82 | elseif ($part === '' && 0 < $i && $i < $count - 1) // missing hextets in :: 83 | { 84 | $missing = 8 - $count + 1; 85 | while ($missing--) 86 | { 87 | $hextets[] = '0000'; 88 | } 89 | } 90 | else 91 | { 92 | $hextets[] = str_pad($part, 4, '0', STR_PAD_LEFT); 93 | } 94 | } 95 | 96 | return implode(':', $hextets); 97 | } 98 | 99 | protected function __construct(\Math_BigInteger|int|string $address) 100 | { 101 | if ($address instanceOf \Math_BigInteger) 102 | { 103 | parent::__construct(str_pad($address->abs()->toBytes(), 16, chr(0), STR_PAD_LEFT)); 104 | } 105 | else 106 | { 107 | parent::__construct($address); 108 | } 109 | } 110 | 111 | public function is_encoded_IPv4_address(): bool 112 | { 113 | return strncmp($this->address, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 12) === 0; 114 | } 115 | 116 | public function as_IPv4_address(): IPv4\Address 117 | { 118 | if(!$this->is_encoded_IPv4_address()) 119 | throw new \InvalidArgumentException('Not an IPv4 Address encoded in an IPv6 Address'); 120 | [,$hex] = unpack('H*', $this->address); 121 | $parts = array_map('hexdec', array_slice(str_split($hex, 2), 12)); 122 | $address = implode('.', $parts); 123 | return IPv4\Address::factory($address); 124 | } 125 | 126 | public function add($value): Address 127 | { 128 | $left = new \Math_BigInteger($this->address, 256); 129 | $right = ($value instanceof \Math_BigInteger) ? $value : new \Math_BigInteger($value); 130 | return new IPv6\Address($left->add($right)); 131 | } 132 | 133 | public function subtract($value): IPv6\Address 134 | { 135 | $left = new \Math_BigInteger($this->address, 256); 136 | $right = ($value instanceof \Math_BigInteger) ? $value : new \Math_BigInteger($value); 137 | return new IPv6\Address($left->subtract($right)); 138 | } 139 | 140 | /** 141 | * Calculates the Bitwise & (AND) of a given IP address. 142 | * @param IP\Address $other is the ip to be compared against 143 | * @return IPv6\Address 144 | */ 145 | public function bitwise_and(IP\Address $other): IPv6\Address 146 | { 147 | return $this->bitwise_operation('&', $other); 148 | } 149 | 150 | /** 151 | * Calculates the Bitwise | (OR) of a given IP address. 152 | * @param IP\Address $other is the ip to be compared against 153 | * @return IPv6\Address 154 | */ 155 | public function bitwise_or(IP\Address $other): IPv6\Address 156 | { 157 | return $this->bitwise_operation('|', $other); 158 | } 159 | 160 | /** 161 | * Calculates the Bitwise ^ (XOR) of a given IP address. 162 | * @param IP\Address $other is the ip to be compared against 163 | * @return IPv6\Address 164 | */ 165 | public function bitwise_xor(IP\Address $other): IPv6\Address 166 | { 167 | return $this->bitwise_operation('^', $other); 168 | } 169 | 170 | /** 171 | * Calculates the Bitwise ~ (NOT) of a given IP address. 172 | * @return IPv6\Address 173 | */ 174 | public function bitwise_not(): IPv6\Address 175 | { 176 | return $this->bitwise_operation('~'); 177 | } 178 | 179 | public function bitwise_operation(string $operation, IP\Address $other = NULL): IPv6\Address 180 | { 181 | if ($operation !== '~') 182 | { 183 | $this->check_types($other); 184 | } 185 | 186 | $result = match ($operation) { 187 | '&' => $other->address & $this->address, 188 | '|' => $other->address | $this->address, 189 | '^' => $other->address ^ $this->address, 190 | '~' => ~$this->address, 191 | default => throw new \InvalidArgumentException('Unknown Operation type \'' . $operation . '\'.'), 192 | }; 193 | 194 | return new IPv6\Address($result); 195 | } 196 | 197 | public function compare_to(IP\Address $other): int 198 | { 199 | $this->check_types($other); 200 | 201 | if ($this->address < $other->address) { 202 | return -1; 203 | } 204 | elseif ($this->address > $other->address) { 205 | return 1; 206 | } 207 | else { 208 | return 0; 209 | } 210 | } 211 | 212 | public function format(int $mode): string 213 | { 214 | [, $hex] = unpack('H*', $this->address); 215 | $parts = str_split($hex, 4); 216 | 217 | if ($mode === self::FORMAT_MAY_MAPPED_COMPACT) { 218 | if ($this->is_encoded_IPv4_address()) { 219 | $mode = self::FORMAT_MAPPED_IPV4; 220 | } else { 221 | $mode = IP\Address::FORMAT_COMPACT; 222 | } 223 | } 224 | 225 | switch ($mode) { 226 | case IP\Address::FORMAT_FULL: 227 | // Do nothing 228 | break; 229 | 230 | case IPv6\Address::FORMAT_ABBREVIATED: 231 | foreach ($parts as $i => $quad) 232 | { 233 | $parts[$i] = ($quad === '0000') ? '0' : ltrim($quad, '0'); 234 | } 235 | break; 236 | 237 | case IPv6\Address::FORMAT_MAPPED_IPV4: 238 | list($a, $b) = str_split($parts[6], 2); 239 | list($c, $d) = str_split($parts[7], 2); 240 | return '::ffff:' . implode('.', array(hexdec($a), hexdec($b), hexdec($c), hexdec($d))); 241 | 242 | case IP\Address::FORMAT_COMPACT: 243 | $best_pos = $zeros_pos = FALSE; 244 | $best_count = 1; 245 | $zeros_count = 0; 246 | foreach ($parts as $i => $quad) 247 | { 248 | $parts[$i] = ($quad === '0000') ? '0' : ltrim($quad, '0'); 249 | 250 | if ($quad === '0000') 251 | { 252 | if ($zeros_pos === FALSE) 253 | { 254 | $zeros_pos = $i; 255 | } 256 | $zeros_count++; 257 | 258 | if ($zeros_count > $best_count) 259 | { 260 | $best_count = $zeros_count; 261 | $best_pos = $zeros_pos; 262 | } 263 | } 264 | else 265 | { 266 | $zeros_count = 0; 267 | $zeros_pos = FALSE; 268 | 269 | $parts[$i] = ltrim($quad, '0'); 270 | } 271 | } 272 | 273 | 274 | if ($best_pos !== FALSE) 275 | { 276 | $insert = array(NULL); 277 | 278 | if ($best_pos == 0 OR $best_pos + $best_count == 8) 279 | { 280 | $insert[] = NULL; 281 | if ($best_count == count($parts)) 282 | { 283 | $best_count--; 284 | } 285 | } 286 | array_splice($parts, $best_pos, $best_count, $insert); 287 | } 288 | 289 | break; 290 | 291 | default: 292 | throw new \InvalidArgumentException('Unsupported format mode: '.$mode); 293 | } 294 | 295 | return implode(':', $parts); 296 | } 297 | 298 | public function __toString() 299 | { 300 | return $this->format(IPv6\Address::FORMAT_MAY_MAPPED_COMPACT); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /classes/Leth/IPAddress/IPv6/NetworkAddress.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | namespace Leth\IPAddress\IPv6; 21 | use \Leth\IPAddress\IPv6; 22 | 23 | class NetworkAddress extends \Leth\IPAddress\IP\NetworkAddress 24 | { 25 | public const IP_VERSION = 6; 26 | public const MAX_SUBNET = 128; 27 | 28 | public static function generate_subnet_mask(int $cidr): IPv6\Address 29 | { 30 | $masks = array(); 31 | for ($i=1; $i <= 4; $i++) 32 | { 33 | // left shift operates over arch-specific integer sizes, 34 | // so we have to special case 32 bit shifts 35 | $shift = min(32, max(0, 32*$i - $cidr)); 36 | if ($shift === 32) 37 | { 38 | $masks[] = 0; 39 | } 40 | else 41 | { 42 | $masks[] = (~0) << $shift; 43 | } 44 | } 45 | $result = unpack('H*', pack('N4', $masks[0], $masks[1], $masks[2], $masks[3])); 46 | return IPv6\Address::factory(implode(':', str_split($result[1], 4))); 47 | } 48 | 49 | /** 50 | * Gets the Global subnet mask for this IP Protocol 51 | * 52 | * @return Address An IP Address representing the mask. 53 | * @author Marcus Cobden 54 | */ 55 | public static function get_global_netmask(): Address 56 | { 57 | return static::generate_subnet_mask(static::MAX_SUBNET); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leth/ip-address", 3 | "description": "IPv4 and IPv6 address and subnet classes with awesome utility functions.", 4 | "license": "LGPL-3.0-or-later", 5 | "support": { 6 | "source": "https://github.com/leth/PHP-IPAddress", 7 | "issues": "https://github.com/leth/PHP-IPAddress/issues" 8 | }, 9 | "require": { 10 | "php": ">=8.1", 11 | "pear/math_biginteger": "1.0.3" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^9", 15 | "phpstan/phpstan": "^1", 16 | "phpstan/phpstan-phpunit": "^1", 17 | "phpstan/phpstan-deprecation-rules": "^1", 18 | "phpstan/phpstan-strict-rules": "^1" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Leth\\IPAddress\\": "classes/Leth/IPAddress" 23 | } 24 | }, 25 | "scripts": { 26 | "test": "vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-text", 27 | "phpstan": "phpstan analyse --memory-limit=2G --error-format=table" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lgpl-3.0.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - classes 5 | - tests 6 | fileExtensions: 7 | - php 8 | reportUnmatchedIgnoredErrors: false 9 | checkMissingIterableValueType: false 10 | checkGenericClassInNonGenericObjectType: false 11 | parallel: 12 | processTimeout: 300.0 13 | jobSize: 20 14 | maximumNumberOfProcesses: 32 15 | minimumNumberOfJobsPerProcess: 4 16 | ignoreErrors: 17 | - '#Parameter \#1 \$x of class Math_BigInteger constructor expects optional, .* given#' 18 | includes: 19 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | ./classes 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/IPAddressTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($instance); 47 | $this->assertEquals($full, $instance->format(IP\Address::FORMAT_FULL)); 48 | $this->assertEquals($compact, $instance->format(IP\Address::FORMAT_COMPACT)); 49 | $this->assertEquals($expected_class, get_class($instance)); 50 | } 51 | } 52 | 53 | public function providerFactoryException(): array 54 | { 55 | return array( 56 | array('cake'), 57 | array('12345'), 58 | array('-12345'), 59 | ); 60 | } 61 | 62 | /** 63 | * 64 | * @dataProvider providerFactoryException 65 | */ 66 | public function testFactoryException(mixed $input): void 67 | { 68 | $this->expectException(\InvalidArgumentException::class); 69 | IP\Address::factory($input); 70 | } 71 | 72 | public function providerCompare(): array 73 | { 74 | return array( 75 | array('127.0.0.1', '127.0.0.1', 0), 76 | array('127.0.0.0', '127.0.0.1', -1), 77 | array('127.0.0.0', '127.0.0.2', -1), 78 | array('127.0.0.1', '127.0.0.2', -1), 79 | array('127.0.0.2', '127.0.0.1', 1), 80 | array('10.0.0.1', '127.0.0.2', -1) 81 | ); 82 | } 83 | 84 | /** 85 | * @dataProvider providerCompare 86 | */ 87 | public function testCompare(string $a, string $b, int $expected): void 88 | { 89 | $result = IP\Address::compare(IP\Address::factory($a), IP\Address::factory($b)); 90 | 91 | // Division is to ensure things are either -1, 0 or 1. abs() is to preseve sign. 92 | $this->assertEquals($expected, $result === 0 ? 0: $result / abs($result)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/IPNetworkAddressTest.php: -------------------------------------------------------------------------------- 1 | check_IP_version($other->address); 39 | } 40 | } 41 | 42 | class IPv6_NetworkAddress_Tester extends IPv6\NetworkAddress 43 | { 44 | public static function factory(NetworkAddress|Address|string $address, int|string|null $cidr = NULL): IPv6_NetworkAddress_Tester 45 | { 46 | $ip = IPv6\Address::factory($address); 47 | return new IPv6_NetworkAddress_Tester($ip, $cidr); 48 | } 49 | public function test_check_IP_version(NetworkAddress $other): void 50 | { 51 | $this->check_IP_version($other->address); 52 | } 53 | } 54 | 55 | /** 56 | * Tests for the IP\NetworkAddress Class 57 | * 58 | * @package default 59 | * @author Marcus Cobden 60 | */ 61 | class IP_NetworkAddress_Test extends TestCase 62 | { 63 | public function providerFactory(): array 64 | { 65 | return array( 66 | array('127.0.0.1/16', NULL, '127.0.0.1', 16, '127.0.0.0'), 67 | array('127.0.0.1', 16, '127.0.0.1', 16, '127.0.0.0'), 68 | array('127.0.0.1/32', NULL, '127.0.0.1', 32, '127.0.0.1'), 69 | array('127.0.0.1', 32, '127.0.0.1', 32, '127.0.0.1'), 70 | array(IP\NetworkAddress::factory('127.0.0.1/16'), NULL, '127.0.0.1', 16, '127.0.0.0'), 71 | array(IP\NetworkAddress::factory('127.0.0.1/16'), 10, '127.0.0.1', 10, '127.0.0.0'), 72 | 73 | array('::1/16', NULL, '::1', 16, '::0'), 74 | array('::1', 16, '::1', 16, '::0'), 75 | array('::1/128', NULL, '::1', 128, '::1'), 76 | array('::1', 128, '::1', 128, '::1'), 77 | 78 | ); 79 | } 80 | 81 | /** 82 | * @dataProvider providerFactory 83 | */ 84 | public function testFactory(string|IP\NetworkAddress $address, string|int|null $cidr, string $expected_address, int $expected_cidr, string $expected_subnet): void 85 | { 86 | $ip = IP\NetworkAddress::factory($address, $cidr); 87 | 88 | $this->assertEquals($expected_cidr, $ip->get_cidr()); 89 | $this->assertEquals($expected_address, (string) $ip->get_address()); 90 | $this->assertEquals($expected_subnet, (string) $ip->get_network_start()); 91 | } 92 | 93 | 94 | public function providerFactoryThrowsException(): array 95 | { 96 | return array( 97 | array(new IP_Address_Tester(), 1), 98 | array(new IP_Address_Tester(), 3) 99 | ); 100 | } 101 | 102 | /** 103 | * @dataProvider providerFactoryThrowsException 104 | * 105 | */ 106 | public function testFactoryThrowsException(IP_Address_Tester $address, int $cidr): void 107 | { 108 | $this->expectException(\InvalidArgumentException::class); 109 | IP\NetworkAddress::factory($address, $cidr); 110 | } 111 | 112 | public function provideFactoryParseCIDR(): array 113 | { 114 | return array( 115 | array('127.0.0.1/16', 24, 24), 116 | array('127.0.0.1', NULL, 32), 117 | array('127.0.0.1/24', NULL, 24), 118 | array('::1', NULL, 128), 119 | array('::1/58', 64, 64), 120 | array('::1/58', NULL, 58), 121 | ); 122 | } 123 | 124 | /** 125 | * @dataProvider provideFactoryParseCIDR 126 | */ 127 | public function testParseCIDR(string $address, string|int|null $cidr, int $expected): void 128 | { 129 | $network = IP\NetworkAddress::factory($address, $cidr); 130 | $this->assertEquals($expected, $network->get_cidr()); 131 | } 132 | 133 | public function providerUnimplementedException(): array 134 | { 135 | return array( 136 | #array('IP_NetworkAddress_Tester', 'generate_subnet_mask'), 137 | array('IP_NetworkAddress_Tester', 'get_global_netmask'), 138 | ); 139 | } 140 | 141 | /** 142 | * 143 | * @dataProvider providerUnimplementedException 144 | */ 145 | public function testUnimplementedException(string $class, string $method): void 146 | { 147 | $this->expectException(\LogicException::class); 148 | $class::$method(NULL); 149 | } 150 | 151 | public function providerCompare(): array 152 | { 153 | $data = array( 154 | array('0.0.0.0/16', '0.0.0.0/16', 0), 155 | array('0.0.0.0/16', '0.0.0.1/16', -1), 156 | array('0.0.0.1/16', '0.0.0.0/16', 1), 157 | array('127.0.0.1/16' , '127.0.0.1/16', 0), 158 | array('127.0.10.1/16', '127.0.2.1/16', 1), 159 | array('127.0.2.1/16' , '127.0.10.1/16', -1), 160 | // TODO add more addresses and v6 addresses 161 | ); 162 | foreach ($data as &$d) 163 | { 164 | $d[0] = IP\NetworkAddress::factory($d[0]); 165 | $d[1] = IP\NetworkAddress::factory($d[1]); 166 | } 167 | 168 | return $data; 169 | } 170 | 171 | /** 172 | * @dataProvider providerCompare 173 | */ 174 | public function testCompare(IP\NetworkAddress $left, IP\NetworkAddress $right, int $expected): void 175 | { 176 | $cmp = IP\NetworkAddress::compare($left, $right); 177 | 178 | if ($cmp !== 0) { 179 | $cmp /= abs($cmp); 180 | } 181 | 182 | $this->assertEquals($expected, $cmp); 183 | } 184 | 185 | public function providerAddressInNetwork(): array 186 | { 187 | return array( 188 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 0, NULL, '192.168.1.0'), 189 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 1, NULL, '192.168.1.1'), 190 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 2, NULL, '192.168.1.2'), 191 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 0, FALSE, '192.168.1.255'), 192 | array(IP\NetworkAddress::factory('192.168.1.1/24'), -1, NULL, '192.168.1.254'), 193 | array(IP\NetworkAddress::factory('192.168.1.1/24'), -2, NULL, '192.168.1.253'), 194 | array(IP\NetworkAddress::factory('192.168.1.1/24'), -3, NULL, '192.168.1.252'), 195 | 196 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 0, NULL, '192.168.1.0'), 197 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 1, NULL, '192.168.1.1'), 198 | array(IP\NetworkAddress::factory('192.168.1.1/24'), 0, FALSE, '192.168.1.255'), 199 | array(IP\NetworkAddress::factory('192.168.1.1/24'), -1, NULL, '192.168.1.254'), 200 | array(IP\NetworkAddress::factory('192.168.1.1/24'), -2, NULL, '192.168.1.253'), 201 | 202 | array(IP\NetworkAddress::factory('10.13.1.254/24'), 0, NULL, '10.13.1.0'), 203 | array(IP\NetworkAddress::factory('10.13.1.254/24'), 1, NULL, '10.13.1.1'), 204 | array(IP\NetworkAddress::factory('10.13.1.254/24'), 0, FALSE, '10.13.1.255'), 205 | array(IP\NetworkAddress::factory('10.13.1.254/24'), -1, NULL, '10.13.1.254'), 206 | 207 | array(IP\NetworkAddress::factory('10.13.1.254/24'), new \Math_BigInteger( 0), NULL, '10.13.1.0'), 208 | array(IP\NetworkAddress::factory('10.13.1.254/24'), new \Math_BigInteger( 1), NULL, '10.13.1.1'), 209 | array(IP\NetworkAddress::factory('10.13.1.254/24'), new \Math_BigInteger( 0), FALSE, '10.13.1.255'), 210 | array(IP\NetworkAddress::factory('10.13.1.254/24'), new \Math_BigInteger(-1), NULL, '10.13.1.254'), 211 | ); 212 | } 213 | 214 | /** 215 | * @dataProvider providerAddressInNetwork 216 | */ 217 | public function testAddressInNetwork(IP\NetworkAddress $network, int|Math_BigInteger $index, ?bool $from_start, string $expected): void 218 | { 219 | $address = $network->get_address_in_network($index, $from_start); 220 | $this->assertEquals($expected, (string) $address); 221 | } 222 | 223 | public function providerCheck_IP_version(): array 224 | { 225 | return array( 226 | array( 227 | IPv4_NetworkAddress_Tester::factory('10.1.0.0', 24), 228 | IPv4_NetworkAddress_Tester::factory('10.2.0.0', 24), 229 | IPv6_NetworkAddress_Tester::factory('::1', 24), 230 | IPv6_NetworkAddress_Tester::factory('1::1', 24) 231 | ) 232 | ); 233 | } 234 | 235 | public function providerCheck_IP_version_fail(): array 236 | { 237 | [[$a4, $b4, $a6, $b6]] = $this->providerCheck_IP_version(); 238 | return array( 239 | array($a4, $a6), 240 | array($a4, $b6), 241 | array($a6, $a4), 242 | array($a6, $b4), 243 | 244 | array($b4, $a6), 245 | array($b4, $b6), 246 | array($b6, $a4), 247 | array($b6, $b4), 248 | ); 249 | } 250 | 251 | /** 252 | * @dataProvider providerCheck_IP_version_fail 253 | */ 254 | public function test_check_IP_version_fail(IPv4_NetworkAddress_Tester|IPv6_NetworkAddress_Tester $left, IPv4_NetworkAddress_Tester|IPv6_NetworkAddress_Tester $right): void 255 | { 256 | try 257 | { 258 | $left->test_check_IP_version($right); 259 | $this->fail('An expected exception was not raised.'); 260 | } 261 | catch (\InvalidArgumentException $e) { 262 | // We expect this 263 | $this->assertTrue(true); 264 | } 265 | catch (\PHPUnit\Framework\AssertionFailedError $e) 266 | { 267 | // We expect this 268 | $this->assertTrue(true); 269 | } 270 | catch (Exception $e) { 271 | $this->fail('An unexpected exception was raised.' . $e->getMessage()); 272 | } 273 | } 274 | 275 | /** 276 | * @dataProvider providerCheck_IP_version 277 | */ 278 | public function test_check_IP_version(IPv4_NetworkAddress_Tester $a4, IPv4_NetworkAddress_Tester $b4, IPv6_NetworkAddress_Tester $a6, IPv6_NetworkAddress_Tester $b6): void 279 | { 280 | try 281 | { 282 | $a4->test_check_IP_version($b4); 283 | $b4->test_check_IP_version($a4); 284 | 285 | $a6->test_check_IP_version($b6); 286 | $b6->test_check_IP_version($a6); 287 | } 288 | catch (Exception $e) { 289 | $this->fail('An unexpected exception was raised.' . $e->getMessage()); 290 | } 291 | $this->assertTrue(true); 292 | } 293 | 294 | public function providerSubnets(): array 295 | { 296 | $data = array( 297 | array('2000::/3','2001:630:d0:f104::80a/128', true, true), 298 | array('2000::/3','2001:630:d0:f104::80a/96', true, true), 299 | array('2000::/3','2001:630:d0:f104::80a/48', true, true), 300 | 301 | array('2001:630:d0:f104::80a/96', '2000::/3', true, false), 302 | array('2001:630:d0:f104::80a/48', '2000::/3', true, false), 303 | 304 | array('2000::/3','4000::/3', false, false), 305 | array('2000::/3','1000::/3', false, false), 306 | ); 307 | 308 | foreach ($data as &$d) 309 | { 310 | $d[0] = IP\NetworkAddress::factory($d[0]); 311 | $d[1] = IP\NetworkAddress::factory($d[1]); 312 | } 313 | 314 | return $data; 315 | } 316 | 317 | /** 318 | * @dataProvider providerSubnets 319 | */ 320 | public function testSubnets(IP\NetworkAddress $sub1, IP\NetworkAddress $sub2, bool $shares, bool $encloses): void 321 | { 322 | $this->assertEquals($shares, $sub1->shares_subnet_space($sub2)); 323 | $this->assertEquals($encloses, $sub1->encloses_subnet($sub2)); 324 | } 325 | 326 | public function providerEnclosesAddress(): array 327 | { 328 | $data = array( 329 | array('2000::/3','2001:630:d0:f104::80a', true), 330 | array('2000::/3','2001:630:d0:f104::80a', true), 331 | array('2000::/3','2001:630:d0:f104::80a', true), 332 | 333 | array('2001:630:d0:f104::80a/96', '2000::', false), 334 | array('2001:630:d0:f104::80a/48', '2000::', false), 335 | 336 | array('2000::/3','4000::', false), 337 | array('2000::/3','1000::', false), 338 | ); 339 | 340 | foreach ($data as &$d) 341 | { 342 | $d[0] = IP\NetworkAddress::factory($d[0]); 343 | $d[1] = IP\Address::factory($d[1]); 344 | } 345 | 346 | return $data; 347 | } 348 | 349 | /** 350 | * @dataProvider providerEnclosesAddress 351 | */ 352 | public function testEnclosesAddress(IP\NetworkAddress $subnet, IP\Address $address, bool $expected): void 353 | { 354 | $this->assertEquals($expected, $subnet->encloses_address($address)); 355 | } 356 | 357 | public function provideNetworkIdentifiers(): array 358 | { 359 | $data = array( 360 | array('2000::/3', true), 361 | array('2000::1/3', false), 362 | 363 | array('2000::/3', true), 364 | array('2000::1/3', false), 365 | ); 366 | 367 | foreach ($data as &$d) 368 | { 369 | $d[0] = IP\NetworkAddress::factory($d[0]); 370 | } 371 | return $data; 372 | } 373 | 374 | /** 375 | * @dataProvider provideNetworkIdentifiers 376 | */ 377 | public function testNetworkIdentifiers(IP\NetworkAddress $subnet, bool $expected): void 378 | { 379 | $this->assertEquals($expected, $subnet->is_network_identifier()); 380 | $this->assertTrue($subnet->get_network_identifier()->is_network_identifier()); 381 | } 382 | 383 | public function test__toString(): void 384 | { 385 | $ip = '192.128.1.1/24'; 386 | $this->assertEquals($ip, (string) IP\NetworkAddress::factory($ip)); 387 | 388 | $ip = '::1/24'; 389 | $this->assertEquals($ip, (string) IP\NetworkAddress::factory($ip)); 390 | } 391 | 392 | public function providerExcluding(): array 393 | { 394 | $data = array( 395 | array('192.168.0.0/24', 396 | array(), 397 | array('192.168.0.0/24')), 398 | array('192.168.0.0/24', 399 | array('192.168.0.0/25'), 400 | array('192.168.0.128/25')), 401 | array('192.168.0.0/24', 402 | array('192.168.0.64/26', '192.168.0.128/26'), 403 | array('192.168.0.0/26', '192.168.0.192/26')), 404 | array('192.168.0.0/24', 405 | array('192.168.0.0/26'), 406 | array('192.168.0.64/26', '192.168.0.128/25')), 407 | array('192.168.0.0/24', 408 | array('192.168.0.0/27'), 409 | array('192.168.0.32/27', '192.168.0.64/26', '192.168.0.128/25')), 410 | // Test out of range exclusions 411 | array('192.168.0.0/24', 412 | array('10.0.0.0/24'), 413 | array('192.168.0.0/24')), 414 | array('192.168.0.0/24', 415 | array('10.0.0.0/24', '192.168.0.0/25'), 416 | array('192.168.0.128/25')), 417 | // Test an encompassing subnet 418 | array('192.168.0.0/24', 419 | array('192.168.0.0/23'), 420 | array()), 421 | ); 422 | foreach ($data as &$d) 423 | { 424 | $d[0] = IP\NetworkAddress::factory($d[0]); 425 | for ($i=1, $iMax = count($d); $i < $iMax; $i++) 426 | { 427 | foreach ($d[$i] as &$e) 428 | { 429 | $e = IP\NetworkAddress::factory($e); 430 | } 431 | } 432 | } 433 | return $data; 434 | } 435 | 436 | /** 437 | * @dataProvider providerExcluding 438 | */ 439 | public function testExcluding(IP\NetworkAddress $block, array $excluded, array $expected): void 440 | { 441 | $this->assertEquals($expected, $block->excluding($excluded)); 442 | } 443 | 444 | public function provideMerge(): array 445 | { 446 | $data = array( 447 | // Simple merge 448 | array( 449 | array('0.0.0.0/32', '0.0.0.1/32'), 450 | array('0.0.0.0/31'), 451 | ), 452 | // No merge 453 | array( 454 | array('0.0.0.1/32'), 455 | array('0.0.0.1/32'), 456 | ), 457 | array( 458 | array('0.0.0.0/32', '0.0.0.2/32'), 459 | array('0.0.0.0/32', '0.0.0.2/32'), 460 | ), 461 | // Duplicate entries 462 | array( 463 | array('0.0.0.0/32', '0.0.0.1/32', '0.0.0.1/32'), 464 | array('0.0.0.0/31'), 465 | ), 466 | array( 467 | array('0.0.0.0/32', '0.0.0.0/32', '0.0.0.1/32'), 468 | array('0.0.0.0/31'), 469 | ), 470 | array( 471 | array('0.0.0.0/32', '0.0.0.0/32', '0.0.0.1/32', '0.0.0.1/32'), 472 | array('0.0.0.0/31'), 473 | ), 474 | // Single merge with remainder 475 | array( 476 | array('0.0.0.0/32', '0.0.0.1/32', '0.0.0.2/32'), 477 | array('0.0.0.2/32', '0.0.0.0/31'), 478 | ), 479 | // Double merge 480 | array( 481 | array('0.0.0.0/32', '0.0.0.1/32', '0.0.0.2/31'), 482 | array('0.0.0.0/30'), 483 | ), 484 | // Non-network identifier 485 | array( 486 | array('0.0.0.0/31', '0.0.0.3/31'), 487 | array('0.0.0.0/30'), 488 | ), 489 | // IPv6 merges 490 | array( 491 | array('::0/128', '::1/128'), 492 | array('::0/127'), 493 | ), 494 | array( 495 | array('::0/128', '::1/128', '::2/127'), 496 | array('::0/126'), 497 | ), 498 | // Mixed subnets 499 | array( 500 | array('0.0.0.0/32', '0.0.0.1/32', '::0/128', '::1/128'), 501 | array('0.0.0.0/31', '::0/127'), 502 | ), 503 | // Merge with duplicate resultant entry 504 | array( 505 | array('0.0.0.0/22', '0.0.0.0/24', '0.0.1.0/24', '0.0.2.0/24', '0.0.3.0/24'), 506 | array('0.0.0.0/22'), 507 | ), 508 | ); 509 | 510 | foreach ($data as &$x) 511 | { 512 | foreach ($x as &$y) 513 | { 514 | foreach ($y as &$addr) 515 | { 516 | $addr = IP\NetworkAddress::factory($addr); 517 | } 518 | } 519 | } 520 | return $data; 521 | } 522 | 523 | /** 524 | * @dataProvider provideMerge 525 | */ 526 | public function testMerge(array $net_addrs, array $expected): void 527 | { 528 | $this->assertEquals($expected, IP\NetworkAddress::merge($net_addrs)); 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /tests/IPv4AddressTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($instance); 49 | $this->assertEquals($expected, (string) $instance); 50 | } 51 | 52 | public function providerFactoryException(): array 53 | { 54 | return array( 55 | array('256.0.0.1'), 56 | array('127.-1.0.1'), 57 | array('127.128.256.1'), 58 | array(new \Math_BigInteger('99999999999999999')), 59 | #array(123.45), throws TypeError 60 | #array(-123.45), throws TypeError 61 | array('cake'), 62 | array('12345'), 63 | array('-12345'), 64 | array('0000:0000:0000:ffff:0127:0000:0000:0001'), 65 | ); 66 | } 67 | 68 | public function testFormatInteger(): void 69 | { 70 | $ip = IPv4\Address::factory('127.0.0.1'); 71 | $this->assertEquals(2130706433, $ip->format(IPv4\Address::FORMAT_INTEGER)); 72 | } 73 | 74 | public function providerFormatException(): array 75 | { 76 | $bad_mode = -1; 77 | $data = $this->providerFactory(); 78 | foreach ($data as $i => $entry) { 79 | $data[$i] = array($entry[0], $bad_mode); 80 | } 81 | 82 | return $data; 83 | } 84 | 85 | /** 86 | * 87 | * @dataProvider providerFormatException 88 | */ 89 | public function testFormatException(mixed $input, mixed $mode): void 90 | { 91 | $this->expectException(InvalidArgumentException::class); 92 | $instance = IPv4\Address::factory($input); 93 | echo $instance->format($mode); 94 | } 95 | 96 | /** 97 | * 98 | * @dataProvider providerFactoryException 99 | */ 100 | public function testFactoryException(mixed $input): void 101 | { 102 | $this->expectException(InvalidArgumentException::class); 103 | IPv4\Address::factory($input); 104 | } 105 | 106 | public function providerBitwise(): array 107 | { 108 | return array( 109 | // OP1 OP2 AND OR XOR NOT 110 | array('0.0.0.1', '0.0.0.1', '0.0.0.1', '0.0.0.1', '0.0.0.0', '255.255.255.254'), 111 | array('0.0.0.0', '0.0.0.1', '0.0.0.0', '0.0.0.1', '0.0.0.1', '255.255.255.255'), 112 | array('0.0.0.1', '0.0.0.0', '0.0.0.0', '0.0.0.1', '0.0.0.1', '255.255.255.254'), 113 | array('0.0.0.0', '0.0.0.0', '0.0.0.0', '0.0.0.0', '0.0.0.0', '255.255.255.255'), 114 | ); 115 | } 116 | 117 | /** 118 | * @dataProvider providerBitwise 119 | */ 120 | public function testBitwise(string $ip1, string $ip2, string $ex_and, string $ex_or, string $ex_xor, string $ex_not): void 121 | { 122 | $ip1 = IPv4\Address::factory($ip1); 123 | $ip2 = IPv4\Address::factory($ip2); 124 | 125 | $this->assertEquals($ex_and, (string) $ip1->bitwise_and($ip2)); 126 | $this->assertEquals($ex_or , (string) $ip1->bitwise_or($ip2)); 127 | $this->assertEquals($ex_xor, (string) $ip1->bitwise_xor($ip2)); 128 | $this->assertEquals($ex_not, (string) $ip1->bitwise_not()); 129 | } 130 | 131 | // TODO Check this 132 | // public function providerAsIPv6Address() 133 | // { 134 | // return array( 135 | // array('127.0.0.1', '0000:0000:0000:ffff:0127:0000:0000:0001'), 136 | // ); 137 | // } 138 | // 139 | // /** 140 | // * @dataProvider providerAsIPv6Address 141 | // */ 142 | // public function testAsIPv6Address($v4, $v6) 143 | // { 144 | // $ip = IPv4\Address::factory($v4); 145 | // 146 | // $this->assertEquals($v6, (string) $ip->asIPv6Address()); 147 | // } 148 | 149 | public function providerAddSubtract(): array 150 | { 151 | $data = array( 152 | array('0.0.0.0' , 0, '0.0.0.0'), 153 | array('0.0.0.0' , 1, '0.0.0.1'), 154 | array('0.0.0.1' , 0, '0.0.0.1'), 155 | array('0.0.0.1' , 1, '0.0.0.2'), 156 | array('0.0.0.10' , 1, '0.0.0.11'), 157 | array('0.0.0.255', 1, '0.0.1.0'), 158 | array('0.0.255.0', 257, '0.1.0.1'), 159 | array('255.255.0.0' , 0, '255.255.0.0'), 160 | array('255.255.0.0' , 1, '255.255.0.1'), 161 | array('255.255.0.1' , 0, '255.255.0.1'), 162 | array('255.255.0.1' , 1, '255.255.0.2'), 163 | array('255.255.0.10' , 1, '255.255.0.11'), 164 | array('255.255.0.255', 1, '255.255.1.0'), 165 | array('255.0.255.0', 257, '255.1.0.1'), 166 | array('192.168.0.0', 4, '192.168.0.4'), 167 | ); 168 | 169 | for ($i=0, $iMax = count($data); $i < $iMax; $i++) { 170 | $data[$i][0] = IPv4\Address::factory($data[$i][0]); 171 | $data[$i][2] = IPv4\Address::factory($data[$i][2]); 172 | } 173 | 174 | return $data; 175 | } 176 | 177 | /** 178 | * @dataProvider providerAddSubtract 179 | */ 180 | public function testAddSubtract(IPv4\Address $left, int $right, IPv4\Address $expected): void 181 | { 182 | $result = $left->add($right); 183 | $this->assertEquals(0, $result->compare_to($expected)); 184 | $result = $result->subtract($right); 185 | $this->assertEquals(0, $result->compare_to($left)); 186 | } 187 | 188 | public function providerAsIPv6Address(): array 189 | { 190 | $data = array( 191 | array('0.0.0.0' , '::ffff:0:0' ), 192 | array('0.0.0.1' , '::ffff:0:1' ), 193 | array('0.0.0.255', '::ffff:0:ff' ), 194 | array('0.0.255.0', '::ffff:0:ff00'), 195 | array('0.255.0.0', '::ffff:ff:0' ), 196 | array('255.0.0.0', '::ffff:ff00:0'), 197 | ); 198 | 199 | foreach ($data as $i => $entry) { 200 | $data[$i] = array( 201 | IPv4\Address::factory($entry[0]), 202 | IPv6\Address::factory($entry[1])); 203 | } 204 | 205 | return $data; 206 | } 207 | 208 | /** 209 | * @dataProvider providerAsIPv6Address 210 | */ 211 | public function testAsIPv6Address(IPv4\Address $input, IPv6\Address $expected_equal): void 212 | { 213 | $converted = $input->as_IPv6_address(); 214 | 215 | $this->assertInstanceOf(Address::class, $converted); 216 | $this->assertEquals(0, $converted->compare_to($expected_equal)); 217 | } 218 | 219 | public function testGetOctet(): void 220 | { 221 | $ip = IPv4\Address::factory('10.250.30.40'); 222 | $this->assertEquals(10, $ip->get_octet(-4)); 223 | $this->assertEquals(250, $ip->get_octet(-3)); 224 | $this->assertEquals(30, $ip->get_octet(-2)); 225 | $this->assertEquals(40, $ip->get_octet(-1)); 226 | $this->assertEquals(10, $ip->get_octet(0)); 227 | $this->assertEquals(250, $ip->get_octet(1)); 228 | $this->assertEquals(30, $ip->get_octet(2)); 229 | $this->assertEquals(40, $ip->get_octet(3)); 230 | 231 | $this->assertNull($ip->get_octet(4)); 232 | } 233 | 234 | public function testArrayAccess(): void 235 | { 236 | $ip = IPv4\Address::factory('10.250.30.40'); 237 | $this->assertEquals(10, $ip[-4]); 238 | $this->assertEquals(250, $ip[1]); 239 | $this->assertNotEmpty($ip[1]); 240 | 241 | $this->assertNull($ip[4]); 242 | $this->assertFalse(isset($ip[4])); 243 | } 244 | 245 | public function testArrayAccessSet(): void 246 | { 247 | $this->expectException(\LogicException::class); 248 | $ip = IPv4\Address::factory('10.250.30.40'); 249 | $ip[0] = 0; 250 | } 251 | 252 | public function testArrayAccessUnset(): void 253 | { 254 | $this->expectException(\LogicException::class); 255 | $ip = IPv4\Address::factory('10.250.30.40'); 256 | unset($ip[0]); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /tests/IPv4NetworkAddressTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($subnet, (string) $net->get_subnet_mask()); 70 | $this->assertEquals($address_count, $net->get_NetworkAddress_count()); 71 | $this->assertEquals($network_class, $net->get_network_class()); 72 | } 73 | 74 | public function testGlobalNetmask(): void 75 | { 76 | $this->assertEquals('255.255.255.255', (string) IPv4\NetworkAddress::get_global_netmask()); 77 | } 78 | 79 | public function testDodgyBitwiseStuff(): void 80 | { 81 | $block = IPv4\NetworkAddress::factory('10.13.112.20/30'); 82 | $address = IPv4\Address::factory('10.13.112.21'); 83 | 84 | $this->assertTrue($block->encloses_address($address)); 85 | } 86 | 87 | public function providerNetworkBroadcastAddress(): array 88 | { 89 | return array( 90 | array(IPv4\NetworkAddress::factory('192.168.1.1/24'), '192.168.1.0', '192.168.1.255'), 91 | array(IPv4\NetworkAddress::factory('192.168.0.10/24'), '192.168.0.0', '192.168.0.255'), 92 | ); 93 | } 94 | 95 | /** 96 | * @dataProvider providerNetworkBroadcastAddress 97 | */ 98 | public function testNetworkBroadcastAddress(IPv4\NetworkAddress $ip, string $ex_network, string $ex_broadcast): void 99 | { 100 | $this->assertEquals($ex_network, (string) $ip->get_NetworkAddress()); 101 | $this->assertEquals($ex_broadcast, (string) $ip->get_broadcast_address()); 102 | } 103 | 104 | public function providerSplit(): array 105 | { 106 | $data = array( 107 | array('192.168.0.0/24', 0, array('192.168.0.0/24')), 108 | array('192.168.0.0/24', 1, array('192.168.0.0/25', '192.168.0.128/25')), 109 | array('192.168.0.0/24', 2, array('192.168.0.0/26', '192.168.0.64/26', '192.168.0.128/26', '192.168.0.192/26')), 110 | ); 111 | foreach ($data as &$d) 112 | { 113 | $d[0] = IPv4\NetworkAddress::factory($d[0]); 114 | foreach ($d[2] as &$e) 115 | { 116 | $e = IPv4\NetworkAddress::factory($e); 117 | } 118 | } 119 | return $data; 120 | } 121 | 122 | /** 123 | * @dataProvider providerSplit 124 | */ 125 | public function testSplit(IPv4\NetworkAddress $block, int $degree, array $expected): void 126 | { 127 | $this->assertEquals($expected, $block->split($degree)); 128 | } 129 | 130 | public function testSplitBeyondRange(): void 131 | { 132 | $this->expectException(InvalidArgumentException::class); 133 | $block = IPv4\NetworkAddress::factory('192.168.0.0/32'); 134 | $block->split(); 135 | } 136 | 137 | public function testIteratorInterface(): void 138 | { 139 | $block = IPv4\NetworkAddress::factory('192.168.0.0/30'); 140 | $expected = array('192.168.0.0', '192.168.0.1', '192.168.0.2', '192.168.0.3'); 141 | $actual = array(); 142 | foreach ($block as $key => $ip) 143 | { 144 | $actual[] = (string)$ip; 145 | } 146 | $this->assertEquals($expected, $actual); 147 | $this->assertEquals($expected, array_map('strval', $block->toArray())); 148 | } 149 | 150 | public function testTwoIterators(): void 151 | { 152 | $block = IPv4\NetworkAddress::factory('192.168.0.0/31'); 153 | $expected = array('192.168.0.0', '192.168.0.0', '192.168.0.1', '192.168.0.1', '192.168.0.0', '192.168.0.1'); 154 | $actual = array(); 155 | foreach ($block as $key => $ip) 156 | { 157 | $actual[] = (string)$ip; 158 | foreach ($block as $key2 => $ip2) 159 | { 160 | $actual[] = (string)$ip2; 161 | } 162 | } 163 | $this->assertEquals($expected, $actual); 164 | } 165 | 166 | public function testCountableInterface(): void 167 | { 168 | $block = IPv4\NetworkAddress::factory('192.168.0.0/30'); 169 | $this->assertCount(4, $block); 170 | $block = IPv4\NetworkAddress::factory('192.168.0.0/24'); 171 | $this->assertCount(2 ** 8, $block); 172 | $block = IPv4\NetworkAddress::factory('192.168.0.0/16'); 173 | $this->assertCount(2 ** 16, $block); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/IPv6AddressTest.php: -------------------------------------------------------------------------------- 1 | bitwise_operation($flag, $other); 15 | } 16 | } 17 | 18 | /** 19 | * Tests for the IPv6\Address Class 20 | * 21 | * @package default 22 | * @author Marcus Cobden 23 | */ 24 | class IPv6_Address_Test extends TestCase 25 | { 26 | 27 | public function providerFactory(): array 28 | { 29 | return array( 30 | array( 31 | '::1', 32 | '::1', 33 | '0:0:0:0:0:0:0:1', 34 | '0000:0000:0000:0000:0000:0000:0000:0001'), 35 | array( 36 | 1, 37 | '::1', 38 | '0:0:0:0:0:0:0:1', 39 | '0000:0000:0000:0000:0000:0000:0000:0001'), 40 | array( 41 | 'fe80::226:bbff:fe14:7372', 42 | 'fe80::226:bbff:fe14:7372', 43 | 'fe80:0:0:0:226:bbff:fe14:7372', 44 | 'fe80:0000:0000:0000:0226:bbff:fe14:7372'), 45 | array( 46 | '::ffff:127:0:0:1', 47 | '::ffff:127:0:0:1', 48 | '0:0:0:ffff:127:0:0:1', 49 | '0000:0000:0000:ffff:0127:0000:0000:0001'), 50 | array( 51 | '2001:504:0:1:0:3:1898:1', 52 | '2001:504:0:1:0:3:1898:1', 53 | '2001:504:0:1:0:3:1898:1', 54 | '2001:0504:0000:0001:0000:0003:1898:0001'), 55 | ); 56 | } 57 | 58 | /** 59 | * @dataProvider providerFactory 60 | */ 61 | public function testFactory(string|int $input,string $compact,string $abbr,string $full): void 62 | { 63 | $instance = IPv6\Address::factory($input); 64 | 65 | $this->assertNotNull($instance); 66 | $this->assertEquals($compact, $instance->format(IP\Address::FORMAT_COMPACT)); 67 | $this->assertEquals($abbr, $instance->format(IPv6\Address::FORMAT_ABBREVIATED)); 68 | $this->assertEquals($full, $instance->format(IP\Address::FORMAT_FULL)); 69 | } 70 | 71 | public function providerFormatException(): array 72 | { 73 | $bad_mode = -1; 74 | $data = static::providerFactory(); 75 | foreach ($data as $i => $entry) { 76 | $data[$i] = array($entry[0], $bad_mode); 77 | } 78 | 79 | return $data; 80 | } 81 | 82 | /** 83 | * 84 | * @dataProvider providerFormatException 85 | */ 86 | public function testFormatException(string|int $input, int $mode): void 87 | { 88 | $this->expectException(InvalidArgumentException::class); 89 | $instance = IPv6\Address::factory($input); 90 | echo $instance->format($mode); 91 | } 92 | 93 | public function providerFactoryException(): array 94 | { 95 | return array( 96 | array('256.0.0.1'), 97 | array('127.-1.0.1'), 98 | array('127.128.256.1'), 99 | array('cake'), 100 | array('12345'), 101 | array('-12345'), 102 | array('0000:0000:0000:ffff:0127:0000:0000:000g'), 103 | array('000000000000ffff0127000000000001'), 104 | ); 105 | } 106 | 107 | /** 108 | * 109 | * @dataProvider providerFactoryException 110 | */ 111 | public function testFactoryException(string $input): void 112 | { 113 | $this->expectException(InvalidArgumentException::class); 114 | IPv6\Address::factory($input); 115 | } 116 | 117 | public function providerAddSubtract(): array 118 | { 119 | $data = array( 120 | array('::' , 0, '::' ), 121 | array('::1' , 0, '::1' ), 122 | array('::1' , 1, '::2' ), 123 | array('::1' , 2, '::3' ), 124 | array('::5' , 6, '::b' ), 125 | array('::10', 1, '::11' ), 126 | array('::10', new \Math_BigInteger(1), '::11' ), 127 | array('::10', new \Math_BigInteger(2), '::12' ), 128 | ); 129 | 130 | for ($i=0, $iMax = count($data); $i < $iMax; $i++) 131 | { 132 | $data[$i][0] = IPv6\Address::factory($data[$i][0]); 133 | $data[$i][2] = IPv6\Address::factory($data[$i][2]); 134 | } 135 | return $data; 136 | } 137 | 138 | /** 139 | * @dataProvider providerAddSubtract 140 | */ 141 | public function testAddSubtract(IPv6\Address $left, int|\Math_BigInteger $right, IPv6\Address $expected): void 142 | { 143 | $result = $left->add($right); 144 | $this->assertEquals(0, $result->compare_to($expected)); 145 | $again = $result->subtract($right); 146 | $this->assertEquals(0, $again->compare_to($left)); 147 | } 148 | 149 | public function providerCompareTo(): array 150 | { 151 | $data = array( 152 | array('::', '::', 0), 153 | array('::1', '::1', 0), 154 | array('::1', '::2', -1), 155 | array('::2', '::1', 1), 156 | array('::f', '::1', 1), 157 | array('::a', '::b', -1), 158 | ); 159 | 160 | for ($i=0, $iMax = count($data); $i < $iMax; $i++){ 161 | $data[$i][0] = IPv6\Address::factory($data[$i][0]); 162 | $data[$i][1] = IPv6\Address::factory($data[$i][1]); 163 | } 164 | return $data; 165 | } 166 | 167 | /** 168 | * @dataProvider providerCompareTo 169 | */ 170 | public function testCompareTo(IPv6\Address $left, IPv6\Address $right, int $expected): void 171 | { 172 | $this->assertEquals($expected, $left->compare_to($right)); 173 | } 174 | 175 | public function providerBitwise(): array 176 | { 177 | $data = array( 178 | // OP1 OP2 AND OR XOR NOT 179 | array('::1', '::1', '::1', '::1', '::0', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe'), 180 | array('::' , '::1', '::0', '::1', '::1', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 181 | array('::1', '::' , '::0', '::1', '::1', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe'), 182 | array('::' , '::' , '::0', '::0', '::0', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 183 | ); 184 | 185 | for ($i=0, $iMax = count($data); $i < $iMax; $i++) { 186 | for ($j=0; $j < 6; $j++) { 187 | $data[$i][$j] = IPv6\Address::factory($data[$i][$j]); 188 | } 189 | } 190 | 191 | return $data; 192 | } 193 | 194 | /** 195 | * @dataProvider providerBitwise 196 | */ 197 | public function testBitwise(IPv6\Address $ip1, IPv6\Address $ip2, IPv6\Address $ex_and, IPv6\Address $ex_or, IPv6\Address $ex_xor, IPv6\Address $ex_not): void 198 | { 199 | $this->assertEquals((string) $ex_and, (string) $ip1->bitwise_and($ip2)); 200 | $this->assertEquals((string) $ex_or , (string) $ip1->bitwise_or($ip2)); 201 | $this->assertEquals((string) $ex_xor, (string) $ip1->bitwise_xor($ip2)); 202 | $this->assertEquals((string) $ex_not, (string) $ip1->bitwise_not()); 203 | } 204 | 205 | public function testBitwiseException(): void 206 | { 207 | 208 | $ip = TestingIPv6_Address::factory('::1'); 209 | 210 | try 211 | { 212 | $ip->call_bitwise_operation('!', $ip); 213 | $this->fail('An expected exception has not been raised.'); 214 | } 215 | catch (InvalidArgumentException $e) 216 | { 217 | $exception_message = $e->getMessage(); 218 | $expected_exception_message = "Unknown Operation type '!'."; 219 | $this->assertEquals($expected_exception_message, $exception_message); 220 | } 221 | 222 | $ip->call_bitwise_operation('&', $ip); 223 | $ip->call_bitwise_operation('|', $ip); 224 | $ip->call_bitwise_operation('^', $ip); 225 | $ip->call_bitwise_operation('~'); 226 | } 227 | 228 | // 229 | // public function provider_as_IPv4\Address() 230 | // { 231 | // return array( 232 | // array('0000:0000:0000:ffff:0127:0000:0000:0001', '127.0.0.1'), 233 | // ); 234 | // } 235 | // 236 | // /** 237 | // * @dataProvider provider_as_IPv4\Address 238 | // */ 239 | // public function test_as_IPv4\Address($v6, $v4 = NULL) 240 | // { 241 | // $ip = new IPv6\Address($v6); 242 | // 243 | // if ($v4 === NULL) 244 | // $this->assertFalse($ip->isEncodedIPv4Address()); 245 | // else 246 | // $this->assertEquals($v4, (string) $ip->asIPv4Address()); 247 | // 248 | // } 249 | 250 | public function testGetOctet(): void 251 | { 252 | $ip = IPv6\Address::factory('0001:0002:aaaa:1234:abcd:1000:2020:fffe'); 253 | $this->assertEquals(0, $ip->get_octet(-16)); 254 | $this->assertEquals(1, $ip->get_octet(-15)); 255 | $this->assertEquals(0xAA, $ip->get_octet(-11)); 256 | $this->assertEquals(0x12, $ip->get_octet(-10)); 257 | $this->assertEquals(0x20, $ip->get_octet(-4)); 258 | $this->assertEquals(0x20, $ip->get_octet(-3)); 259 | $this->assertEquals(0xFF, $ip->get_octet(-2)); 260 | $this->assertEquals(0xFE, $ip->get_octet(-1)); 261 | $this->assertEquals(0, $ip->get_octet(0)); 262 | $this->assertEquals(1, $ip->get_octet(1)); 263 | $this->assertEquals(0x10, $ip->get_octet(10)); 264 | $this->assertEquals(0xFE, $ip->get_octet(15)); 265 | 266 | $this->assertNull($ip->get_octet(16)); 267 | } 268 | 269 | public function testMappedIPv4(): void 270 | { 271 | /** @var IPv6\Address $ip */ 272 | $ip = IP\Address::factory('::ffff:141.44.23.50'); 273 | $this->assertEquals(1, $ip->is_encoded_IPv4_address()); 274 | $ipv4 = $ip->as_IPv4_address(); 275 | $this->assertEquals( '141.44.23.50',$ipv4->format(IP\Address::FORMAT_COMPACT)); 276 | } 277 | 278 | public function testMayMappedIPv4Format(): void 279 | { 280 | $mappedIPv4String = '::ffff:141.44.23.50'; 281 | $ordinaryIPv6String = '1:2:aaaa:1234:abcd:1000:2020:fffe'; 282 | $mappedIPv4Address = IP\Address::factory($mappedIPv4String); 283 | $ordinaryIPv6Address = IP\Address::factory($ordinaryIPv6String); 284 | $this->assertEquals($mappedIPv4Address->format(IPv6\Address::FORMAT_MAY_MAPPED_COMPACT), $mappedIPv4String); 285 | $this->assertEquals($ordinaryIPv6Address->format(IPv6\Address::FORMAT_MAY_MAPPED_COMPACT), $ordinaryIPv6String); 286 | } 287 | 288 | public function testArrayAccess(): void 289 | { 290 | $ip = IPv6\Address::factory('0001:0002:aaaa:1234:abcd:1000:2020:fffe'); 291 | $this->assertEquals(0x12, $ip[-10]); 292 | $this->assertEquals(0x10, $ip[10]); 293 | 294 | $this->assertNull($ip[16]); 295 | } 296 | 297 | /** 298 | * @return array 299 | */ 300 | public function providerPadIps(): array 301 | { 302 | return array( 303 | array('::', '0000:0000:0000:0000:0000:0000:0000:0000'), 304 | array('::fff', '0000:0000:0000:0000:0000:0000:0000:0fff'), 305 | array('::ff:fff', '0000:0000:0000:0000:0000:0000:00ff:0fff'), 306 | array('::f:ff:fff', '0000:0000:0000:0000:0000:000f:00ff:0fff'), 307 | array('fff::', '0fff:0000:0000:0000:0000:0000:0000:0000'), 308 | array('fff:ff::', '0fff:00ff:0000:0000:0000:0000:0000:0000'), 309 | array('fff:ff:f::', '0fff:00ff:000f:0000:0000:0000:0000:0000'), 310 | array('2001:630:d0::', '2001:0630:00d0:0000:0000:0000:0000:0000'), 311 | array('f:f:f:f:f:f:f:f', '000f:000f:000f:000f:000f:000f:000f:000f'), 312 | array('fff::fff', '0fff:0000:0000:0000:0000:0000:0000:0fff'), 313 | array('fff:0000:bb::aa:0000:fff', '0fff:0000:00bb:0000:0000:00aa:0000:0fff'), 314 | // not need pad 315 | array('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 316 | array('0000:0000:0000:0000:0000:0000:0000:0000', '0000:0000:0000:0000:0000:0000:0000:0000'), 317 | ); 318 | } 319 | 320 | /** 321 | * @dataProvider providerPadIps 322 | * 323 | * @param string $actual 324 | * @param string $expected 325 | */ 326 | public function testPad(string $actual, string $expected): void 327 | { 328 | $this->assertEquals($expected, IPv6\Address::pad($actual)); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /tests/IPv6NetworkAddressTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', (string) IPv6\NetworkAddress::get_global_netmask()); 16 | } 17 | 18 | public function providerSplit(): array 19 | { 20 | $data = array( 21 | array('::0/126', 0, array('::0/126')), 22 | array('::0/126', 1, array('::0/127', '::2/127')), 23 | array('::0/126', 2, array('::0/128', '::1/128', '::2/128', '::3/128')), 24 | ); 25 | foreach ($data as &$d) 26 | { 27 | $d[0] = IPv6\NetworkAddress::factory($d[0]); 28 | foreach ($d[2] as &$e) 29 | { 30 | $e = IPv6\NetworkAddress::factory($e); 31 | } 32 | } 33 | return $data; 34 | } 35 | 36 | /** 37 | * @dataProvider providerSplit 38 | */ 39 | public function testSplit(IPv6\NetworkAddress $block, int $degree, array $expected): void 40 | { 41 | $this->assertEquals($expected, $block->split($degree)); 42 | } 43 | 44 | public function testSplitBeyondRange(): void 45 | { 46 | $this->expectException(InvalidArgumentException::class); 47 | $block = IPv6\NetworkAddress::factory('::0/128'); 48 | $block->split(); 49 | } 50 | 51 | public function testIterationInterface(): void 52 | { 53 | $block = IPv6\NetworkAddress::factory('::0/126'); 54 | $expected = array('::0', '::1', '::2', '::3'); 55 | $actual = array(); 56 | foreach ($block as $key => $ip) 57 | { 58 | $actual[] = (string)$ip; 59 | } 60 | $this->assertEquals($expected, $actual); 61 | } 62 | 63 | public function testCountableInterface(): void 64 | { 65 | $block = IPv6\NetworkAddress::factory('::0/126'); 66 | $this->assertCount(4, $block); 67 | $block = IPv6\NetworkAddress::factory('::0/120'); 68 | $this->assertCount(2 ** 8, $block); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |