iyi

Standard library, 0.18.0

std/netip

IP addresses and prefixes as values, the way Go's net/netip holds them and Python's ipaddress reads and classifies them: no socket, no resolver, nothing of the platform. std/socket connects to addresses; this module is for what a program decides about one - is it loopback, is it inside 10.0.0.0/8, which route of a table is the longest that holds it - and it can be imported by code that must not reach the network or the clock.

import std/netip to use it. Source: src/std/netip.iyi at v0.18.0.

IP is either family in one value: 32 or 128 bits, and for IPv6 a zone (fe80::1%eth0, RFC 4007 section 11), which is kept, printed, and part of == and of the order, as Go and Python keep it. An IPv4-mapped IPv6 address (::ffff:192.0.2.1, RFC 4291 section 2.5.5.2) is an IPv6 address, not equal to the IPv4 one, as in both of those; unmap and ipv4_mapped take it apart and ipv6_mapped makes one.

Reading is Python 3.14's ip_address, rule for rule, because Python's rules are the strict ones and a program checking an allowlist must not read an address another reader reads differently. IPv4 is four decimal octets of one to three ASCII digits, none past 255 and none with a leading zero: 010.0.0.1 is refused, because glibc's inet_aton reads 010 as octal 8 (CVE-2021-29921, Python's bpo-36384), and so are the shorthand forms inet_aton takes (127.1, 0x7f.1). IPv6 is RFC 4291 section 2.2's text: eight groups of one to four hex digits, one :: for a run of zero groups, an IPv4 dotted tail in place of the last two, at most 45 characters before the zone, and a zone that is not empty and holds no second %. A / anywhere is refused: that is a prefix.

Writing is RFC 5952 section 4: lower case, no leading zeros, the longest run of two or more zero groups as :: (the first of two equal runs), a single zero group written 0, and an IPv4-mapped address with its dotted tail - what Python 3.14's str writes. exploded is every group in four digits, and reverse_pointer the in-addr.arpa or ip6.arpa name (RFC 1035 section 3.5, RFC 3596 section 2.5). For a zoned address exploded keeps the zone, as Python 3.14.8 does, and reverse_pointer leaves it out, a PTR name having no place for one; 3.14.8 writes the zone reversed into the name. Python before 3.14.8 raised on both.

Order is Go's Addr.Compare: IPv4 before IPv6, then by value, then by zone; what Python's get_mixed_type_key sorts by, the zone added. next and prev are the neighbouring address, nil past either end of the family (Python raises there), the zone kept.

Classification follows Python 3.14, whose is_global reads the IANA IPv4 and IPv6 Special-Purpose Address Registries (RFC 6890 and its updates): global? is false for every block the registry marks not globally reachable, the exceptions inside them (192.0.0.9, 2001:3::/32, and the rest) being global again, and for 100.64.0.0/10 (RFC 6598's shared space, neither global nor private by Python's reading). loopback?, link_local?, multicast?, unspecified?, reserved? and site_local? are Python's predicates of those names. private? is not Python's is_private, which means "not globally reachable": it is Go's IsPrivate, RFC 1918's three IPv4 blocks and RFC 4193's fc00::/7, the question "is this one of the networks people number their own machines from". Every predicate of an IPv4-mapped address is the predicate of the IPv4 address inside it, as Python 3.13 made it - but site_local?, which Python reads off the IPv6 bits alone.

Prefix is an address and a length - a CIDR block, RFC 4632. parse reads what Python's ip_network reads: an address, then / and a decimal length (leading zeros allowed, as Python allows them), or for IPv4 a dotted netmask (/255.255.255.0) or hostmask (/0.0.0.255), all ones and all zeros read as netmasks; with no /, the address alone, at full length. Host bits set past the length are refused, as Python's strict=True refuses them, unless mask: true asks for them cleared. A zone is refused in a prefix, as Go refuses it, where Python keeps it. Prefix.new(ip, length) keeps the host bits it is given, as Go's PrefixFrom does, to hold an interface's address with its network's length; masked clears them, and everything that answers about the block - first, last, contains?, overlaps?, subnets, supernets - answers about the masked one. Containment never crosses families (an IPv4 address is in no IPv6 prefix, a mapped address in no IPv4 one) and ignores a zone, as Python's in does. subnets(m) answers at most 65,536 prefixes at once; subnet(m, i) reaches any one.

