net.ipv6
Parse IPv6 addresses and write them in RFC 5952 canonical form: lower-case, zeros compressed, IPv4 embedded.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 47 tests, run in TypeScript, Python and Rust.parseIpv6 22 · formatIpv6 15 · isIpv6 10
What it does
Parse IPv6 text to its eight 16-bit groups, write groups back as the one canonical text RFC 5952 defines, and check text without raising. The canonical form of any address is `formatIpv6(parseIpv6(text))`.
## Why canonical text matters
The functions
A group: 3 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.
- parseIpv6 (text: string) -> int[]
- formatIpv6 (groups: int[]) -> string
- isIpv6 (text: string) -> bool
Once installed, your code imports each one from the group's module.
parseIpv6 throws on bad input 22 tests
export function parseIpv6(text: string): readonly number[]
| text | string | RFC 4291 text: hex groups, at most one ::, optionally a dotted IPv4 tail; no zone id or prefix |
| returns | int[] | the eight 16-bit groups, each 0 to 65535 |
For example
parseIpv6(2001:db8::1)→ 8,193, 3,512, 0, 0, 0, 0, 0, 1 documentation address with ::parseIpv6(::)→ 0, 0, 0, 0, 0, 0, 0, 0 the unspecified address is all zerosparseIpv6(::1)→ 0, 0, 0, 0, 0, 0, 0, 1 loopback
import { parseIpv6 } from "#fune/net.ipv6@^1";
import { ipv4Value } from "./net_ipv4_parse_ipv4.ts";
/**
* The eight 16-bit groups of an IPv6 address written as RFC 4291 section 2.2
* allows: hex groups of one to four digits in either case, at most one "::"
* standing for one or more zero groups, and optionally a dotted IPv4 address
* as the last 32 bits. A zone id ("%eth0") or a prefix ("/64") is not part of
* an address and is refused, as is anything around it (spaces, newlines).
*/
export function parseIpv6(text: string): readonly number[] {
const groups = ipv6Groups(text);
if (groups === null) throw new Error(`"${String(text)}" is not an IPv6 address`);
return groups;
}
// Exported for isIpv6 and for capabilities that parse IPv6 text without
// wanting an error; not part of the group's contract.
/** The eight groups, or null when the text is not an IPv6 address. */
export function ipv6Groups(text: unknown): number[] | null {
if (typeof text !== "string" || text.length === 0) return null;
for (const ch of text) {
const ok = (ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "f") || (ch >= "A" && ch <= "F") || ch === ":" || ch === ".";
if (!ok) return null;
}
const at = text.indexOf("::");
if (at < 0) {
const groups = pieces(text.split(":"), true);
return groups !== null && groups.length === 8 ? groups : null;
}
const before = text.slice(0, at);
const after = text.slice(at + 2);
if (after.includes("::")) return null;
const head = pieces(before === "" ? [] : before.split(":"), false);
const tail = pieces(after === "" ? [] : after.split(":"), true);
if (head === null || tail === null) return null;
// "::" stands for at least one group, so at most seven are written.
if (head.length + tail.length > 7) return null;
const zeros = new Array<number>(8 - head.length - tail.length).fill(0);
return [...head, ...zeros, ...tail];
}
/** Hex pieces to groups; a dotted IPv4 tail only as the very last piece. */
function pieces(parts: readonly string[], mayEndWithIpv4: boolean): number[] | null {
const groups: number[] = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.includes(".")) {
if (!mayEndWithIpv4 || i !== parts.length - 1) return null;
const v4 = ipv4Value(part);
if (v4 === null) return null;
groups.push(Math.floor(v4 / 65536), v4 % 65536);
continue;
}
if (part.length < 1 || part.length > 4) return null;
groups.push(parseInt(part, 16));
}
return groups;
}formatIpv6 throws on bad input 15 tests
export function formatIpv6(groups: readonly number[]): string
| groups | int[] | exactly eight integers 0 to 65535 |
| returns | string | RFC 5952 canonical text; ::ffff:0:0/96 (IPv4-mapped) keeps a dotted-quad tail |
For example
formatIpv6(8,193, 3,512, 0, 0, 0, 0, 0, 1)→ 2001:db8::1 compresses the zero runformatIpv6(0, 0, 0, 0, 0, 0, 0, 0)→ :: all zeros is ::formatIpv6(0, 0, 0, 0, 0, 0, 0, 1)→ ::1 loopback
import { formatIpv6 } from "#fune/net.ipv6@^1";
import { formatIpv4 } from "./net_ipv4_format_ipv4.ts";
/**
* RFC 5952 canonical text for eight 16-bit groups: lower-case hex, no
* leading zeros, the longest run of two or more zero groups written "::" (the
* first on a tie, never a single group). An IPv4-mapped address
* (::ffff:0:0/96) keeps its dotted-quad tail, as section 5 recommends.
*/
export function formatIpv6(groups: readonly number[]): string {
if (!Array.isArray(groups) || groups.length !== 8) {
throw new Error("IPv6 groups must be eight integers from 0 to 65535");
}
for (const g of groups) {
if (typeof g !== "number" || !Number.isInteger(g) || g < 0 || g > 65535) {
throw new Error("IPv6 groups must be eight integers from 0 to 65535");
}
}
if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 0xffff) {
return "::ffff:" + formatIpv4(groups[6] * 65536 + groups[7]);
}
let bestStart = -1;
let bestLength = 0;
let i = 0;
while (i < 8) {
if (groups[i] !== 0) {
i++;
continue;
}
let j = i;
while (j < 8 && groups[j] === 0) j++;
// Strictly longer, so the first of two equal runs wins.
if (j - i > bestLength) {
bestStart = i;
bestLength = j - i;
}
i = j;
}
const hex = groups.map((g) => g.toString(16));
if (bestLength < 2) return hex.join(":");
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLength).join(":");
}isIpv6 10 tests
export function isIpv6(text: string): boolean
| text | string | any text; true exactly when parseIpv6 would accept it |
| returns | bool |
For example
isIpv6(::1)→ true loopbackisIpv6(2001:db8::1)→ true documentation addressisIpv6(::ffff:192.0.2.1)→ true IPv4-mapped
import { isIpv6 } from "#fune/net.ipv6@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { ipv6Groups } from "./net_ipv6_parse_ipv6.ts"; ← parseIpv6, another function of this group · built into the same file, even by a slim install
/** True exactly when parseIpv6 would accept the text; never throws. */
export function isIpv6(text: string): boolean {
return ipv6Groups(text) !== null;
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add net.ipv6
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add net.ipv6 --only parseIpv6
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.ipv6-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.ipv6@1.0.0:typescript.
The whole function, every language, is one file too: net.ipv6-1.0.0.fune, 25,384 bytes, sha256 f6cadea3f28991bad7dc0085b557c9d1e5f7db5ed39785b4040b78946ff72b56. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before net.ipv6.parseIpv6
// fune: before net.ipv6.formatIpv6
// fune: before net.ipv6.isIpv6
after — your function gets the result and the arguments, and returns the final result.
// fune: after net.ipv6.parseIpv6
// fune: after net.ipv6.formatIpv6
// fune: after net.ipv6.isIpv6
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace net.ipv4 in net.ipv6
step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.ipv6 --steps.
// fune: step net.ipv6.<fn> after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
parseIpv6 22 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| documentation address with :: | 2001:db8::1 | → | 8,193, 3,512, 0, 0, 0, 0, 0, 1 |
| the unspecified address is all zeros | :: | → | 0, 0, 0, 0, 0, 0, 0, 0 |
| loopback | ::1 | → | 0, 0, 0, 0, 0, 0, 0, 1 |
| :: at the end | fe80:: | → | 65,152, 0, 0, 0, 0, 0, 0, 0 |
| full form, upper case and leading zeros | 2001:0DB8:0000:0000:0000:ff00:0042:8329 | → | 8,193, 3,512, 0, 0, 0, 65,280, 66, 33,577 |
| IPv4-mapped with a dotted tail: 192.0 is c000, 2.128 is 0280 | ::ffff:192.0.2.128 | → | 0, 0, 0, 0, 0, 65,535, 49,152, 640 |
| NAT64 well-known prefix with a dotted tail | 64:ff9b::192.0.2.33 | → | 100, 65,435, 0, 0, 0, 0, 49,152, 545 |
| six groups and a dotted tail without :: | 1:2:3:4:5:6:1.2.3.4 | → | 1, 2, 3, 4, 5, 6, 258, 772 |
| seven groups and :: standing for one zero group | 1:2:3:4:5:6:7:: | → | 1, 2, 3, 4, 5, 6, 7, 0 |
| two :: are ambiguous | 2001:db8::1::1 | → | error: is not an IPv6 address |
Show the other 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| nine groups | 1:2:3:4:5:6:7:8:9 | → | error: is not an IPv6 address |
| seven groups without :: | 1:2:3:4:5:6:7 | → | error: is not an IPv6 address |
| eight groups and :: (:: must stand for at least one) | 1:2:3:4:5:6:7:8:: | → | error: is not an IPv6 address |
| a five-digit group | 12345:: | → | error: is not an IPv6 address |
| a zone id is not part of the address | fe80::1%eth0 | → | error: is not an IPv6 address |
| a prefix length is not part of the address | 2001:db8::/32 | → | error: is not an IPv6 address |
| a dotted part that is not last | ::192.0.2.1:1 | → | error: is not an IPv6 address |
| the dotted tail follows the IPv4 rules (no leading zeros) | ::ffff:01.2.3.4 | → | error: is not an IPv6 address |
| a single leading colon | :1:: | → | error: is not an IPv6 address |
| a trailing newline | ::1 | → | error: is not an IPv6 address |
| a non-ASCII digit | ١::1 | → | error: is not an IPv6 address |
| empty text | → | error: is not an IPv6 address |
formatIpv6 15 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| compresses the zero run | 8,193, 3,512, 0, 0, 0, 0, 0, 1 | → | 2001:db8::1 |
| all zeros is :: | 0, 0, 0, 0, 0, 0, 0, 0 | → | :: |
| loopback | 0, 0, 0, 0, 0, 0, 0, 1 | → | ::1 |
| a run at the end | 65,152, 0, 0, 0, 0, 0, 0, 0 | → | fe80:: |
| the longest run wins, not the first (RFC 5952 4.2.3) | 8,193, 3,512, 0, 1, 0, 0, 0, 1 | → | 2001:db8:0:1::1 |
| equal runs: the first is compressed | 8,193, 3,512, 0, 0, 1, 0, 0, 1 | → | 2001:db8::1:0:0:1 |
| equal runs at both ends: the first | 0, 0, 1, 2, 3, 4, 0, 0 | → | ::1:2:3:4:0:0 |
| a single zero group is never :: (RFC 5952 4.2.2) | 8,193, 3,512, 0, 1, 1, 1, 1, 1 | → | 2001:db8:0:1:1:1:1:1 |
| lower case, leading zeros dropped | 8,193, 3,512, 0, 0, 0, 65,280, 66, 33,577 | → | 2001:db8::ff00:42:8329 |
| IPv4-mapped keeps a dotted tail (RFC 5952 5) | 0, 0, 0, 0, 0, 65,535, 49,152, 640 | → | ::ffff:192.0.2.128 |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| not mapped (a non-zero fifth group): plain hex | 0, 0, 0, 0, 1, 65,535, 49,152, 640 | → | ::1:ffff:c000:280 |
| seven groups | 1, 2, 3, 4, 5, 6, 7 | → | error: IPv6 groups must be eight integers from 0 to 65535 |
| a group above 65535 | 65,536, 0, 0, 0, 0, 0, 0, 0 | → | error: IPv6 groups must be eight integers from 0 to 65535 |
| a negative group | 0, 0, 0, 0, 0, 0, 0, -1 | → | error: IPv6 groups must be eight integers from 0 to 65535 |
| a fractional group | 0, 0, 0, 0, 0, 0, 0, 1.5 | → | error: IPv6 groups must be eight integers from 0 to 65535 |
isIpv6 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| loopback | ::1 | → | true |
| documentation address | 2001:db8::1 | → | true |
| IPv4-mapped | ::ffff:192.0.2.1 | → | true |
| upper case | FE80::1 | → | true |
| a zone id | fe80::1%eth0 | → | false |
| an IPv4 address | 1.2.3.4 | → | false |
| a trailing newline | ::1 | → | false |
| three colons | 2001:db8:::1 | → | false |
| two :: | 1::2::3 | → | false |
| empty text | → | false |
More from the author
`2001:DB8:0:0:1:0:0:1`, `2001:db8::1:0:0:1` and `2001:0db8:0:0:1::1` are the same address. Compare them as text, log them, or use them as a map key, and they are three. RFC 5952 fixes one spelling:
- lower-case hex, no leading zeros in a group; - the longest run of two or more zero groups becomes `::`; on a tie the first run; a single zero group is written `0`, never `::`; - an IPv4-mapped address (`::ffff:0:0/96`) keeps its dotted-quad tail (`::ffff:192.0.2.128`), as section 5 recommends. Other prefixes that can carry IPv4 (the deprecated `::a.b.c.d` compatible form, `64:ff9b::/96`) are written in hex; the parser accepts a dotted tail on any of them.
## What parses
RFC 4291 section 2.2 text: one to four hex digits per group in either case, at most one `::` (standing for at least one zero group), and optionally a dotted IPv4 address, following `net.ipv4`'s strict rules, as the last 32 bits. Refused: a zone id (`fe80::1%eth0`, RFC 4007: it names an interface, it is not part of the address), a prefix (`/64`, see `net.cidr`), brackets, whitespace and non-ASCII digits.
`ipv6Groups` (`ipv6_groups`) is exported for sibling capabilities that want "groups or null"; it is not part of the contract.
Sources: RFC 4291 (IP Version 6 Addressing Architecture) section 2.2; RFC 5952 (A Recommendation for IPv6 Address Text Representation) sections 4 and 5.