Functional Weave
Code in TypeScript

net.cidr

CIDR blocks: network, broadcast, first and last host, host count, netmask and prefix, and membership.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 70 tests, run in TypeScript, Python and Rust.cidrInfo 15 · cidrContains 18 · isCidr 13 · prefixToNetmask 12 · netmaskToPrefix 12

What it does

A subnet calculator as five functions that only make sense together: describe an IPv4 block (`cidrInfo`), test membership for IPv4 or IPv6 (`cidrContains`), check a block without raising (`isCidr`), and convert between prefix lengths and dotted netmasks.

## Decisions

The functions

A group: 5 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. cidrInfo (cidr: string) -> CidrInfo
  2. cidrContains (cidr: string, address: string) -> bool
  3. isCidr (text: string) -> bool
  4. prefixToNetmask (prefix: int) -> string
  5. netmaskToPrefix (netmask: string) -> int

The type it declares, generated into your project

/** Everything a subnet calculator shows for one IPv4 block. */
export interface CidrInfo {
  /** the block with host bits cleared, e.g. 192.168.1.0/24 */
  readonly cidr: string;
  readonly network: string;
  readonly prefix: number;
  readonly netmask: string;
  /** the inverse mask, as ACLs write it */
  readonly wildcard: string;
  /** null for /31 and /32, which have none */
  readonly broadcast: string | null;
  readonly firstHost: string;
  readonly lastHost: string;
  /** usable addresses: all of them for /31 (RFC 3021) and /32 */
  readonly hostCount: number;
  readonly addressCount: number;
}

Once installed, your code imports each one from the group's module.

cidrInfo throws on bad input 15 tests

export function cidrInfo(cidr: string): CidrInfo
cidrstringan IPv4 block, address/prefix, e.g. 192.168.1.0/24; host bits may be set and are dropped
returnsCidrInfo

For example

  • cidrInfo(192.168.1.0/24) → cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun… a /24
  • cidrInfo(192.168.1.77/24) → cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun… an interface address: host bits are dropped
  • cidrInfo(10.0.0.0/8) → cidr 10.0.0.0/8, network 10.0.0.0, prefix 8, netmask 255.0.0.0, wildcard 0.255.255.255, broadcast 10.255.255.255, first host 10.0.0.1, last host 10.255.255.254, host count 16,777,… a /8
import { cidrInfo } from "#fune/net.cidr@^1";
impl/typescript/cidr_info.ts · 33 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 { formatIpv4 } from "./net_ipv4_format_ipv4.ts";
import { cidrBlock } from "./net_cidr_is_cidr.ts";  ← isCidr, another function of this group · built into the same file, even by a slim install
import { type CidrInfo } from "./net_cidr_types.ts";

/**
 * Everything a subnet calculator shows for one IPv4 block. Host bits in the
 * input are dropped (192.168.1.77/24 describes 192.168.1.0/24), since that is
 * how an interface address is usually written. A /31 is a point-to-point link
 * with two usable addresses and no broadcast (RFC 3021); a /32 is one host.
 */
export function cidrInfo(cidr: string): CidrInfo {
  const block = cidrBlock(cidr);
  if (block === null) throw new Error(`"${String(cidr)}" is not a CIDR block`);
  if (block.version !== 4) throw new Error(`cidrInfo handles IPv4 blocks only, received "${cidr}"`);
  const prefix = block.prefix;
  const size = 2 ** (32 - prefix);
  // Arithmetic rather than bit masks: JavaScript's & is signed 32-bit.
  const network = block.bits[0] - (block.bits[0] % size);
  const last = network + size - 1;
  const pointToPoint = prefix >= 31;
  return {
    cidr: `${formatIpv4(network)}/${prefix}`,
    network: formatIpv4(network),
    prefix,
    netmask: formatIpv4(4294967296 - size),
    wildcard: formatIpv4(size - 1),
    broadcast: pointToPoint ? null : formatIpv4(last),
    firstHost: formatIpv4(pointToPoint ? network : network + 1),
    lastHost: formatIpv4(pointToPoint ? last : last - 1),
    hostCount: pointToPoint ? size : size - 2,
    addressCount: size,
  };
}

cidrContains throws on bad input 18 tests

