///|
/// A dual-stack IP network. Scoped IPv6 network addresses retain their scope
/// only when construction does not need to clear host bits, matching Python.
struct IpNetwork {
cidr_ : Cidr
scope_ : String?
} derive(Eq)
///|
pub extend IpNetwork with Eq::{equal, not_equal}
///|
pub extend IpNetwork with Compare::{compare, op_lt, op_le, op_gt, op_ge}
///|
pub extend IpNetwork with Show::{output}
///|
pub extend IpNetwork with Hash::{hash, hash_combine}
///|
pub impl Show for IpNetwork with fn output(self, logger) {
logger.write_string(self.to_string())
}
///|
pub impl Hash for IpNetwork with fn hash_combine(self, hasher) {
hasher.combine(self.network_address())
hasher.combine(self.prefixlen())
}
///|
/// Total order: family (IPv4 first), numeric address, scope, then prefix length.
/// A missing scope precedes a present scope; present scopes compare as strings.
/// Unlike compare_networks or get_mixed_type_key, this order includes scope.
pub impl Compare for IpNetwork with fn compare(self, other) {
let address_order = self.network_address().compare(other.network_address())
if address_order != 0 {
address_order
} else {
self.prefixlen().compare(other.prefixlen())
}
}
///|
/// Split the address and mask, accepting an omitted host-width prefix.
fn ip_split_prefix(text : String) -> (IpAddress, Int32) raise IpError {
let (address_text, mask_text) = match text.split_once("/") {
Some((address, mask)) => (address.to_owned(), Some(mask.to_owned()))
None => (text, None)
}
let address = IpAddress::parse(address_text)
let prefix = match mask_text {
None => address.max_prefixlen()
Some(mask) => ip_parse_prefix(mask, address.family())
}
(address, prefix)
}
///|
/// An all-zero dotted mask is /0, and all ones is /32; otherwise accept
/// contiguous netmask bits or their contiguous hostmask complement.
fn ip_parse_prefix(text : String, family : Family) -> Int32 raise IpError {
let width = if family == V4 { 32 } else { 128 }
if text.contains(".") && family == V4 {
let mask = Address::parse(text) catch { _ => raise InvalidPrefix(text) }
if mask.family() != V4 {
raise InvalidPrefix(text)
}
for prefix = 0; prefix <= 32; prefix = prefix + 1 {
if mask.d == word_mask(prefix) {
return prefix
}
}
for prefix = 0; prefix <= 32; prefix = prefix + 1 {
if mask.d == word_mask(prefix).lnot() {
return prefix
}
}
raise InvalidPrefix(text)
}
if text.is_empty() {
raise InvalidPrefix(text)
}
let mut prefix = 0
for unit in text.code_units() {
let digit = unit.to_int() - 48
if digit < 0 || digit > 9 {
raise InvalidPrefix(text)
}
// Checking before multiplication also handles arbitrarily long inputs.
if prefix > width / 10 || (prefix == width / 10 && digit > width % 10) {
raise InvalidPrefix(text)
}
prefix = prefix * 10 + digit
}
prefix
}
///|
/// Parse an IPv4/IPv6 network; unlike legacy Cidr::parse, strict defaults true.
pub fn IpNetwork::parse(
text : String,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let (address, prefix) = ip_split_prefix(text)
IpNetwork::from_address(address, prefix, strict~)
}
///|
pub fn ip_network(
text : String,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
IpNetwork::parse(text, strict~)
}
///|
pub fn ipv4_network(
text : String,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let network = IpNetwork::parse(text, strict~)
if network.family() != V4 {
raise InvalidAddress(text)
}
network
}
///|
pub fn ipv6_network(
text : String,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let network = IpNetwork::parse(text, strict~)
if network.family() != V6 {
raise InvalidAddress(text)
}
network
}
///|
pub fn IpNetwork::from_address(
address : IpAddress,
prefix : Int32,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let raw = address.as_address()
let cidr = Cidr::new(raw, prefix, strict~)
let scope = if cidr.network() == raw { address.scope_id() } else { None }
{ cidr_: cidr, scope_: scope, }
}
///|
/// Construct from a typed address and decimal prefix, IPv4 netmask or hostmask.
/// IPv6 accepts a decimal prefix only. Strict defaults true, as with parse.
pub fn IpNetwork::from_address_mask(
address : IpAddress,
mask : String,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let prefix = ip_parse_prefix(mask, address.family())
IpNetwork::from_address(address, prefix, strict~)
}
///|
pub fn IpNetwork::from_cidr(cidr : Cidr) -> IpNetwork {
{ cidr_: cidr, scope_: None, }
}
///|
pub fn IpNetwork::as_cidr(self : IpNetwork) -> Cidr {
self.cidr_
}
///|
pub fn IpNetwork::from_bigint(
value : @bigint.BigInt,
prefix? : Int32,
family? : Family,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let address = IpAddress::from_bigint(value, family?)
IpNetwork::from_address(
address,
prefix.unwrap_or(address.max_prefixlen()),
strict~,
)
}
///|
pub fn IpNetwork::from_bytes(
bytes : Bytes,
prefix? : Int32,
strict~ : Bool = true,
) -> IpNetwork raise IpError {
let address = IpAddress::from_bytes(bytes)
IpNetwork::from_address(
address,
prefix.unwrap_or(address.max_prefixlen()),
strict~,
)
}
///|
pub fn IpNetwork::family(self : IpNetwork) -> Family {
self.cidr_.network().family()
}
///|
pub fn IpNetwork::version(self : IpNetwork) -> Int32 {
if self.family() == V4 {
4
} else {
6
}
}
///|
pub fn IpNetwork::max_prefixlen(self : IpNetwork) -> Int32 {
self.cidr_.network().width()
}
///|
pub fn IpNetwork::prefixlen(self : IpNetwork) -> Int32 {
self.cidr_.prefix()
}
///|
pub fn IpNetwork::network_address(self : IpNetwork) -> IpAddress {
{ address_: self.cidr_.network(), scope_: self.scope_, }
}
///|
/// Inclusive final address; IPv6 uses this name for parity with ipaddress.
pub fn IpNetwork::broadcast_address(self : IpNetwork) -> IpAddress {
IpAddress::from_address(self.cidr_.last())
}
///|
pub fn IpNetwork::netmask(self : IpNetwork) -> IpAddress {
let prefix = self.prefixlen()
let raw = if self.family() == V4 {
{ family_: V4, a: 0U, b: 0U, c: 0U, d: word_mask(prefix), }
} else {
{
family_: V6,
a: word_mask(prefix),
b: word_mask(prefix - 32),
c: word_mask(prefix - 64),
d: word_mask(prefix - 96),
}
}
IpAddress::from_address(raw)
}
///|
pub fn IpNetwork::hostmask(self : IpNetwork) -> IpAddress {
let raw = self.netmask().as_address()
IpAddress::from_address(
if self.family() == V4 {
{ ..raw, d: raw.d.lnot(), }
} else {
{
..raw,
a: raw.a.lnot(),
b: raw.b.lnot(),
c: raw.c.lnot(),
d: raw.d.lnot(),
}
},
)
}
///|
/// Exact cardinality, including the 2^128 addresses of IPv6 /0.
pub fn IpNetwork::num_addresses(self : IpNetwork) -> @bigint.BigInt {
1N << (self.max_prefixlen() - self.prefixlen())
}
///|
pub fn IpNetwork::to_string(self : IpNetwork) -> String {
"\{self.network_address()}/\{self.prefixlen()}"
}
///|
pub fn IpNetwork::compressed(self : IpNetwork) -> String {
self.to_string()
}
///|
pub fn IpNetwork::with_prefixlen(self : IpNetwork) -> String {
self.to_string()
}
///|
pub fn IpNetwork::with_netmask(self : IpNetwork) -> String {
"\{self.network_address()}/\{self.netmask()}"
}
///|
pub fn IpNetwork::with_hostmask(self : IpNetwork) -> String {
"\{self.network_address()}/\{self.hostmask()}"
}
///|
/// Expanded IPv6 representation. Scope is retained (Python's scoped exploded
/// formatter raises instead); this method remains total for valid values.
pub fn IpNetwork::exploded(self : IpNetwork) -> String {
let raw = self.cidr_.network()
let numeric = if self.family() == V4 {
raw.to_string()
} else {
[
raw.a >> 16,
raw.a & 65535U,
raw.b >> 16,
raw.b & 65535U,
raw.c >> 16,
raw.c & 65535U,
raw.d >> 16,
raw.d & 65535U,
]
.map(g => ip_padded(g.to_string(radix=16), 4))
.join(":")
}
let scoped = match self.scope_ {
None => numeric
Some(scope) => "\{numeric}%\{scope}"
}
"\{scoped}/\{self.prefixlen()}"
}
///|
pub fn IpNetwork::contains(self : IpNetwork, address : IpAddress) -> Bool {
address.family() == self.family() &&
address.as_address().mask(self.prefixlen()) == self.cidr_.network()
}
///|
pub fn IpNetwork::overlaps(self : IpNetwork, other : IpNetwork) -> Bool {
self.family() == other.family() &&
self.cidr_.network() <= other.cidr_.last() &&
other.cidr_.network() <= self.cidr_.last()
}
///|
/// Compare numeric network addresses, then prefixes; return -1, 0 or 1.
/// Different families raise InvalidRange. Scope is intentionally ignored:
/// unlike Compare, distinct scoped objects can compare equal here. This avoids
/// Python 3.13.2 returning 1 in both directions for equal bases with new scopes.
pub fn IpNetwork::compare_networks(
self : IpNetwork,
other : IpNetwork,
) -> Int32 raise IpError {
if self.family() != other.family() {
raise InvalidRange("network families differ")
}
let address_order = self.cidr_.network().compare(other.cidr_.network())
if address_order < 0 {
-1
} else if address_order > 0 {
1
} else if self.prefixlen() < other.prefixlen() {
-1
} else if self.prefixlen() > other.prefixlen() {
1
} else {
0
}
}
///|
/// Numeric containment ignores IPv6 scope, including equal network bases.
/// This differs from Python's scope-sensitive endpoint equality checks.
pub fn IpNetwork::subnet_of(
self : IpNetwork,
other : IpNetwork,
) -> Bool raise IpError {
if self.family() != other.family() {
raise InvalidRange("network families differ")
}
self.cidr_.network() >= other.cidr_.network() &&
self.cidr_.last() <= other.cidr_.last()
}
///|
pub fn IpNetwork::supernet_of(
self : IpNetwork,
other : IpNetwork,
) -> Bool raise IpError {
other.subnet_of(self)
}
///|
fn ip_address_iter(first : Address, last : Address) -> Iter[IpAddress] {
let mut cursor = if first <= last { Some(first) } else { None }
Iter::new(fn() {
match cursor {
None => None
Some(address) => {
cursor = if address == last { None } else { address.next() }
Some(IpAddress::from_address(address))
}
}
})
}
///|
/// Lazily iterate all numeric addresses, without allocating the address space.
pub fn IpNetwork::iter(self : IpNetwork) -> Iter[IpAddress] {
ip_address_iter(self.cidr_.network(), self.cidr_.last())
}
///|
/// IPv4 excludes network/broadcast except /31 and /32; IPv6 excludes subnet
/// router anycast except /127 and /128. Iteration uses constant auxiliary space.
pub fn IpNetwork::hosts(self : IpNetwork) -> Iter[IpAddress] {
if self.prefixlen() == self.max_prefixlen() {
return Iter::singleton(self.network_address())
}
if self.prefixlen() == self.max_prefixlen() - 1 {
return self.iter()
}
let first = self.cidr_.network().next().unwrap()
let last = if self.family() == V4 {
self.cidr_.last().previous().unwrap()
} else {
self.cidr_.last()
}
ip_address_iter(first, last)
}
///|
/// Zero-based address indexing; negative indices count back from the end.
pub fn IpNetwork::nth(
self : IpNetwork,
index : @bigint.BigInt,
) -> IpAddress raise IpError {
let offset = if index < 0N { self.num_addresses() + index } else { index }
if offset < 0N || offset >= self.num_addresses() {
raise InvalidRange("address index out of bounds")
}
IpAddress::from_bigint(
self.network_address().to_bigint() + offset,
family=self.family(),
)
}
///|
/// Lazily split into equal-size subnets. Only one resulting block is retained.
pub fn IpNetwork::subnets(
self : IpNetwork,
prefixlen_diff~ : Int32 = 1,
new_prefix? : Int32,
) -> Iter[IpNetwork] raise IpError {
if self.prefixlen() == self.max_prefixlen() {
return Iter::singleton(self)
}
let prefix = match new_prefix {
Some(prefix) => {
if prefixlen_diff != 1 ||
prefix < self.prefixlen() ||
prefix > self.max_prefixlen() {
raise InvalidPrefix("invalid subnet prefix or conflicting options")
}
prefix
}
None => {
if prefixlen_diff < 0 ||
prefixlen_diff > self.max_prefixlen() - self.prefixlen() {
raise InvalidPrefix("invalid subnet prefix length difference")
}
self.prefixlen() + prefixlen_diff
}
}
let end = self.cidr_.last()
let mut cursor = Some(self.cidr_.network())
Iter::new(fn() {
match cursor {
None => None
Some(address) => {
let cidr = cidr_from_normalized(address, prefix)
cursor = if cidr.last() == end { None } else { cidr.last().next() }
Some(IpNetwork::from_cidr(cidr))
}
}
})
}
///|
pub fn IpNetwork::supernet(
self : IpNetwork,
prefixlen_diff~ : Int32 = 1,
new_prefix? : Int32,
) -> IpNetwork raise IpError {
if self.prefixlen() == 0 {
return self
}
let prefix = match new_prefix {
Some(prefix) => {
if prefixlen_diff != 1 || prefix < 0 || prefix > self.prefixlen() {
raise InvalidPrefix("invalid supernet prefix or conflicting options")
}
prefix
}
None => {
if prefixlen_diff < 0 || prefixlen_diff > self.prefixlen() {
raise InvalidPrefix("invalid supernet prefix length difference")
}
self.prefixlen() - prefixlen_diff
}
}
IpNetwork::from_cidr(
cidr_from_normalized(self.cidr_.network().mask(prefix), prefix),
)
}
///|
/// Remove a contained network in at most 128 lazy binary partition steps.
/// Results use Python's sibling-first order rather than numeric sorting.
/// Containment uses numeric ranges and output networks do not retain scope.
pub fn IpNetwork::address_exclude(
self : IpNetwork,
other : IpNetwork,
) -> Iter[IpNetwork] raise IpError {
if !other.subnet_of(self) {
raise InvalidRange("excluded network is not contained")
}
let mut current = self.cidr_
let target = other.cidr_
Iter::new(fn() {
if current.prefix() >= target.prefix() {
return None
}
let prefix = current.prefix() + 1
let left = cidr_from_normalized(current.network(), prefix)
let right = cidr_from_normalized(left.last().next().unwrap(), prefix)
if target.network() <= left.last() {
current = left
Some(IpNetwork::from_cidr(right))
} else {
current = right
Some(IpNetwork::from_cidr(left))
}
})
}
///|
pub fn IpNetwork::is_private(self : IpNetwork) -> Bool {
let first = self.cidr_.network()
let last = self.cidr_.last()
ip_private_networks(self.family())
.iter()
.any(fn(range) { ip_in_cidr(first, range) && ip_in_cidr(last, range) }) &&
ip_private_exceptions(self.family())
.iter()
.all(fn(range) { !ip_in_cidr(first, range) && !ip_in_cidr(last, range) })
}
///|
pub fn IpNetwork::is_global(self : IpNetwork) -> Bool {
let cgnat = self.family() == V4 &&
self.cidr_.network().d >= 0x64400000U &&
self.cidr_.last().d <= 0x647fffffU
!cgnat && !self.is_private()
}
///|
pub fn IpNetwork::is_multicast(self : IpNetwork) -> Bool {
self.network_address().is_multicast() &&
self.broadcast_address().is_multicast()
}
///|
pub fn IpNetwork::is_reserved(self : IpNetwork) -> Bool {
self.network_address().is_reserved() && self.broadcast_address().is_reserved()
}
///|
pub fn IpNetwork::is_unspecified(self : IpNetwork) -> Bool {
self.network_address().is_unspecified() &&
self.broadcast_address().is_unspecified()
}
///|
pub fn IpNetwork::is_loopback(self : IpNetwork) -> Bool {
self.network_address().is_loopback() && self.broadcast_address().is_loopback()
}
///|
pub fn IpNetwork::is_link_local(self : IpNetwork) -> Bool {
self.network_address().is_link_local() &&
self.broadcast_address().is_link_local()
}
///|
pub fn IpNetwork::is_site_local(self : IpNetwork) -> Bool {
self.network_address().is_site_local() &&
self.broadcast_address().is_site_local()
}
///|
/// Exact minimal CIDR cover of an inclusive numeric range, in ascending order.
/// Ignore IPv6 scope on both endpoints and omit it from the resulting cover.
pub fn summarize_address_range(
first : IpAddress,
last : IpAddress,
) -> Array[IpNetwork] raise IpError {
let interval = Interval::new(first.as_address(), last.as_address())
IpSet::from_ranges([interval]).cidrs().map(IpNetwork::from_cidr)
}
///|
/// Merge overlapping/adjacent networks of one address family. Scopes are not
/// address bits and are omitted from the resulting canonical numeric cover.
pub fn collapse_addresses(
networks : Array[IpNetwork],
) -> Array[IpNetwork] raise IpError {
let ranges : Array[Interval] = []
let mut family : Family? = None
for network in networks {
match family {
Some(previous) if previous != network.family() =>
raise InvalidRange("cannot collapse mixed address families")
_ => family = Some(network.family())
}
ranges.push(Interval::new(network.cidr_.network(), network.cidr_.last()))
}
IpSet::from_ranges(ranges).cidrs().map(IpNetwork::from_cidr)
}
///|
pub extend IpNetwork with @debug.Debug::{to_repr}
///|
pub impl @debug.Debug for IpNetwork with fn to_repr(self) {
@debug.Repr(self.to_string())
}