A Zig library for parsing, validating, formatting and classifying IPv4 and IPv6 addresses and CIDR networks.
  • Zig 99.4%
  • Nix 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 6b1eb253e6
Speed up network and endpoint formatting
The composite types went through Writer.print, whose per-integer
formatting costs more than the whole value. Each now renders into a
local buffer and hands the writer the finished text, with the port and
prefix length written by a new internal decimal module as one branchless
word store, leading zeros pre-shifted out. The address bufPrint renders
directly into the caller's buffer when it has room for the word-store
slack, so the composite renders pay no intermediate copy; that also
speeds up Ipv6Address.format itself. The port parser becomes a single
pass to match the octet and group parsers.

The separator searches stay on std.mem.indexOfScalar: unlike the scans
inside an address, the ']' of an IPv6 endpoint sits far enough in that
the vectorized search beats a plain loop.

Measured with zig build bench on x86-64: IPv4 network format
16.4 -> 15.7 ns and parse 22.1 -> 19.7; IPv6 network format
30.0 -> 24.7; IPv4 endpoint format 19.5 -> 15.5; IPv6 endpoint format
42.2 -> 27.2; IPv6 address format 26.3 -> 22.9. The contains checks were
already fractions of a nanosecond and are unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LmXwdQjknPtR5e1Uq1YZNL
2026-08-28 22:31:22 -05:00
.forgejo/workflows Add a Forgejo Actions workflow 2026-08-27 21:52:01 -05:00
LICENSES Add a Forgejo Actions workflow 2026-08-27 21:52:01 -05:00
src Speed up network and endpoint formatting 2026-08-28 22:31:22 -05:00
tools Benchmark the network and endpoint types too 2026-08-28 22:31:11 -05:00
.gitignore Add IPv4 and IPv6 address and network library 2026-08-27 21:44:35 -05:00
build.zig Add a zig build bench microbenchmark step 2026-08-28 20:13:13 -05:00
build.zig.zon Strip the zig init boilerplate from the build files 2026-08-27 23:13:06 -05:00
flake.lock Add IPv4 and IPv6 address and network library 2026-08-27 21:44:35 -05:00
flake.nix Add a kcov coverage report 2026-08-27 23:30:26 -05:00
README.md Speed up network and endpoint formatting 2026-08-28 22:31:22 -05:00
REUSE.toml Add a Forgejo Actions workflow 2026-08-27 21:52:01 -05:00

z46

A Zig library for parsing, validating, formatting and classifying IPv4 and IPv6 addresses, CIDR networks, and the endpoints that pair an address with a port.

Everything is allocation-free and works at comptime as readily as at runtime. These are values, not sockets: nothing here opens a connection or touches the network, and conversions to and from the std.Io.net socket address types are provided for when you need one.

The API reference is published from the default branch. zig build docs-serve builds the same pages and serves them at http://127.0.0.1:8000/, with -Ddocs-port=N to choose another port. A server is needed rather than opening the files directly, because the viewer fetches its data at runtime and a browser refuses that from a file:// page; zig std works this way for the same reason. zig build docs alone just writes the pages to zig-out/docs.

Quick start

const z46 = @import("z46");

const network = try z46.Ipv4Network.parse("192.168.1.0/24");
const address = try z46.Ipv4Address.parse("192.168.1.42");

std.debug.assert(network.contains(address));
std.debug.assert(address.isPrivate());

// {f} formats any of these types in canonical form.
std.debug.print("{f} in {f}\n", .{ address, network });

When the version is not known ahead of time, use the union types:

const address = try z46.Address.parse("2001:db8::1");
switch (address) {
    .ipv4 => |v4| ...,
    .ipv6 => |v6| ...,
}

Parsing the host out of a URL, where an IPv6 address arrives in brackets and an IPv4 address does not:

const host = try z46.Address.parseWithOptions(text, .{ .allow_brackets = true });

When the port comes along too, parse the authority as an endpoint:

const endpoint = try z46.Endpoint.parse("[2001:db8::1]:443");
const connect_to = endpoint.toStdAddress(); // a std.Io.net.IpAddress

Validation alone needs no error handling:

if (!z46.Network.isValid(user_input)) return error.BadConfig;

Types

Type Holds
Ipv4Address a 32-bit address
Ipv6Address a 128-bit address
Ipv4Network an IPv4 network in CIDR notation
Ipv6Network an IPv6 network in CIDR notation
Ipv4Endpoint an IPv4 address paired with a port
Ipv6Endpoint an IPv6 address paired with a port
Address a tagged union of either address type
Network a tagged union of either network type
Endpoint a tagged union of either endpoint type
Family .ipv4 or .ipv6