export function cidrContains(cidr: string, address: string): boolean
cidrstringan IPv4 or IPv6 block
addressstringan IPv4 or IPv6 address; one of the other family is never contained
returnsbool

For example

  • cidrContains(192.168.1.0/24, 192.168.1.200) → true inside a /24
  • cidrContains(192.168.1.0/24, 192.168.2.1) → false the next /24 is outside
  • cidrContains(10.0.0.0/8, 10.255.255.255) → true the last address of a /8
import { cidrContains } from "#fune/net.cidr@^1";
impl/typescript/cidr_contains.ts · 28 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 { ipv4Value } from "./net_ipv4_parse_ipv4.ts";
import { ipv6Groups } from "./net_ipv6_parse_ipv6.ts";
import { cidrBlock } from "./net_cidr_is_cidr.ts";  ← isCidr, another function of this group · built into the same file, even by a slim install

/**
 * Whether an address lies inside a block, IPv4 or IPv6. An address of the
 * other family is never inside (an IPv4-mapped IPv6 address is not treated
 * as its IPv4 address; map it yourself if you mean that).
 */
export function cidrContains(cidr: string, address: string): boolean {
  const block = cidrBlock(cidr);
  if (block === null) throw new Error(`"${String(cidr)}" is not a CIDR block`);
  const v4 = ipv4Value(address);
  const v6 = v4 === null ? ipv6Groups(address) : null;
  if (v4 === null && v6 === null) throw new Error(`"${String(address)}" is not an IP address`);
  if (block.version === 4) {
    if (v4 === null) return false;
    const size = 2 ** (32 - block.prefix);
    return Math.floor(v4 / size) === Math.floor(block.bits[0] / size);
  }
  if (v6 === null) return false;
  for (let i = 0; i < 8; i++) {
    const bits = Math.max(0, Math.min(16, block.prefix - 16 * i));
    const size = 2 ** (16 - bits);
    if (Math.floor(v6[i] / size) !== Math.floor(block.bits[i] / size)) return false;
  }
  return true;
}

isCidr 13 tests

export function isCidr(text: string): boolean
textstringany text; true for a well-formed IPv4 or IPv6 block
returnsbool

For example

  • isCidr(10.0.0.0/8) → true an IPv4 block
  • isCidr(2001:db8::/32) → true an IPv6 block
  • isCidr(0.0.0.0/0) → true /0
import { isCidr } from "#fune/net.cidr@^1";
impl/typescript/is_cidr.ts · 34 lines · open · raw
import { ipv4Value } from "./net_ipv4_parse_ipv4.ts";
import { ipv6Groups } from "./net_ipv6_parse_ipv6.ts";

/** True for a well-formed IPv4 (prefix 0-32) or IPv6 (prefix 0-128) block; never throws. */
export function isCidr(text: string): boolean {
  return cidrBlock(text) !== null;
}

// Exported for the other functions of this group; not part of its contract.

/** A parsed block: version 4 with bits [address], or version 6 with the eight groups. */
export interface CidrBlock {
  version: number;
  bits: number[];
  prefix: number;
}

/** The block, or null when the text is not address/prefix with a strict decimal prefix. */
export function cidrBlock(text: unknown): CidrBlock | null {
  if (typeof text !== "string") return null;
  const slash = text.indexOf("/");
  if (slash < 0 || text.indexOf("/", slash + 1) >= 0) return null;
  const address = text.slice(0, slash);
  const digits = text.slice(slash + 1);
  if (digits.length < 1 || digits.length > 3) return null;
  for (const ch of digits) if (ch < "0" || ch > "9") return null;
  if (digits.length > 1 && digits[0] === "0") return null;
  const prefix = Number(digits);
  const v4 = ipv4Value(address);
  if (v4 !== null) return prefix <= 32 ? { version: 4, bits: [v4], prefix } : null;
  const v6 = ipv6Groups(address);
  if (v6 !== null) return prefix <= 128 ? { version: 6, bits: v6, prefix } : null;
  return null;
}

prefixToNetmask throws on bad input 12 tests

export function prefixToNetmask(prefix: number): string
prefixint0 to 32
returnsstring

For example

  • prefixToNetmask(24) → 255.255.255.0 /24
  • prefixToNetmask(0) → 0.0.0.0 /0
  • prefixToNetmask(8) → 255.0.0.0 /8
