From c5f604cdc2c0af57c482c7e67fa12d7fa98b754a Mon Sep 17 00:00:00 2001 From: Boden Garman Date: Wed, 10 Jun 2026 09:12:10 +1000 Subject: [PATCH] Fix numeric strings being misinterpreted as binary strings in IP::plus/minus IPBlock iteration passes the position to IP::plus() as a decimal string. The IPv4 constructor's auto-detection treats any 4-character string as a packed binary string, so once the position reached 1000 the string "1000" was read as the bytes 0x31303030 (825241648) instead of the number 1000, yielding addresses far outside the block. IP::plus() and IP::minus() now convert digit-only strings with initGmpFromNumericString() instead of the auto-detecting constructor, and IPBlock::getIterator() passes the GMP position directly. Fixes #74 Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 ++++ src/IP.php | 18 ++++++++++++++++-- src/IPBlock.php | 2 +- tests/IPTest.php | 4 ++++ tests/IPv4BlockTest.php | 18 ++++++++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfaf0ea..212ab14 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- `IP::plus()` and `IP::minus()` no longer misinterpret 4-character numeric strings (e.g. `'1000'`) as binary "packed" strings, which caused `IPBlock` iteration to yield addresses outside of the block [#74](https://github.com/rlanvin/php-ip/issues/74) + ## [3.0.0] - 2022-03-02 ### Backward Compatibility Breaking Changes diff --git a/src/IP.php b/src/IP.php index c08f5c3..5b06f31 100644 --- a/src/IP.php +++ b/src/IP.php @@ -401,7 +401,14 @@ public function plus($value): IP } if (!$value instanceof self) { - $value = new static($value); + if (is_string($value) && ctype_digit($value)) { + // numeric strings must bypass the constructor's auto-detection, + // which would treat a string of NB_BYTES characters (e.g. "1000") + // as a binary "packed" string + $value = new static(static::initGmpFromNumericString($value)); + } else { + $value = new static($value); + } } $result = gmp_add($this->ip, $value->ip); @@ -438,7 +445,14 @@ public function minus($value): IP } if (!$value instanceof self) { - $value = new static($value); + if (is_string($value) && ctype_digit($value)) { + // numeric strings must bypass the constructor's auto-detection, + // which would treat a string of NB_BYTES characters (e.g. "1000") + // as a binary "packed" string + $value = new static(static::initGmpFromNumericString($value)); + } else { + $value = new static($value); + } } $result = gmp_sub($this->ip, $value->ip); diff --git a/src/IPBlock.php b/src/IPBlock.php index b36b476..88d889b 100644 --- a/src/IPBlock.php +++ b/src/IPBlock.php @@ -574,7 +574,7 @@ public function getIterator(): \Generator $position = gmp_init(0); while (gmp_cmp($position, 0) >= 0 && gmp_cmp($position, $this->getNbAddresses()) < 0) { - yield $this->first_ip->plus(gmp_strval($position)); + yield $this->first_ip->plus($position); $position = gmp_add($position, 1); } } diff --git a/tests/IPTest.php b/tests/IPTest.php index a7db0f3..5fe28e2 100755 --- a/tests/IPTest.php +++ b/tests/IPTest.php @@ -86,6 +86,10 @@ public function validOperations() ['ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', -1, null, 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe'], ['::', 1, null, '::1'], ['::', null, -1, '::1'], + // numeric strings of 4 characters must not be mistaken + // for binary "packed" strings (see issue #74) + ['0.0.0.0', '1000', null, '0.0.3.232'], + ['0.0.3.232', null, '1000', '0.0.0.0'], ]; } diff --git a/tests/IPv4BlockTest.php b/tests/IPv4BlockTest.php index 9238d60..80b782c 100755 --- a/tests/IPv4BlockTest.php +++ b/tests/IPv4BlockTest.php @@ -45,6 +45,24 @@ public function testIterator() $this->assertEquals($expectation, iterator_to_array($subnet->getIterator())); } + /** + * Iterating past the 1000th address used to yield addresses outside of + * the block, because the numeric position was mistaken for a binary + * "packed" string (see issue #74). + */ + public function testIteratorStaysWithinBlock() + { + $subnet = new IPv4Block('35.35.32.0/22'); + + $ips = iterator_to_array($subnet->getIterator()); + + $this->assertCount(1024, $ips); + $this->assertEquals('35.35.32.0', (string) $ips[0]); + $this->assertEquals('35.35.35.231', (string) $ips[999]); + $this->assertEquals('35.35.35.232', (string) $ips[1000]); + $this->assertEquals('35.35.35.255', (string) $ips[1023]); + } + public function testGetPrivateBlocks() { $private_blocks = IPv4Block::getPrivateBlocks();