Every type provides parse, isValid, format (via the {f} specifier), bufPrint, eql and order. Addresses add fromInt/toInt, conversions between the two versions, and a set of classification predicates (isLoopback, isPrivate, isLinkLocal, isMulticast, isDocumentation, isGlobal and more) that follow the IANA special-purpose address registries. Networks add netmask, hostmask, networkAddress, broadcastAddress or lastAddress, addressCount, contains, containsNetwork, overlaps, supernet, and the subnets, addresses and hosts iterators. Endpoints hold an address and a port and convert to and from the standard library's std.Io.net socket address types.

Parsing is strict

Only canonical text is accepted, because the alternative is a security problem: a parser that reads 192.168.01.1 as octal and another that reads it as decimal will disagree about which network an address belongs to.

  • Exactly four decimal octets for IPv4. The inet_aton shorthands (127.1, 0x7f.0.0.1, octal octets) are rejected.
  • No leading zeros in an IPv4 octet or in a prefix length.
  • No leading or trailing whitespace.
  • IPv6 accepts :: compression, uppercase hex and a trailing dotted-quad (::ffff:192.0.2.1), and rejects a :: that compresses nothing.
  • A zone identifier (fe80::1%eth0) is rejected by Ipv6Address.parse; Ipv6Address.parseZoned accepts one and hands back the zone text.
  • The square brackets a URL puts around an IPv6 address are rejected too. Ipv6Address.parseWithOptions(text, .{ .allow_brackets = true }) accepts both [2001:db8::1] and the bare form, with parseZonedWithOptions doing the same for [fe80::1%eth0] and Address.parseWithOptions taking the same option for text that could be either version. Only the address is parsed, so an authority such as [2001:db8::1]:443 has to have its port split off first.
  • An endpoint is an address and a port, so a bare address is error.MissingPort. An IPv6 endpoint must bracket its address ([2001:db8::1]:443); without brackets the : before the port cannot be told from the colons in the address, which is reported as error.MissingBrackets rather than as a malformed address. Ports are parsed as strictly as everything else: no leading zeros, and nothing above 65535. Port 0 is accepted, since that is how a caller asks the operating system to pick one.
  • Networks must have their host bits clear: 192.168.1.1/24 is error.HasHostBits. Use parseTruncate to clear them instead. An IPv4 network may be written with a dotted netmask (10.0.0.0/255.0.0.0), and text with no / is read as a single host.

Errors are specific — error.LeadingZero, error.OctetOutOfRange, error.RedundantEllipsis, error.HasHostBits — so a program can explain what was wrong with what a user typed.

Output is canonical too: IPv6 follows RFC 5952 (lowercase, no leading zeros, longest run of zero groups compressed, leftmost run on a tie). IPv4-mapped addresses print as ::ffff:a.b.c.d; the deprecated IPv4-compatible range ::/96 prints in plain hex, so ::3 does not come out as ::0.0.0.3.

Using it as a dependency

zig fetch --save https://codeberg.org/jcollie/z46/archive/v0.1.0.tar.gz

Fetching a tag keeps the hash stable; archive/main.tar.gz tracks the branch instead and changes hash on every push.

