fix: honor the ExactSizeIterator contract in the IPv4 address iterators - #94
Merged
Conversation
An IPv4 /0 network holds 2^32 addresses, one more than `u32::MAX` and one more than a 32-bit `usize` can hold. Both IPv4 address iterators claimed `ExactSizeIterator` regardless, and neither could keep its promise: - `len()` and `size_hint()` disagreed for /0 on every target. `len()` reported the true 2^32 while `size_hint()` saturated to `(usize::MAX, None)`, breaking the trait's requirement that `size_hint` return exactly `(len(), Some(len()))`. - On a 32-bit target `Ipv4NetworkAddrs::len()` silently truncated /0 to 0, and `ContiguousIpv4NetworkAddrs::len()` failed to compile outright: `1usize << 32` is a deny-by-default `arithmetic_overflow` error. Make `size_hint` exact whenever the count fits a `usize` and saturate honestly when it does not, adopting the `usize::try_from` idiom the IPv6 iterators already use. On 64-bit /0 now reports `(2^32, Some(2^32))`, so the contradiction with `len()` disappears; on 32-bit it reports `(usize::MAX, None)`, an honest lower bound. Gate `ExactSizeIterator` itself to 64-bit targets, where every IPv4 count fits. On 32-bit the trait simply is not implemented, matching the standard library's decision not to implement it for `RangeInclusive<u32>` while implementing it for `Range<u32>`. `count()` no longer routes through `len()`, so it stays available and correct on both pointer widths. The rewritten `size_hint` is also leaner on 64-bit: the /0 special case folds away entirely, turning 24 instructions with a branch into 22 straight-line ones.
This was referenced Jul 12, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
An IPv4
/0network holds exactly2^32addresses -- one more thanu32::MAX, and one more than a 32-bitusizecan represent. Both IPv4 address iterators (Ipv4NetworkAddrsandContiguousIpv4NetworkAddrs) implementExactSizeIteratorand both got this wrong, in two separate ways.On 64-bit, today, in released code:
len()returns2^32whilesize_hint()returns(usize::MAX, None).ExactSizeIteratorrequiressize_hint()to be exactly(len(), Some(len())), so the contract is violated for/0. Measured on x86-64 before this change:On 32-bit:
Ipv4NetworkAddrs::len()(self.remaining as usize) silently truncates2^32to0, andContiguousIpv4NetworkAddrs::len()(1usize << 32) does not even compile:The fix
size_hintis exact whenever the count fits, and saturates honestly when it does not -- adopting theusize::try_fromidiom the IPv6 iterators in this crate already use, so both families now share one scheme instead of two:ContiguousIpv4NetworkAddrsgains a privateremaining() -> u64as the single source of truth for the count. It replaces theu32wrapping-sentinel arithmetic thatsize_hint,countandleneach re-derived independently.ExactSizeIteratoris implemented on 64-bit targets only. On a 32-bit target no exact length exists for/0, so the contract is unsatisfiable and the trait must not be implemented. The standard library draws the same line:RangeInclusive<u32>is not anExactSizeIteratorwhileRange<u32>is.count()no longer routes throughlen(), so it keeps working on both pointer widths.This is not a breaking change for 64-bit targets: the trait, and
len(), behave exactly as before for every network except/0, wheresize_hint()is now correct.Verification
cargo build --libonmainfails oni686-unknown-linux-gnuwith the overflow above. On this branch it builds clean, as doeswasm32-unknown-unknown-- both 32-bit-usizetargets.Run under a real 32-bit
usize(wasm32, executed under node):/0reports an honest saturated lower bound, everything that fits stays exact,count()works withoutExactSizeIterator, and.len()correctly fails to resolve.size_hintalso got shorter: 24 instructions with a branch to a/0special case, down to 22 straight-line instructions -- LLVM provesusize::try_from(u64)infallible on 64-bit and folds the error arm away.Tests
Unit tests for
size_hint() == (len(), Some(len()))at/0, across a full drain, and on non-contiguous masks, for both IPv4 iterators, plus the IPv6 honesty counterpart. Property tests (proptest) assert the contract holds in every drain state for IPv4 across the full prefix range, and for IPv6 that the hint's lower bound never overstates what is actually yielded. The pin-test that previously asserted(usize::MAX, None)now asserts the corrected value and stands as the regression guard. Benchmarks cover the changed path.