Functional Weave
Code in TypeScript

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.

  1. parseIpv6 (text: string) -> int[]
  2. formatIpv6 (groups: int[]) -> string
  3. 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[]
textstringRFC 4291 text: hex groups, at most one ::, optionally a dotted IPv4 tail; no zone id or prefix
returnsint[]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 zeros
  • parseIpv6(::1) → 0, 0, 0, 0, 0, 0, 0, 1 loopback
import { parseIpv6 } from "#fune/net.ipv6@^1";
impl/typescript/parse_ipv6.ts · 59 lines · open · raw
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
groupsint[]exactly eight integers 0 to 65535
returnsstringRFC 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 run
  • formatIpv6(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";
impl/typescript/format_ipv6.ts · 41 lines · open · raw
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
textstringany text; true exactly when parseIpv6 would accept it
returnsbool

For example

  • isIpv6(::1) → true loopback
  • isIpv6(2001:db8::1) → true documentation address
  • isIpv6(::ffff:192.0.2.1) → true IPv4-mapped
import { isIpv6 } from "#fune/net.ipv6@^1";
impl/typescript/is_ipv6.ts · 6 lines · open · raw

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
Download for TypeScript net.ipv6-1.0.0-typescript.fune · 16,015 bytes sha256 be5bfbf90d0edb5a3f0d620c9314c7c48c064b8ff230245e53c9bbec10a67278

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

CaseArgumentsExpected
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
CaseArgumentsExpected
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

CaseArgumentsExpected
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
CaseArgumentsExpected
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

CaseArgumentsExpected
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.

Files

PathBytes
README.md1,624
impl/python/format_ipv6.py1,435
impl/python/is_ipv6.py196
impl/python/parse_ipv6.py2,279
impl/rust/format_ipv6.rs1,820
impl/rust/is_ipv6.rs307
impl/rust/parse_ipv6.rs2,786
impl/typescript/format_ipv6.ts1,482
impl/typescript/is_ipv6.ts214
impl/typescript/parse_ipv6.ts2,491
vectors.json5,987