From b30cfa364806ffc4c42d6b1d6a56338001adee73 Mon Sep 17 00:00:00 2001 From: Evgeny Safronov Date: Sun, 12 Jul 2026 13:04:27 +0300 Subject: [PATCH] fix: honor the ExactSizeIterator contract in the IPv4 address iterators 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` while implementing it for `Range`. `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. --- benches/net.rs | 32 +++++++++ src/net.rs | 158 +++++++++++++++++++++++++++++++++++------- src/net/contiguous.rs | 121 ++++++++++++++++++++++++++------ 3 files changed, 265 insertions(+), 46 deletions(-) diff --git a/benches/net.rs b/benches/net.rs index 5deca0a..ee3958c 100644 --- a/benches/net.rs +++ b/benches/net.rs @@ -73,6 +73,8 @@ fn bench_net_addrs(c: &mut Criterion) { } fn bench_net_addrs_count(c: &mut Criterion) { + use netip::Contiguous; + let mut group = c.benchmark_group("netip"); group.bench_function("Ipv4Network::addrs count /24", |b| { @@ -106,6 +108,36 @@ fn bench_net_addrs_count(c: &mut Criterion) { }); }); + // The /0 networks are the counts that do not fit a `u32`/`u128`: the IPv4 + // one is exactly `2^32`, and the IPv6 one saturates the hint. + group.bench_function("Ipv4Network::addrs count /0", |b| { + let net = Ipv4Network::parse("0.0.0.0/0").unwrap(); + b.iter(|| { + core::hint::black_box(core::hint::black_box(net).addrs().count()); + }); + }); + + group.bench_function("Contiguous::addrs count /0", |b| { + let net = Contiguous::::parse("0.0.0.0/0").unwrap(); + b.iter(|| { + core::hint::black_box(core::hint::black_box(net).addrs().count()); + }); + }); + + group.bench_function("Ipv6Network::addrs count /0", |b| { + let net = Ipv6Network::parse("::/0").unwrap(); + b.iter(|| { + core::hint::black_box(core::hint::black_box(net).addrs().count()); + }); + }); + + group.bench_function("Ipv4Network::addrs count non-contiguous 16 host bits", |b| { + let net = Ipv4Network::parse("10.0.55.0/255.0.255.0").unwrap(); + b.iter(|| { + core::hint::black_box(core::hint::black_box(net).addrs().count()); + }); + }); + group.finish(); } diff --git a/src/net.rs b/src/net.rs index c33b24d..a7790ff 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1820,8 +1820,9 @@ impl Ipv4Network { /// For a non-contiguous mask the numeric order of the produced addresses /// may differ from the iteration order. /// - /// The iterator implements [`DoubleEndedIterator`] and - /// [`ExactSizeIterator`]. + /// The iterator always implements [`DoubleEndedIterator`], and implements + /// [`ExactSizeIterator`] on 64-bit targets only: the /0 network holds + /// `2^32` addresses, one more than a 32-bit `usize` can represent. /// /// # Examples /// @@ -2007,16 +2008,15 @@ impl Iterator for Ipv4NetworkAddrs { #[inline] fn count(self) -> usize { - self.len() + let (n, ..) = self.size_hint(); + n } #[inline] fn size_hint(&self) -> (usize, Option) { - if self.remaining > u32::MAX as u64 { - (usize::MAX, None) - } else { - let n = self.remaining as usize; - (n, Some(n)) + match usize::try_from(self.remaining) { + Ok(n) => (n, Some(n)), + Err(..) => (usize::MAX, None), } } } @@ -2056,9 +2056,17 @@ impl DoubleEndedIterator for Ipv4NetworkAddrs { } } +// Implemented only where the count of every IPv4 network fits a `usize`. The /0 +// network holds 2^32 addresses, one more than a 32-bit `usize` can represent, +// so on a 32-bit target no exact length exists for it and the trait's contract +// (`size_hint` must equal `(len(), Some(len()))`) is unsatisfiable. The +// standard library draws the same line: `RangeInclusive` is not an +// `ExactSizeIterator` while `Range` is. +#[cfg(target_pointer_width = "64")] impl ExactSizeIterator for Ipv4NetworkAddrs { #[inline] fn len(&self) -> usize { + // 2^32, the largest count any IPv4 network has, fits a 64-bit `usize`. self.remaining as usize } } @@ -5319,6 +5327,10 @@ mod test { } // Pins `last()`'s default forward-walk output on purpose. + // + // The /0 network's 2^32 addresses only fit a 64-bit `usize`, which is also + // the only target family where `ExactSizeIterator` is implemented. + #[cfg(target_pointer_width = "64")] #[allow(clippy::double_ended_iterator_last)] #[test] fn ipv4_addrs_edge_masks_pin_len_and_size_hint() { @@ -5329,15 +5341,44 @@ mod test { assert_eq!(Some(Ipv4Addr::new(10, 0, 0, 1)), single.addrs().last()); let full = Ipv4Network::parse("0.0.0.0/0").unwrap(); - assert_eq!(1usize << 32, full.addrs().len()); - assert_eq!((usize::MAX, None), full.addrs().size_hint()); + let total = 1usize << 32; + assert_eq!(total, full.addrs().len()); + assert_eq!((total, Some(total)), full.addrs().size_hint()); + assert_eq!(total, full.addrs().count()); let mut addrs = full.addrs(); assert_eq!(Some(Ipv4Addr::new(0, 0, 0, 0)), addrs.next()); assert_eq!(Some(Ipv4Addr::new(0, 0, 0, 1)), addrs.next()); assert_eq!(Some(Ipv4Addr::new(255, 255, 255, 255)), addrs.next_back()); assert_eq!(Some(Ipv4Addr::new(255, 255, 255, 254)), addrs.next_back()); - assert_eq!((1usize << 32) - 4, addrs.len()); + assert_eq!(total - 4, addrs.len()); + assert_eq!((total - 4, Some(total - 4)), addrs.size_hint()); + } + + // `ExactSizeIterator` requires `size_hint` to report exactly + // `(len(), Some(len()))`. The /0 network is the only IPv4 network whose + // count exceeds `u32::MAX`, so it is the case where the two can disagree. + #[cfg(target_pointer_width = "64")] + #[test] + fn ipv4_addrs_size_hint_matches_len_on_non_contiguous_masks() { + // 30 host bits: the widest count a non-contiguous mask can hold, since + // an all-zero mask is contiguous. + let wide = Ipv4Network::parse("128.0.0.1/128.0.0.1").unwrap(); + assert_eq!(1 << 30, wide.addrs().len()); + assert_eq!((1 << 30, Some(1 << 30)), wide.addrs().size_hint()); + assert_eq!(1 << 30, wide.addrs().count()); + + let net = Ipv4Network::parse("10.0.0.1/255.255.15.255").unwrap(); + let mut addrs = net.addrs(); + + for expected_len in (1..=16).rev() { + assert_eq!(expected_len, addrs.len()); + assert_eq!((expected_len, Some(expected_len)), addrs.size_hint()); + addrs.next(); + } + + assert_eq!(0, addrs.len()); + assert_eq!((0, Some(0)), addrs.size_hint()); } #[test] @@ -5469,6 +5510,22 @@ mod test { assert_eq!((usize::MAX, None), addrs.size_hint()); } + #[test] + fn ipv6_addrs_size_hint_lower_bound_never_exceeds_yielded_count() { + // Four host bits, at the non-contiguous positions 4..8. + let net = Ipv6Network::parse("2001:db8::/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff0f").unwrap(); + let mut addrs = net.addrs(); + + for _ in 0..=16 { + let (lower, upper) = addrs.size_hint(); + let yielded = addrs.collect::>().len(); + + assert_eq!(yielded, lower); + assert_eq!(Some(yielded), upper); + addrs.next(); + } + } + #[test] fn ipv6_addrs_wide_non_contiguous_pins_both_ends() { // 96 host bits (positions 16..112): far beyond `usize`, so only the @@ -5535,18 +5592,43 @@ mod test { } } + // Every IPv4 count, the /0 network's 2^32 included, fits a 64-bit + // `usize`, so the hint is exact for every network and the + // `ExactSizeIterator` contract holds unconditionally. + #[cfg(target_pointer_width = "64")] #[test] fn prop_ipv4_addrs_len_and_size_hint_match_host_count(net in arb_ipv4_network()) { let host_bits = (!net.mask().to_bits()).count_ones(); let total = 1u64 << host_bits; let addrs = net.addrs(); + let len = addrs.len(); - prop_assert_eq!(total, addrs.len() as u64); - prop_assert_eq!(total, net.addrs().count() as u64); - if host_bits == 32 { - prop_assert_eq!((usize::MAX, None), addrs.size_hint()); - } else { - prop_assert_eq!((total as usize, Some(total as usize)), addrs.size_hint()); + prop_assert_eq!(total, len as u64); + prop_assert_eq!(len, net.addrs().count()); + prop_assert_eq!((len, Some(len)), addrs.size_hint()); + } + + // Draining moves both `len` and the hint in lockstep, so the contract + // holds in every state the iterator can reach, not just the initial one. + #[cfg(target_pointer_width = "64")] + #[test] + fn prop_ipv4_addrs_size_hint_equals_len_across_drain( + net in arb_ipv4_network(), + script in prop::collection::vec(any::(), 1..48), + ) { + let host_bits = (!net.mask().to_bits()).count_ones(); + let mut expected_len = 1u64 << host_bits; + let mut addrs = net.addrs(); + + for take_front in script { + let len = addrs.len(); + prop_assert_eq!(expected_len, len as u64); + prop_assert_eq!((len, Some(len)), addrs.size_hint()); + + let yielded = if take_front { addrs.next() } else { addrs.next_back() }; + if yielded.is_some() { + expected_len -= 1; + } } } @@ -5611,18 +5693,46 @@ mod test { } } + // Counts above `usize::MAX` cannot be reported exactly, so the hint + // saturates to an honest lower bound with no upper bound. #[test] fn prop_ipv6_addrs_size_hint_matches_host_count(net in arb_ipv6_network()) { let host_bits = (!net.mask().to_bits()).count_ones(); + let exact = 1u128.checked_shl(host_bits).and_then(|total| usize::try_from(total).ok()); let addrs = net.addrs(); - if host_bits >= 64 { - prop_assert_eq!((usize::MAX, None), addrs.size_hint()); - prop_assert_eq!(usize::MAX, net.addrs().count()); - } else { - let total = 1usize << host_bits; - prop_assert_eq!((total, Some(total)), addrs.size_hint()); - prop_assert_eq!(total, net.addrs().count()); + match exact { + Some(total) => { + prop_assert_eq!((total, Some(total)), addrs.size_hint()); + prop_assert_eq!(total, net.addrs().count()); + } + None => { + prop_assert_eq!((usize::MAX, None), addrs.size_hint()); + prop_assert_eq!(usize::MAX, net.addrs().count()); + } + } + } + + // Brute-force check that the hint's lower bound never overstates what + // the iterator actually yields, in every state of a full drain. + #[test] + fn prop_ipv6_addrs_size_hint_lower_bound_never_exceeds_yielded_count(net in arb_ipv6_network()) { + // Host bits confined to the low byte, so the network is small + // enough to drain and count directly. + let (addr, mask) = net.to_bits(); + let net = Ipv6Network::from_bits(addr, mask | !0xFF); + let mut addrs = net.addrs(); + + loop { + let (lower, upper) = addrs.size_hint(); + let yielded = addrs.collect::>().len(); + + prop_assert_eq!(yielded, lower); + prop_assert_eq!(Some(yielded), upper); + + if addrs.next().is_none() { + break; + } } } diff --git a/src/net/contiguous.rs b/src/net/contiguous.rs index 07eb9ae..b7591a8 100644 --- a/src/net/contiguous.rs +++ b/src/net/contiguous.rs @@ -277,8 +277,9 @@ impl Contiguous { /// index instead. Reach the general iterator for a possibly /// non-contiguous network through [`Deref`], e.g. `(*net).addrs()`. /// - /// The iterator implements [`DoubleEndedIterator`] and - /// [`ExactSizeIterator`]. + /// The iterator always implements [`DoubleEndedIterator`], and implements + /// [`ExactSizeIterator`] on 64-bit targets only: the /0 network holds + /// `2^32` addresses, one more than a 32-bit `usize` can represent. /// /// # Examples /// @@ -334,6 +335,22 @@ pub struct ContiguousIpv4NetworkAddrs { back: u32, } +impl ContiguousIpv4NetworkAddrs { + /// Returns the number of addresses not yet yielded. + /// + /// Widened to `u64` because the /0 network's `2^32` addresses are one more + /// than the `u32` index range can count: the difference of the two bounds + /// is `u32::MAX`, and the inclusive count is that plus one. + #[inline] + fn remaining(&self) -> u64 { + if self.front > self.back { + return 0; + } + + u64::from(self.back - self.front) + 1 + } +} + impl Iterator for ContiguousIpv4NetworkAddrs { type Item = Ipv4Addr; @@ -360,22 +377,15 @@ impl Iterator for ContiguousIpv4NetworkAddrs { #[inline] fn count(self) -> usize { - self.len() + let (n, ..) = self.size_hint(); + n } #[inline] fn size_hint(&self) -> (usize, Option) { - if self.front > self.back { - return (0, Some(0)); - } - - let rem = self.back.wrapping_sub(self.front).wrapping_add(1); - - if rem == 0 { - (usize::MAX, None) - } else { - let n = rem as usize; - (n, Some(n)) + match usize::try_from(self.remaining()) { + Ok(n) => (n, Some(n)), + Err(..) => (usize::MAX, None), } } } @@ -400,16 +410,18 @@ impl DoubleEndedIterator for ContiguousIpv4NetworkAddrs { } } +// Implemented only where the count of every IPv4 network fits a `usize`. The /0 +// network holds 2^32 addresses, one more than a 32-bit `usize` can represent, +// so on a 32-bit target no exact length exists for it and the trait's contract +// (`size_hint` must equal `(len(), Some(len()))`) is unsatisfiable. The +// standard library draws the same line: `RangeInclusive` is not an +// `ExactSizeIterator` while `Range` is. +#[cfg(target_pointer_width = "64")] impl ExactSizeIterator for ContiguousIpv4NetworkAddrs { #[inline] fn len(&self) -> usize { - if self.front > self.back { - return 0; - } - - let rem = self.back.wrapping_sub(self.front).wrapping_add(1); - - if rem == 0 { 1usize << 32 } else { rem as usize } + // 2^32, the largest count any IPv4 network has, fits a 64-bit `usize`. + self.remaining() as usize } } @@ -968,7 +980,42 @@ mod test { let mut addrs = net.addrs(); assert_eq!(addrs.next(), Some(Ipv4Addr::new(0, 0, 0, 0))); assert_eq!(addrs.next_back(), Some(Ipv4Addr::new(255, 255, 255, 255))); - assert_eq!(net.addrs().len(), 1usize << 32); + } + + // The /0 network's 2^32 addresses only fit a 64-bit `usize`, which is also + // the only target family where `ExactSizeIterator` is implemented. + #[cfg(target_pointer_width = "64")] + #[test] + fn contiguous_ipv4_addrs_unspecified_size_hint_is_exact_and_matches_len() { + let net = Contiguous::::parse("0.0.0.0/0").unwrap(); + let total = 1usize << 32; + + let addrs = net.addrs(); + assert_eq!(total, addrs.len()); + assert_eq!((total, Some(total)), addrs.size_hint()); + assert_eq!(total, net.addrs().count()); + + let mut addrs = net.addrs(); + addrs.next(); + addrs.next_back(); + assert_eq!(total - 2, addrs.len()); + assert_eq!((total - 2, Some(total - 2)), addrs.size_hint()); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn contiguous_ipv4_addrs_size_hint_matches_len_across_drain() { + let net = Contiguous::::parse("192.168.1.0/30").unwrap(); + let mut addrs = net.addrs(); + + for expected_len in (1..=4).rev() { + assert_eq!(expected_len, addrs.len()); + assert_eq!((expected_len, Some(expected_len)), addrs.size_hint()); + addrs.next(); + } + + assert_eq!(0, addrs.len()); + assert_eq!((0, Some(0)), addrs.size_hint()); } #[test] @@ -1745,6 +1792,7 @@ mod test { } } + #[cfg(target_pointer_width = "64")] #[test] fn prop_contiguous_ipv4_addrs_len_matches_host_bits(net in arb_small_contiguous_ipv4_network()) { let (.., mask) = (*net).to_bits(); @@ -1753,6 +1801,35 @@ mod test { prop_assert_eq!(net.addrs().count(), expected_len); } + // Covers every prefix, including the /0 network whose 2^32 addresses + // are the reason `ExactSizeIterator` is 64-bit-only here. + #[cfg(target_pointer_width = "64")] + #[test] + fn prop_contiguous_ipv4_addrs_size_hint_equals_len(net in arb_contiguous_ipv4_network()) { + let (.., mask) = (*net).to_bits(); + let expected_len = 1u64 << (!mask).count_ones(); + let addrs = net.addrs(); + let len = addrs.len(); + + prop_assert_eq!(expected_len, len as u64); + prop_assert_eq!((len, Some(len)), addrs.size_hint()); + prop_assert_eq!(len, net.addrs().count()); + } + + // The upper `2^128 / 2^64` counts do not fit a `usize`, so the hint + // saturates to an honest lower bound instead of an exact length. + #[test] + fn prop_contiguous_ipv6_addrs_size_hint_is_exact_or_saturating(net in arb_contiguous_ipv6_network()) { + let (.., mask) = (*net).to_bits(); + let host_bits = (!mask).count_ones(); + let exact = 1u128.checked_shl(host_bits).and_then(|total| usize::try_from(total).ok()); + + match exact { + Some(total) => prop_assert_eq!((total, Some(total)), net.addrs().size_hint()), + None => prop_assert_eq!((usize::MAX, None), net.addrs().size_hint()), + } + } + #[test] fn prop_contiguous_ipv4_addrs_matches_brute_force_grid(net in arb_small_contiguous_ipv4_network()) { let (net_addr, mask) = (*net).to_bits();