net.ipv4
Parse, format and check dotted-quad IPv4 addresses, refusing leading zeros, octal, hex and short forms.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 39 tests, run in TypeScript, Python and Rust.parseIpv4 17 · formatIpv4 11 · isIpv4 11
What it does
Parse dotted-quad IPv4 text to an unsigned 32-bit number, format a number back to text, and check text without raising.
## Why so strict
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.
- parseIpv4 (text: string) -> int
- formatIpv4 (value: int) -> string
- isIpv4 (text: string) -> bool
Once installed, your code imports each one from the group's module.
parseIpv4 throws on bad input 17 tests
export function parseIpv4(text: string): number
| text | string | four decimal octets 0-255 separated by dots; no leading zeros, signs or whitespace |
| returns | int | the address as an unsigned 32-bit number, 0 to 4294967295 |
For example
parseIpv4(192.168.1.1)→ 3,232,235,777 a private address: 192*2^24 + 168*2^16 + 1*2^8 + 1parseIpv4(127.0.0.1)→ 2,130,706,433 loopback is 127*2^24 + 1parseIpv4(1.2.3.4)→ 16,909,060 each octet in its own byte
import { parseIpv4 } from "#fune/net.ipv4@^1";
/**
* The address as an unsigned 32-bit number.
*
* Only the strict dotted quad is accepted. The C library's inet_aton also
* reads "010.1.1.1" as octal 8.1.1.1, "0x7f.1" as hex and "127.1" as
* 127.0.0.1; a validator that agrees with one reader and a socket library that
* agrees with the other is how an allow-list gets bypassed, so all of those
* are refused.
*/
export function parseIpv4(text: string): number {
const value = ipv4Value(text);
if (value === null) throw new Error(`"${String(text)}" is not a dotted-quad IPv4 address`);
return value;
}
// Exported for isIpv4 and for capabilities that parse IPv4 text without
// wanting an error; not part of the group's contract.
/** The address as a number, or null when the text is not a strict dotted quad. */
export function ipv4Value(text: unknown): number | null {
if (typeof text !== "string") return null;
const parts = text.split(".");
if (parts.length !== 4) return null;
let value = 0;
for (const part of parts) {
if (part.length < 1 || part.length > 3) return null;
for (const ch of part) if (ch < "0" || ch > "9") return null;
// A leading zero is where octal readers and decimal readers part company.
if (part.length > 1 && part[0] === "0") return null;
const octet = Number(part);
if (octet > 255) return null;
value = value * 256 + octet;
}
return value;
}formatIpv4 throws on bad input 11 tests
export function formatIpv4(value: number): string
| value | int | 0 to 4294967295 |
| returns | string | dotted-quad text, the inverse of parseIpv4 |
For example
formatIpv4(3,232,235,777)→ 192.168.1.1 3232235777 is 192.168.1.1formatIpv4(2,130,706,433)→ 127.0.0.1 2130706433 is loopbackformatIpv4(0)→ 0.0.0.0 zero
import { formatIpv4 } from "#fune/net.ipv4@^1";
/** Dotted-quad text for an unsigned 32-bit address: the inverse of parseIpv4. */
export function formatIpv4(value: number): string {
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 4294967295) {
throw new Error(`IPv4 value must be an integer from 0 to 4294967295, received ${value}`);
}
// Arithmetic rather than >>> and &, which work on signed 32-bit numbers.
return [
Math.floor(value / 16777216) % 256,
Math.floor(value / 65536) % 256,
Math.floor(value / 256) % 256,
value % 256,
].join(".");
}isIpv4 11 tests
export function isIpv4(text: string): boolean
| text | string | any text; true exactly when parseIpv4 would accept it |
| returns | bool |
For example
isIpv4(192.168.0.1)→ true an ordinary addressisIpv4(0.0.0.0)→ true all zerosisIpv4(255.255.255.255)→ true all ones
import { isIpv4 } from "#fune/net.ipv4@^1";
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"; ← parseIpv4, another function of this group · built into the same file, even by a slim install
/** True exactly when parseIpv4 would accept the text; never throws. */
export function isIpv4(text: string): boolean {
return ipv4Value(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 nothing else, 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.ipv4
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add net.ipv4 --only parseIpv4
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.ipv4-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.ipv4@1.0.0:typescript.
The whole function, every language, is one file too: net.ipv4-1.0.0.fune, 17,239 bytes, sha256 239a64ddd143396d95f8918b6a17f5f3532ede133b4c5a6482d44307f26f5f5d. 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.ipv4.parseIpv4
// fune: before net.ipv4.formatIpv4
// fune: before net.ipv4.isIpv4
after — your function gets the result and the arguments, and returns the final result.
// fune: after net.ipv4.parseIpv4
// fune: after net.ipv4.formatIpv4
// fune: after net.ipv4.isIpv4
replace — it requires no other capability, so there is no dependency to replace.
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.ipv4 --steps.
// fune: step net.ipv4.<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.
parseIpv4 17 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a private address: 192*2^24 + 168*2^16 + 1*2^8 + 1 | 192.168.1.1 | → | 3,232,235,777 |
| loopback is 127*2^24 + 1 | 127.0.0.1 | → | 2,130,706,433 |
| each octet in its own byte | 1.2.3.4 | → | 16,909,060 |
| all zeros is 0 (a lone 0 octet is not a leading zero) | 0.0.0.0 | → | 0 |
| all ones is 2^32 - 1 | 255.255.255.255 | → | 4,294,967,295 |
| 10.0.0.1 | 10.0.0.1 | → | 167,772,161 |
| a leading zero is refused, not read as octal (inet_aton would say 1.2.3.4 or 8.x) | 010.1.1.1 | → | error: is not a dotted-quad IPv4 address |
| an octet above 255 | 256.1.1.1 | → | error: is not a dotted-quad IPv4 address |
| three parts is not shorthand for 1.2.0.3 | 1.2.3 | → | error: is not a dotted-quad IPv4 address |
| five parts | 1.2.3.4.5 | → | error: is not a dotted-quad IPv4 address |
Show the other 7 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an empty octet | 1..3.4 | → | error: is not a dotted-quad IPv4 address |
| hex is refused | 0x7f.0.0.1 | → | error: is not a dotted-quad IPv4 address |
| a sign is refused | +1.2.3.4 | → | error: is not a dotted-quad IPv4 address |
| leading whitespace is refused | 1.2.3.4 | → | error: is not a dotted-quad IPv4 address |
| a trailing newline is refused | 1.2.3.4 | → | error: is not a dotted-quad IPv4 address |
| a non-ASCII digit is refused | ١.2.3.4 | → | error: is not a dotted-quad IPv4 address |
| empty text | → | error: is not a dotted-quad IPv4 address |
formatIpv4 11 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 3232235777 is 192.168.1.1 | 3,232,235,777 | → | 192.168.1.1 |
| 2130706433 is loopback | 2,130,706,433 | → | 127.0.0.1 |
| zero | 0 | → | 0.0.0.0 |
| the largest address | 4,294,967,295 | → | 255.255.255.255 |
| 256 carries into the third octet | 256 | → | 0.0.1.0 |
| 2^24 is 1.0.0.0 | 16,777,216 | → | 1.0.0.0 |
| above 2^31 does not go negative (signed 32-bit shifts would) | 3,221,225,985 | → | 192.0.2.1 |
| 16909060 is 1.2.3.4 | 16,909,060 | → | 1.2.3.4 |
| negative | -1 | → | error: IPv4 value must be an integer from 0 to 4294967295 |
| 2^32 is out of range | 4,294,967,296 | → | error: IPv4 value must be an integer from 0 to 4294967295 |
Show the other 1 test
| Case | Arguments | Expected | |
|---|---|---|---|
| a fraction | 1.5 | → | error: IPv4 value must be an integer from 0 to 4294967295 |
isIpv4 11 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an ordinary address | 192.168.0.1 | → | true |
| all zeros | 0.0.0.0 | → | true |
| all ones | 255.255.255.255 | → | true |
| a double zero octet is a leading zero | 00.0.0.0 | → | false |
| a short form | 127.1 | → | false |
| out of range | 999.999.999.999 | → | false |
| a trailing newline | 1.2.3.4 | → | false |
| non-ASCII digits | ١٢.1.1.1 | → | false |
| an IPv6 address | ::1 | → | false |
| a CIDR block is not an address | 10.0.0.0/8 | → | false |
Show the other 1 test
| Case | Arguments | Expected | |
|---|---|---|---|
| empty text | → | false |
More from the author
There is more than one way to read "an IPv4 address". The C library's `inet_aton` (and so `ping`, `curl` and many socket libraries) accepts `127.1` (meaning 127.0.0.1), `0x7f.0.0.1` (hex) and `010.1.1.1` (octal, so 8.1.1.1). Python's `ipaddress` and most validators refuse them. When the validator and the thing that connects disagree, an allow-list can be bypassed (CVE-2021-29921 was exactly this). This capability accepts only the form RFC 791 writes and everyone reads the same way: exactly four decimal octets 0-255, no leading zeros (a lone `0` is fine), no signs, no whitespace, ASCII digits only.
## Notes
- `parseIpv4` and `formatIpv4` are inverses over 0 to 4294967295. - `isIpv4` never raises; it is true exactly when `parseIpv4` would succeed. - The number is an ordinary integer, so it compares and subtracts correctly in every language (no signed 32-bit wrap above 128.0.0.0). - `ipv4Value` (`ipv4_value`) is exported for sibling capabilities that want "number or null"; it is not part of the contract.
Sources: RFC 791 (Internet Protocol), section 2.3 and 3.2; RFC 6943 section 3.1.1 on the ambiguity of non-dotted-decimal forms.