PrefixSet holds prefixes of both families in a binary trie, one node a bit: lookup answers the longest prefix holding an address, the routing table's question, in at most 33 or 129 steps however many prefixes it holds; contains? is an allowlist's question. Neither allocates, nor do Prefix#contains? and the predicates: 40,000 of those calls together allocated 0 bytes, plain and --release, measured by the exercise. collapsed answers the fewest prefixes covering exactly the same addresses, what Python's collapse_addresses answers per family.

What is wrong with text is a NetipError value naming the text and why. What panics is a program's own mistake: a length outside 0 to 32 or 0 to 128, a zone given to an IPv4 address, a zone with a %, a byte count that is neither 4 nor 16, a subnet length shorter than the prefix or a supernet length longer.

IPstruct

An IPv4 or IPv6 address, an IPv6 one with its zone ("" for none).

std/netipiyi doc src/std/netip.iyi
pub struct IP
  # IPv4 first, then by value, then by zone (none first).
  def <=>(other : IP) : Int32
  def ==(other : IP) : ::Bool
  # 32 or 128.
  def bit_length : Int32
  # Four or sixteen bytes, network order.
  def bytes : Array(UInt8)
  # Every IPv6 group in four digits (an IPv4-mapped tail kept dotted, as
  # Python writes it); IPv4 as `to_s` writes it.
  def exploded : String
  # Four bytes are an IPv4 address and sixteen an IPv6 one, network order,
  # as `bytes` answers them.
  def self.from_bytes(bytes : Array(UInt8)) : IP
  # Globally reachable by the IANA special-purpose registries, as Python
  # 3.14's `is_global` reads them.
  def global? : ::Bool
  def hash : Int32
  def initialize(*, value : UInt128, v6 : ::Bool, zone : String)
  def inspect : String
  # The IPv4 address inside an IPv4-mapped one (`::ffff:a.b.c.d`), or nil.
  def ipv4_mapped : IP | ::Nil
  # `::ffff:a.b.c.d` for an IPv4 address (RFC 4291 section 2.5.5.2).
  def ipv6_mapped : IP
  # 169.254.0.0/16 (RFC 3927), fe80::/10.
  def link_local? : ::Bool
  # 127.0.0.0/8, ::1.
  def loopback? : ::Bool
  protected def max_value : UInt128
  # 224.0.0.0/4, ff00::/8.
  def multicast? : ::Bool
  # The address after this one, nil after the family's last.
  def next : IP | ::Nil
  # Either family, as Python's `ip_address` reads it: IPv4 when it is
  # IPv4, else IPv6. Text with a colon can only be IPv6 and text without
  # one only IPv4, so the refusal is the one family's that applies.
  def self.parse(text : String) : IP | NetipError
  # Dotted decimal, four octets, Python's `IPv4Address`.
  def self.parse_v4(text : String) : IP | NetipError
  # RFC 4291 text with an optional `%zone`, Python's `IPv6Address`.
  def self.parse_v6(text : String) : IP | NetipError
  # The prefix of *length* bits holding this address, its host bits
  # cleared and its zone left behind.
  def prefix(length : Int32) : Prefix
  # The address before this one, nil before the family's first.
  def prev : IP | ::Nil
  # RFC 1918 (10/8, 172.16/12, 192.168/16) and RFC 4193 (fc00::/7): the
  # networks people number their own machines from, Go's `IsPrivate`.
  def private? : ::Bool
  # 240.0.0.0/4 (RFC 1112's class E), and for IPv6 the blocks RFC 4291
  # section 2.4 leaves unassigned: Python's `is_reserved`.
  def reserved? : ::Bool
  # The name a PTR record for this address is under: `4.3.2.1.in-addr.arpa`,
  # or thirty-two nibbles and `ip6.arpa`.
  def reverse_pointer : String
  # fec0::/10, deprecated by RFC 3879; read off the IPv6 bits alone, as
  # Python reads it, and never IPv4.
  def site_local? : ::Bool
  def to_s(io : IO) : Nil
  def to_s : String
  # The address's bits, network order, in the low 32 for IPv4.
  def to_u128 : UInt128
  # The IPv4 address inside an IPv4-mapped one, any other address itself.
  def unmap : IP
  # 0.0.0.0, ::.
  def unspecified? : ::Bool
  # The IPv4 address *a*.*b*.*c*.*d*.
  def self.v4(a : UInt8, b : UInt8, c : UInt8, d : UInt8) : IP
  # The IPv4 address whose 32 bits, network order, are *value*.
  def self.v4(value : UInt32) : IP
  def v4? : ::Bool
  # The IPv6 address whose 128 bits, network order, are *value*.
  def self.v6(value : UInt128) : IP
  def v6? : ::Bool
  # This address in *zone*, or with none for `""`. Python's rules for a
  # zone hold: no `%` in it. IPv4 has no zones.
  def with_zone(zone : String) : IP
  # The zone, or nil for none (and always for IPv4).
  def zone : String | ::Nil