// build.zig
const z46 = b.dependency("z46", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("z46", z46.module("z46"));

Command line tool

The package also builds a small z46 binary that validates and describes its arguments. Text holding a / is read as a network, text carrying a port as an endpoint, and everything else as an address:

$ z46 192.168.1.42 '[2001:db8::1]:443' 2001:db8::/126
192.168.1.42
  family     IPv4
  canonical  192.168.1.42
  scope      private
  as IPv6    ::ffff:192.168.1.42

[2001:db8::1]:443
  family     IPv6
  canonical  [2001:db8::1]:443
  address    2001:db8::1
  port       443
  scope      documentation

2001:db8::/126
  family     IPv6
  canonical  2001:db8::/126
  network    2001:db8::
  netmask    ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc
  hostmask   ::3
  last       2001:db8::3
  addresses  4

It exits 1 if any argument is invalid, so z46 -q "$input" works as a validation check in a script.

Tests

zig build test

The suite covers the RFC text forms, the malformed text each error is meant to catch, the classification registries, iteration at the edges of the address space, and 10,000 seeded random round trips per type. There are also two std.testing.fuzz entry points, which run once each under zig build test.

zig build coverage runs the library's tests under kcov and writes an HTML report to zig-out/coverage, currently 98.82% of the 12 source files. It needs kcov on the PATH, which the nix dev shell provides. The CLI and documentation-server tests are not included: kcov runs those binaries without complaint but records no lines from them.

Benchmarks

zig build bench

This times the parsing, formatting and classification paths of every type over seeded random corpora, always as a ReleaseFast build whatever -Doptimize says, and reports the best of twenty passes for each. On one 2026 x86-64 machine, parsing sits at around 16 ns for an IPv4 address and 4560 ns for an IPv6 address depending on its shape, and formatting at around 11 ns and 23 ns; the network and endpoint types cost a few nanoseconds beyond the address they wrap, and a contains check is fractions of a nanosecond. Run it on your own machine rather than trusting those numbers.

RFCs referenced

The behaviour of this library is drawn from the documents below. Each is cited in a doc comment next to the code that implements it, so zig build docs and the source agree with this table.

Notation and text representation

RFC Title Used for
791 Internet Protocol Dotted-decimal IPv4 addresses; 0.0.0.0/8 as "this network"
3986 Uniform Resource Identifier (URI): Generic Syntax The square brackets around an IPv6 address in a URL authority, accepted by Ipv6Address.parseWithOptions
4007 IPv6 Scoped Address Architecture Zone identifiers (fe80::1%eth0), accepted by Ipv6Address.parseZoned
4291 IP Version 6 Addressing Architecture The IPv6 text form: :: compression, the trailing dotted-quad, ::, ::1, IPv4-mapped and IPv4-compatible addresses
4632 Classless Inter-domain Routing (CIDR): The Internet Address Assignment and Aggregation Plan The address/prefix-length notation both network types parse and print
5952 A Recommendation for IPv6 Address Text Representation Canonical IPv6 output: lowercase, no leading zeros, longest zero run compressed, leftmost run on a tie
6874 Representing IPv6 Zone Identifiers in Address Literals and Uniform Resource Identifiers The %25 escape for a zone in a URI, which parseZonedWithOptions leaves for the caller to undo

Address structure

RFC Title Used for
3021 Using 31-Bit Prefixes on IPv4 Point-to-Point Links Both addresses of an IPv4 /31 counting as usable hosts
6890 Special-Purpose IP Address Registries The registry framework behind isGlobal; 192.0.0.0/24 IETF protocol assignments
7346 IPv6 Multicast Address Scopes The scope nibble returned by Ipv6Address.multicastScope

IPv4 special-purpose ranges

RFC Title Range
919 Broadcasting Internet Datagrams 255.255.255.255, the limited broadcast address
1112 Host extensions for IP multicasting 240.0.0.0/4, reserved for future use
1122 Requirements for Internet Hosts - Communication Layers 127.0.0.0/8, loopback
1918 Address Allocation for Private Internets 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
2544 Benchmarking Methodology for Network Interconnect Devices 198.18.0.0/15
3927 Dynamic Configuration of IPv4 Link-Local Addresses 169.254.0.0/16
5737 IPv4 Address Blocks Reserved for Documentation 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24
5771 IANA Guidelines for IPv4 Multicast Address Assignments 224.0.0.0/4
6598 IANA-Reserved IPv4 Prefix for Shared Address Space 100.64.0.0/10, carrier-grade NAT
7526 Deprecating the Anycast Prefix for 6to4 Relay Routers 192.88.99.0/24

IPv6 special-purpose ranges

RFC Title Range
3056 Connection of IPv6 Domains via IPv4 Clouds 2002::/16, 6to4
3849 IPv6 Address Prefix Reserved for Documentation 2001:db8::/32
4193 Unique Local IPv6 Unicast Addresses fc00::/7
4380 Teredo: Tunneling IPv6 over UDP through Network Address Translations (NATs) 2001::/32
6052 IPv6 Addressing of IPv4/IPv6 Translators 64:ff9b::/96, the NAT64 well-known prefix
6666 A Discard Prefix for IPv6 100::/64
7343 An IPv6 Prefix for Overlay Routable Cryptographic Hash Identifiers Version 2 (ORCHIDv2) 2001:20::/28

Ranges that RFC 4291 defines directly — ::, ::1, ::ffff:0:0/96, ::/96, fe80::/10 and ff00::/8 — are listed under notation above rather than repeated here.

Requirements

Zig 0.16.0 or newer.