import { prefixToNetmask } from "#fune/net.cidr@^1";
impl/typescript/prefix_to_netmask.ts · 9 lines · open · raw
import { formatIpv4 } from "./net_ipv4_format_ipv4.ts";

/** The dotted-quad mask for a prefix length: 24 is 255.255.255.0. */
export function prefixToNetmask(prefix: number): string {
  if (typeof prefix !== "number" || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) {
    throw new Error(`prefix must be an integer from 0 to 32, received ${prefix}`);
  }
  return formatIpv4(4294967296 - 2 ** (32 - prefix));
}

netmaskToPrefix throws on bad input 12 tests

export function netmaskToPrefix(netmask: string): number
netmaskstringa dotted-quad mask whose one bits are contiguous from the left
returnsint

For example

  • netmaskToPrefix(255.255.255.0) → 24 255.255.255.0 is /24
  • netmaskToPrefix(0.0.0.0) → 0 all zeros is /0
  • netmaskToPrefix(255.255.255.255) → 32 all ones is /32
import { netmaskToPrefix } from "#fune/net.cidr@^1";
impl/typescript/netmask_to_prefix.ts · 11 lines · open · raw
import { parseIpv4 } from "./net_ipv4_parse_ipv4.ts";

/** The prefix length of a dotted-quad mask; a mask with a gap in its one bits is refused. */
export function netmaskToPrefix(netmask: string): number {
  const value = parseIpv4(netmask);
  const hostPart = 4294967295 - value + 1;
  for (let prefix = 0; prefix <= 32; prefix++) {
    if (2 ** (32 - prefix) === hostPart) return prefix;
  }
  throw new Error(`"${netmask}" is not a contiguous netmask`);
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, 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.cidr

That builds the whole group. To build only what you call, and whatever it uses inside the group:

fune add net.cidr --only cidrInfo
Download for TypeScript net.cidr-1.0.0-typescript.fune · 24,468 bytes sha256 438567b41289b422f708068cd5889fab539acaf295b199414e20e35e1fbc74c0

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.cidr-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.cidr@1.0.0:typescript.

The whole function, every language, is one file too: net.cidr-1.0.0.fune, 36,244 bytes, sha256 bbc5cdb055ae733750b3c2cfb9061a67690545d83a9deae6e546ea7de67c4510. 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.cidr.cidrInfo
// fune: before net.cidr.cidrContains
// fune: before net.cidr.isCidr
// fune: before net.cidr.prefixToNetmask
// fune: before net.cidr.netmaskToPrefix

after — your function gets the result and the arguments, and returns the final result.

// fune: after net.cidr.cidrInfo
// fune: after net.cidr.cidrContains
// fune: after net.cidr.isCidr
// fune: after net.cidr.prefixToNetmask
// fune: after net.cidr.netmaskToPrefix

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.cidr
// fune: replace net.ipv6 in net.cidr

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.cidr --steps.

// fune: step net.cidr.<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.

cidrInfo 15 tests

CaseArgumentsExpected
a /24 192.168.1.0/24 → cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun…
an interface address: host bits are dropped 192.168.1.77/24 → cidr 192.168.1.0/24, network 192.168.1.0, prefix 24, netmask 255.255.255.0, wildcard 0.0.0.255, broadcast 192.168.1.255, first host 192.168.1.1, last host 192.168.1.254, host coun…
a /8 10.0.0.0/8 → cidr 10.0.0.0/8, network 10.0.0.0, prefix 8, netmask 255.0.0.0, wildcard 0.255.255.255, broadcast 10.255.255.255, first host 10.0.0.1, last host 10.255.255.254, host count 16,777,…
a /12 that does not fall on an octet: 172.16-172.31 172.16.5.4/12 → cidr 172.16.0.0/12, network 172.16.0.0, prefix 12, netmask 255.240.0.0, wildcard 0.15.255.255, broadcast 172.31.255.255, first host 172.16.0.1, last host 172.31.255.254, host coun…
a /26 in the middle of a /24 203.0.113.64/26 → cidr 203.0.113.64/26, network 203.0.113.64, prefix 26, netmask 255.255.255.192, wildcard 0.0.0.63, broadcast 203.0.113.127, first host 203.0.113.65, last host 203.0.113.126, host …
a /30 has two hosts 192.168.1.0/30 → cidr 192.168.1.0/30, network 192.168.1.0, prefix 30, netmask 255.255.255.252, wildcard 0.0.0.3, broadcast 192.168.1.3, first host 192.168.1.1, last host 192.168.1.2, host count 2,…
a /31 point-to-point link: both addresses usable, no broadcast (RFC 3021) 198.51.100.7/31 → cidr 198.51.100.6/31, network 198.51.100.6, prefix 31, netmask 255.255.255.254, wildcard 0.0.0.1, broadcast —, first host 198.51.100.6, last host 198.51.100.7, host count 2, addre…
a /32 is one host 192.0.2.9/32 → cidr 192.0.2.9/32, network 192.0.2.9, prefix 32, netmask 255.255.255.255, wildcard 0.0.0.0, broadcast —, first host 192.0.2.9, last host 192.0.2.9, host count 1, address count 1
the whole IPv4 space 0.0.0.0/0 → cidr 0.0.0.0/0, network 0.0.0.0, prefix 0, netmask 0.0.0.0, wildcard 255.255.255.255, broadcast 255.255.255.255, first host 0.0.0.1, last host 255.255.255.254, host count 4,294,96…
prefix above 32 192.168.1.0/33 → error: is not a CIDR block
Show the other 5 tests
CaseArgumentsExpected
no prefix 192.168.1.0 → error: is not a CIDR block
a leading zero in the prefix 192.168.1.0/024 → error: is not a CIDR block
a trailing newline 192.168.1.0/24 → error: is not a CIDR block
a non-ASCII digit in the prefix 10.0.0.0/٨ → error: is not a CIDR block
IPv6 blocks are not described 2001:db8::/32 → error: cidrInfo handles IPv4 blocks only

cidrContains 18 tests

CaseArgumentsExpected
inside a /24 192.168.1.0/24, 192.168.1.200 → true
the next /24 is outside 192.168.1.0/24, 192.168.2.1 → false
the last address of a /8 10.0.0.0/8, 10.255.255.255 → true
172.31 is inside 172.16.0.0/12 172.16.0.0/12, 172.31.0.1 → true
172.32 is not (a naive /16 reading gets 172.16-only) 172.16.0.0/12, 172.32.0.1 → false
/0 contains everything of its family 0.0.0.0/0, 203.0.113.5 → true
/32 contains only itself 192.0.2.9/32, 192.0.2.10 → false
IPv6 inside a /32 2001:db8::/32, 2001:db8:ffff::1 → true
IPv6 just outside a /32 2001:db8::/32, 2001:db9::1 → false
link-local /10 reaches febf fe80::/10, febf::1 → true
Show the other 8 tests
CaseArgumentsExpected
link-local /10 stops before fec0 fe80::/10, fec0::1 → false
a /33 splits a group: 7fff is in the lower half 2001:db8::/33, 2001:db8:7fff::1 → true
a /33 splits a group: 8000 is in the upper half 2001:db8::/33, 2001:db8:8000::1 → false
an IPv4-mapped IPv6 address is not in an IPv4 block 10.0.0.0/8, ::ffff:10.0.0.1 → false
an IPv4 address is not in ::/0 ::/0, 10.0.0.1 → false
a malformed address 10.0.0.0/8, 10.0.0.256 → error: is not an IP address
a block without a prefix 10.0.0.0, 10.0.0.1 → error: is not a CIDR block
an IPv6 prefix above 128 2001:db8::/129, 2001:db8::1 → error: is not a CIDR block

isCidr 13 tests

CaseArgumentsExpected
an IPv4 block 10.0.0.0/8 → true
an IPv6 block 2001:db8::/32 → true
/0 0.0.0.0/0 → true
IPv6 /128 ::/128 → true
host bits set are still a block 192.168.1.77/24 → true
IPv4 prefix 33 10.0.0.0/33 → false
IPv6 prefix 129 ::/129 → false
a bare address 10.0.0.0 → false
two slashes 10.0.0.0/8/8 → false
a leading zero in the prefix 10.0.0.0/08 → false
Show the other 3 tests
CaseArgumentsExpected
a trailing newline 10.0.0.0/8 → false
a zone id fe80::1%eth0/64 → false
empty text → false

prefixToNetmask 12 tests

CaseArgumentsExpected
/24 24 → 255.255.255.0
/0 0 → 0.0.0.0
/8 8 → 255.0.0.0
/12 12 → 255.240.0.0
/20 20 → 255.255.240.0
/25 25 → 255.255.255.128
/30 30 → 255.255.255.252
/31 31 → 255.255.255.254
/32 32 → 255.255.255.255
33 is too long 33 → error: prefix must be an integer from 0 to 32
Show the other 2 tests
CaseArgumentsExpected
negative -1 → error: prefix must be an integer from 0 to 32
a fraction 24.5 → error: prefix must be an integer from 0 to 32

netmaskToPrefix 12 tests

CaseArgumentsExpected
255.255.255.0 is /24 255.255.255.0 → 24
all zeros is /0 0.0.0.0 → 0
all ones is /32 255.255.255.255 → 32
255.255.254.0 is /23 255.255.254.0 → 23
255.240.0.0 is /12 255.240.0.0 → 12
255.255.255.128 is /25 255.255.255.128 → 25
128.0.0.0 is /1 128.0.0.0 → 1
255.255.255.252 is /30 255.255.255.252 → 30
a gap in the one bits 255.0.255.0 → error: is not a contiguous netmask
a wildcard mask is not a netmask 0.255.255.255 → error: is not a contiguous netmask
Show the other 2 tests
CaseArgumentsExpected
a stray low bit 255.255.255.1 → error: is not a contiguous netmask
not a dotted quad 255.255.256.0 → error: is not a dotted-quad IPv4 address

More from the author

- **Host bits are allowed and dropped.** `192.168.1.77/24` is how an interface address is written, and it describes `192.168.1.0/24`; `cidrInfo` returns the cleaned-up block in `cidr`. (Python's `ipaddress.ip_network` refuses it unless `strict=False`; this behaves like `strict=False`.) - **/31 and /32.** A /31 is a point-to-point link: RFC 3021 makes both addresses usable, so `hostCount` is 2, the hosts are the two addresses and `broadcast` is null. A /32 is one host, also with no broadcast. Everything from /0 to /30 has network and broadcast addresses reserved, so `hostCount` is `addressCount - 2`. - **Families do not mix.** `cidrContains("10.0.0.0/8", "::ffff:10.0.0.1")` is false: an IPv4-mapped IPv6 address is a different address as far as a socket is concerned. Convert it first if you mean the IPv4 address. - **Strict text.** Addresses follow `net.ipv4` and `net.ipv6` (no leading zeros, no zone ids); the prefix is ASCII decimal with no leading zero, 0-32 for IPv4 and 0-128 for IPv6. - `cidrInfo` is IPv4 only: an IPv6 /64 has 2^64 addresses, which no language here holds in an `int`, and "broadcast" has no IPv6 meaning. - Netmasks must be contiguous: `255.0.255.0` is refused, and so is a wildcard mask such as `0.0.0.255` (write the netmask, `255.255.255.0`).

`cidrBlock` (`cidr_block`) is exported for the group's own functions; it is not part of the contract.

Sources: RFC 4632 (CIDR) section 3.1; RFC 3021 (Using 31-Bit Prefixes on IPv4 Point-to-Point Links); RFC 4291 section 2.3 (IPv6 prefix notation).

Files

PathBytes
README.md1,839
impl/python/cidr_contains.py1,080
impl/python/cidr_info.py1,358
impl/python/is_cidr.py1,224
impl/python/netmask_to_prefix.py427
impl/python/prefix_to_netmask.py405
impl/rust/cidr_contains.rs1,472
impl/rust/cidr_info.rs2,236
impl/rust/is_cidr.rs1,435
impl/rust/netmask_to_prefix.rs668
impl/rust/prefix_to_netmask.rs683
impl/typescript/cidr_contains.ts1,202
impl/typescript/cidr_info.ts1,453
impl/typescript/is_cidr.ts1,392
impl/typescript/netmask_to_prefix.ts461
impl/typescript/prefix_to_netmask.ts422
vectors.json9,910