- Zig 99.4%
- Nix 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
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_atonshorthands (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 byIpv6Address.parse;Ipv6Address.parseZonedaccepts 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, withparseZonedWithOptionsdoing the same for[fe80::1%eth0]andAddress.parseWithOptionstaking the same option for text that could be either version. Only the address is parsed, so an authority such as[2001:db8::1]:443has 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 aserror.MissingBracketsrather 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/24iserror.HasHostBits. UseparseTruncateto 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 45–60 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.