end

NetipErrorstruct

Why a text is not an address or a prefix: message names the text.

std/netipiyi doc src/std/netip.iyi
pub struct NetipError
  def initialize(message : String)
  def to_s : String
end

Prefixstruct

An address and a length: a CIDR block (RFC 4632), with the address's host bits as it was given them.

std/netipiyi doc src/std/netip.iyi
pub struct Prefix
  # IPv4 first, then by first address, then shorter first, then by the
  # address as held - Python's order of networks, host bits after.
  def <=>(other : Prefix) : Int32
  def ==(other : Prefix) : ::Bool
  # 32 or 128.
  def bit_length : Int32
  # Whether *ip* is in the block: never across families, the zone not read.
  def contains?(ip : IP) : ::Bool
  # Whether all of *other* is in this block (Python's `supernet_of`).
  def contains?(other : Prefix) : ::Bool
  # The block's first address (Python's `network_address`).
  def first : IP
  def hash : Int32
  # The host bits as an address: `0.0.0.255` for a /24.
  def hostmask : IP
  # *ip* and *length* as they are, host bits and all; `IP#prefix` clears
  # them. A zoned address is refused: a block has no zone.
  def initialize(ip : IP, length : Int32)
  def inspect : String
  def ip : IP
  # The block's last address (Python's `broadcast_address`).
  def last : IP
  def length : Int32
  # This prefix with the address's host bits cleared.
  def masked : Prefix
  # The length as an address: `255.255.255.0` for a /24.
  def netmask : IP
  # Whether the two blocks share an address: as blocks nest or are
  # apart, one holds the other's first address or neither shares any.
  def overlaps?(other : Prefix) : ::Bool
  # Python's `ip_network(text, strict: !mask)`, a zone refused.
  def self.parse(text : String, mask : ::Bool = false) : Prefix | NetipError
  # The *index*th /*new_length* block of this one, from 0.
  def subnet(new_length : Int32, index : UInt128) : Prefix
  # Whether every address of this block is in *other*.
  def subnet_of?(other : Prefix) : ::Bool
  # The /*new_length* blocks this one splits into, in order.
  def subnets(new_length : Int32) : Array(Prefix)
  # The block of *new_length* holding this one.
  def supernet(new_length : Int32) : Prefix
  # The block one bit shorter; a /0 is its own.
  def supernet : Prefix
  def to_s(io : IO) : Nil
  def to_s : String
  def v4? : ::Bool
  def v6? : ::Bool
end

PrefixSetclass

Prefixes of both families in a binary trie, one node a bit: node 0 is the IPv4 root and node 1 the IPv6 one, and a node's children are made after it, so each has a larger index than its parent - which is what lets collapsed decide a node from its children in one backward pass. A removed prefix leaves its nodes behind, unmarked.

std/netipiyi doc src/std/netip.iyi
pub class PrefixSet
  # Adds *prefix*, masked; false when it was there.
  def add(prefix : Prefix) : ::Bool
  # The fewest prefixes holding exactly the addresses the set's do,
  # IPv4 first, in order: Python's `collapse_addresses`, per family.
  def collapsed : Array(Prefix)
  # Whether some prefix of the set holds *ip*.
  def contains?(ip : IP) : ::Bool
  # Removes *prefix*, masked; false when it was not there.
  def delete(prefix : Prefix) : ::Bool
  def empty? : ::Bool
  # Whether *prefix*, masked, is one of the set's - this prefix, not one
  # holding it; `lookup(prefix.first)` asks that.
  def includes?(prefix : Prefix) : ::Bool
  def initialize(prefixes : Array(Prefix))
  def initialize
  # The longest prefix of the set holding *ip*, or nil.
  def lookup(ip : IP) : Prefix | ::Nil
  # Every prefix of the set, IPv4 first, in `Prefix` order.
  def prefixes : Array(Prefix)
  # How many prefixes the set holds.
  def size : Int32
end

Error for NetipErrorimpl

std/netipiyi doc src/std/netip.iyi
impl Error for NetipError
  def message : String
end

Std::Traits::Comparable for IPimpl

std/netipiyi doc src/std/netip.iyi
impl Std::Traits::Comparable for IP
  def <=>(other : self) : Int32
end

Std::Traits::Comparable for Prefiximpl

std/netipiyi doc src/std/netip.iyi
impl Std::Traits::Comparable for Prefix
  def <=>(other : self) : Int32
end