diff --git a/README.md b/README.md index 09f737b..4417930 100644 --- a/README.md +++ b/README.md @@ -491,6 +491,36 @@ echo \IPLib\Factory::parseRangeString('192.168.0.12/30')->getSize(); echo \IPLib\Factory::parseRangeString('192.168.0.1')->getSize(); ``` +Please note that if the number of IP addresses contained in the range is greater than the maximum integer supported by the operating system (2,147,483,647 for 32-bit systems, 9,223,372,036,854,775,807 for 64-bit systems), the `getSize()` method will return a `float` (which may be not precise). + +If instead you want the exact number of IP addresses, you can use the `getExactSize()` method, which will return a string containing the number of IP addresses in decimal format in case of such big numbers. + +```php +// This will print: +// int(1) +var_dump(\IPLib\Factory::parseRangeString('0.0.0.0/32')->getExactSize()); + +// On 32-bit systems, this will print +// string(10) "2147483648" +// On 64-bit systems, this will print +// int(2147483648) +var_dump(\IPLib\Factory::parseRangeString('0.0.0.0/1')->getExactSize()); + +// This will print: +// int(1073741824) +var_dump(\IPLib\Factory::parseRangeString('::/98')->getExactSize()); + +// On 32-bit systems, this will print +// string(10) "2147483648" +// On 64-bit systems, this will print +// int(2147483648) +var_dump(\IPLib\Factory::parseRangeString('::/97')->getExactSize()); + +// On 32-bit and 64-bit systems, this will print +// string(39) "170141183460469231731687303715884105728" +var_dump(\IPLib\Factory::parseRangeString('::/1')->getExactSize()); +``` + ### Getting the reverse DNS lookup address To perform reverse DNS queries, you need to use a special format of the IP addresses. diff --git a/src/Range/Pattern.php b/src/Range/Pattern.php index f7afeeb..b14e744 100644 --- a/src/Range/Pattern.php +++ b/src/Range/Pattern.php @@ -7,6 +7,7 @@ use IPLib\Address\IPv6; use IPLib\Address\Type as AddressType; use IPLib\ParseStringFlag; +use IPLib\Service\BinaryMath; /** * Represents an address range in pattern format (only ending asterisks are supported). @@ -304,7 +305,21 @@ public function getSize() $maxPrefix = $fromAddress::getNumberOfBits(); $prefix = $this->getNetworkPrefix(); - return pow(2, ($maxPrefix - $prefix)); + return pow(2, $maxPrefix - $prefix); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getExactSize() + */ + public function getExactSize() + { + $fromAddress = $this->fromAddress; + $maxPrefix = $fromAddress::getNumberOfBits(); + $prefix = $this->getNetworkPrefix(); + + return BinaryMath::getInstance()->pow2string($maxPrefix - $prefix); } /** diff --git a/src/Range/RangeInterface.php b/src/Range/RangeInterface.php index 4c7c02d..76d715f 100644 --- a/src/Range/RangeInterface.php +++ b/src/Range/RangeInterface.php @@ -150,14 +150,23 @@ public function asPattern(); public function getReverseDNSLookupName(); /** - * Get the count of addresses this IP range contains. + * Get the count of addresses contained in this IP range (possibly approximated). * - * @return int|float Return float as for huge IPv6 networks, int is not enough + * @return int|float If the number of addresses exceeds PHP_INT_MAX a float containing an approximation will be returned * * @since 1.16.0 */ public function getSize(); + /** + * Get the exact count of addresses contained in this IP range. + * + * @return int|numeric-string If the number of addresses exceeds PHP_INT_MAX a string containing the exact number of addresses will be returned + * + * @since 1.21.0 + */ + public function getExactSize(); + /** * Get the "network prefix", that is how many bits of the address are dedicated to the network portion. * diff --git a/src/Range/Single.php b/src/Range/Single.php index 09af4d8..bcc6960 100644 --- a/src/Range/Single.php +++ b/src/Range/Single.php @@ -237,6 +237,16 @@ public function getSize() return 1; } + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getExactSize() + */ + public function getExactSize() + { + return 1; + } + /** * {@inheritdoc} * diff --git a/src/Range/Subnet.php b/src/Range/Subnet.php index 9c00a10..4dcbb54 100644 --- a/src/Range/Subnet.php +++ b/src/Range/Subnet.php @@ -7,6 +7,7 @@ use IPLib\Address\Type as AddressType; use IPLib\Factory; use IPLib\ParseStringFlag; +use IPLib\Service\BinaryMath; /** * Represents an address range in subnet format (eg CIDR). @@ -348,6 +349,20 @@ public function getSize() $maxPrefix = $fromAddress::getNumberOfBits(); $prefix = $this->getNetworkPrefix(); - return pow(2, ($maxPrefix - $prefix)); + return pow(2, $maxPrefix - $prefix); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getExactSize() + */ + public function getExactSize() + { + $fromAddress = $this->fromAddress; + $maxPrefix = $fromAddress::getNumberOfBits(); + $prefix = $this->getNetworkPrefix(); + + return BinaryMath::getInstance()->pow2string($maxPrefix - $prefix); } } diff --git a/src/Service/BinaryMath.php b/src/Service/BinaryMath.php index 727fecf..639661c 100644 --- a/src/Service/BinaryMath.php +++ b/src/Service/BinaryMath.php @@ -9,6 +9,20 @@ */ class BinaryMath { + private static $instance; + + /** + * @return \IPLib\Service\BinaryMath + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance; + } + /** * Trim the leading zeroes from a non-negative integer represented in binary form. * @@ -97,6 +111,37 @@ public function orX($operand1, $operand2) return $result; } + /** + * Compute 2 raised to the given exponent. + * + * If the result fits into a native PHP integer, an int is returned. + * If the result exceeds PHP_INT_MAX, a string containing the exact decimal representation is returned. + * + * @param int $exponent The non-negative exponent + * + * @return int|string + */ + public function pow2string($exponent) + { + if ($exponent < PHP_INT_SIZE * 8 - 1) { + return 1 << $exponent; + } + $digits = array(1); + for ($i = 0; $i < $exponent; $i++) { + $carry = 0; + foreach ($digits as $index => $digit) { + $product = $digit * 2 + $carry; + $digits[$index] = $product % 10; + $carry = (int) ($product / 10); + } + if ($carry !== 0) { + $digits[] = $carry; + } + } + + return implode('', array_reverse($digits)); + } + /** * Zero-padding of two non-negative integers represented in binary form, so that they have the same length. * diff --git a/src/Service/RangesFromBoundaryCalculator.php b/src/Service/RangesFromBoundaryCalculator.php index 7a127e7..f26e94b 100644 --- a/src/Service/RangesFromBoundaryCalculator.php +++ b/src/Service/RangesFromBoundaryCalculator.php @@ -50,7 +50,7 @@ class RangesFromBoundaryCalculator */ public function __construct($numBits) { - $this->math = new BinaryMath(); + $this->math = BinaryMath::getInstance(); $this->setNumBits($numBits); } diff --git a/test/tests/Ranges/RangeSizeTest.php b/test/tests/Ranges/RangeSizeTest.php index 4c21fcb..05b4e03 100644 --- a/test/tests/Ranges/RangeSizeTest.php +++ b/test/tests/Ranges/RangeSizeTest.php @@ -13,17 +13,24 @@ public function rangesProvider() array('8.8.8.8', 1), array('2001:0db8:85a3:0000:0000:8a2e:0370:7334', 1), + array('8.8.8.8/32', 1), array('8.8.8.8/31', 2), - array('0.0.0.0/0', 4294967296), - array('100::/64', 1.8446744073709552E+19), - array('::/0', 3.402823669209385E+38), + array('0.0.0.0/1', PHP_INT_SIZE > 4 ? 0x80000000 : 2147483648.0, PHP_INT_SIZE > 4 ? null : '2147483648'), + array('0.0.0.0/0', PHP_INT_SIZE > 4 ? 0x100000000 : 4294967296.0, PHP_INT_SIZE > 4 ? null : '4294967296'), - array('172.16.0.*', 256), - array('172.*.*.*', 16777216), - array('*.*.*.*', 4294967296), - array('2001:0db8:85a3:0000:0000:8a2e:0370:*', 65536), - array('2001:0db8:85a3:0000:0000:*:*:*', 281474976710656), - array('*:*:*:*:*:*:*:*', 3.402823669209385E+38), + array('100::/128', 1), + array('100::/127', 2), + array('100::/98', 1073741824), + array('100::/97', PHP_INT_SIZE > 4 ? 2147483648 : 2147483648.0, PHP_INT_SIZE > 4 ? null : '2147483648'), + array('0::/1', 170141183460469231731687303715884105728.0, '170141183460469231731687303715884105728'), + array('::/0', 340282366920938463463374607431768211456.0, '340282366920938463463374607431768211456'), + + array('172.16.0.*', 0x100), + array('172.*.*.*', 0x1000000), + array('*.*.*.*', PHP_INT_SIZE > 4 ? 0x100000000 : 4294967296.0, PHP_INT_SIZE > 4 ? null : '4294967296'), + array('2001:0db8:85a3:0000:0000:8a2e:0370:*', 0x10000), + array('2001:0db8:85a3:0000:0000:*:*:*', PHP_INT_SIZE > 4 ? 0x1000000000000 : 281474976710656.0, PHP_INT_SIZE > 4 ? null : '281474976710656'), + array('*:*:*:*:*:*:*:*', 340282366920938463463374607431768211456.0, '340282366920938463463374607431768211456'), ); } @@ -31,12 +38,18 @@ public function rangesProvider() * @dataProvider rangesProvider * * @param string $addressRange - * @param int|float $size + * @param int|float $expectedSize + * @param int|string|null $expectedExactSize */ - public function testSize($addressRange, $size) + public function testSize($addressRange, $expectedSize, $expectedExactSize = null) { $range = Factory::rangeFromString($addressRange); $actualSize = $range->getSize(); - $this->assertSame($size, $actualSize); + $this->assertSame($expectedSize, $actualSize, 'getSize()'); + if ($expectedExactSize === null) { + $expectedExactSize = $expectedSize; + } + $actualExactSize = $range->getExactSize(); + $this->assertSame($expectedExactSize, $actualExactSize, 'getExactSize()'); } } diff --git a/test/tests/Services/BinaryMathTest.php b/test/tests/Services/BinaryMathTest.php index b778042..c07d953 100644 --- a/test/tests/Services/BinaryMathTest.php +++ b/test/tests/Services/BinaryMathTest.php @@ -4,6 +4,7 @@ use IPLib\Service\BinaryMath; use IPLib\Test\TestCase; +use ReflectionProperty; class BinaryMathTest extends TestCase { @@ -19,7 +20,20 @@ class BinaryMathTest extends TestCase */ protected static function doSetUpBeforeClass() { - self::$math = new BinaryMath(); + self::$math = BinaryMath::getInstance(); + } + + public function testSingleton() + { + $this->assertSame(self::$math, BinaryMath::getInstance()); + $instanceProperty = new ReflectionProperty('IPLib\\Service\\BinaryMath', 'instance'); + if (PHP_VERSION_ID < 80100) { + $instanceProperty->setAccessible(true); + } + $instanceProperty->setValue(null, null); + $newMath = BinaryMath::getInstance(); + $this->assertEquals(self::$math, $newMath); + $this->assertNotSame(self::$math, $newMath); } /** @@ -177,4 +191,42 @@ public function provideOrCases() return $cases; } + + /** + * @dataProvider providePow2stringCases + * + * @param int $exponent + * @param int|string $expectedResult + */ + public function testPow2string($exponent, $expectedResult) + { + $actualResult = self::$math->pow2string($exponent); + + $this->assertSame($expectedResult, $actualResult); + } + + /** + * @return array + */ + public function providePow2stringCases() + { + return array( + array(0, 1), + array(1, 2), + array(2, 4), + array(3, 8), + array(30, 0x40000000), + array(31, PHP_INT_SIZE > 4 ? 0x80000000 : '2147483648'), + array(32, PHP_INT_SIZE > 4 ? 0x100000000 : '4294967296'), + array(33, PHP_INT_SIZE > 4 ? 0x200000000 : '8589934592'), + array(62, PHP_INT_SIZE > 4 ? 0x4000000000000000 : '4611686018427387904'), + array(63, PHP_INT_SIZE > 8 ? 0x8000000000000000 : '9223372036854775808'), + array(64, PHP_INT_SIZE > 8 ? 0x10000000000000000 : '18446744073709551616'), + array(65, PHP_INT_SIZE > 8 ? 0x20000000000000000 : '36893488147419103232'), + array(126, PHP_INT_SIZE > 8 ? 0x20000000000000000000000000000000 : '85070591730234615865843651857942052864'), + array(127, PHP_INT_SIZE > 9 ? 0x40000000000000000000000000000000 : '170141183460469231731687303715884105728'), + array(128, PHP_INT_SIZE > 9 ? 0x80000000000000000000000000000000 : '340282366920938463463374607431768211456'), + array(129, PHP_INT_SIZE > 9 ? 0x100000000000000000000000000000000 : '680564733841876926926749214863536422912'), + ); + } }