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.
- parse_ipv4 (text: string) -> int
- format_ipv4 (value: int) -> string
- is_ipv4 (text: string) -> bool
Once installed, your code imports each one from the group's module.
parse_ipv4 throws on bad input 17 tests
def parse_ipv4(text: str) -> int
| 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
parse_ipv4(192.168.1.1)→ 3,232,235,777 a private address: 192*2^24 + 168*2^16 + 1*2^8 + 1parse_ipv4(127.0.0.1)→ 2,130,706,433 loopback is 127*2^24 + 1parse_ipv4(1.2.3.4)→ 16,909,060 each octet in its own byte
from fune.net.ipv4 import parse_ipv4 # net.ipv4@^1
from typing import Optional
def parse_ipv4(text: str) -> int:
"""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.
"""
value = ipv4_value(text)
if value is None:
raise ValueError('"%s" is not a dotted-quad IPv4 address' % (text,))
return value
# Exported for is_ipv4 and for capabilities that parse IPv4 text without
# wanting an error; not part of the group's contract.
def ipv4_value(text: object) -> Optional[int]:
"""The address as a number, or None when the text is not a strict dotted quad."""
if not isinstance(text, str):
return None
parts = text.split(".")
if len(parts) != 4:
return None
value = 0
for part in parts:
if len(part) < 1 or len(part) > 3:
return None
# "0" <= ch <= "9" rather than isdigit(), which accepts other scripts' digits.
for ch in part:
if not ("0" <= ch <= "9"):
return None
# A leading zero is where octal readers and decimal readers part company.
if len(part) > 1 and part[0] == "0":
return None
octet = int(part)
if octet > 255:
return None
value = value * 256 + octet
return valueformat_ipv4 throws on bad input 11 tests
def format_ipv4(value: int) -> str
| value | int | 0 to 4294967295 |
| returns | string | dotted-quad text, the inverse of parseIpv4 |
For example
format_ipv4(3,232,235,777)→ 192.168.1.1 3232235777 is 192.168.1.1format_ipv4(2,130,706,433)→ 127.0.0.1 2130706433 is loopbackformat_ipv4(0)→ 0.0.0.0 zero
from fune.net.ipv4 import format_ipv4 # net.ipv4@^1
def format_ipv4(value: int) -> str:
"""Dotted-quad text for an unsigned 32-bit address: the inverse of parse_ipv4."""
if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > 4294967295:
raise ValueError("IPv4 value must be an integer from 0 to 4294967295, received %r" % (value,))
return "%d.%d.%d.%d" % ((value >> 24) & 255, (value >> 16) & 255, (value >> 8) & 255, value & 255)is_ipv4 11 tests
def is_ipv4(text: str) -> bool
| text | string | any text; true exactly when parseIpv4 would accept it |
| returns | bool |
For example
is_ipv4(192.168.0.1)→ true an ordinary addressis_ipv4(0.0.0.0)→ true all zerosis_ipv4(255.255.255.255)→ true all ones
from fune.net.ipv4 import is_ipv4 # 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.
from .net_ipv4_parse_ipv4 import ipv4_value ← parseIpv4, another function of this group · built into the same file, even by a slim install
def is_ipv4(text: str) -> bool:
"""True exactly when parse_ipv4 would accept the text; never raises."""
return ipv4_value(text) is not NoneInstall
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python 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 Python implementation. Install it without the registry with fune add ./net.ipv4-1.0.0-python.fune, or fetch it from a terminal with fune pull net.ipv4@1.0.0:python.
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.