Functional Weave
Code in Rust

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.

  1. parse_ipv4 (text: string) -> int
  2. format_ipv4 (value: int) -> string
  3. 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

pub fn parse_ipv4(text: &str) -> i64
textstringfour decimal octets 0-255 separated by dots; no leading zeros, signs or whitespace
returnsintthe 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 + 1
  • parse_ipv4(127.0.0.1) → 2,130,706,433 loopback is 127*2^24 + 1
  • parse_ipv4(1.2.3.4) → 16,909,060 each octet in its own byte
fune!(net.ipv4@^1);  // then call parse_ipv4(…)
impl/rust/parse_ipv4.rs · 50 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// 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.
///
/// # Panics
/// Panics when the text is not a strict dotted quad.
pub fn parse_ipv4(text: &str) -> i64 {
    match ipv4_value(text) {
        Some(value) => value,
        None => panic!("\"{}\" is not a dotted-quad IPv4 address", text),
    }
}

// Exported for is_ipv4 and for capabilities that parse IPv4 text without
// wanting a panic; not part of the group's contract.

/// The address as a number, or None when the text is not a strict dotted quad.
pub fn ipv4_value(text: &str) -> Option<i64> {
    let parts: Vec<&str> = text.split('.').collect();
    if parts.len() != 4 {
        return None;
    }
    let mut value: i64 = 0;
    for part in parts {
        let bytes = part.as_bytes();
        if bytes.is_empty() || bytes.len() > 3 || !bytes.iter().all(|b| b.is_ascii_digit()) {
            return None;
        }
        // A leading zero is where octal readers and decimal readers part company.
        if bytes.len() > 1 && bytes[0] == b'0' {
            return None;
        }
        let octet = bytes.iter().fold(0i64, |acc, b| acc * 10 + (b - b'0') as i64);
        if octet > 255 {
            return None;
        }
        value = value * 256 + octet;
    }
    Some(value)
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::Int(parse_ipv4(args[0].as_str()))
}

format_ipv4 throws on bad input 11 tests

pub fn format_ipv4(value: i64) -> String
valueint0 to 4294967295
returnsstringdotted-quad text, the inverse of parseIpv4

For example

  • format_ipv4(3,232,235,777) → 192.168.1.1 3232235777 is 192.168.1.1
  • format_ipv4(2,130,706,433) → 127.0.0.1 2130706433 is loopback
  • format_ipv4(0) → 0.0.0.0 zero
fune!(net.ipv4@^1);  // then call format_ipv4(…)
impl/rust/format_ipv4.rs · 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// Dotted-quad text for an unsigned 32-bit address: the inverse of parse_ipv4.
///
/// # Panics
/// Panics when the value is outside 0 to 4294967295.
pub fn format_ipv4(value: i64) -> String {
    if !(0..=4294967295).contains(&value) {
        panic!("IPv4 value must be an integer from 0 to 4294967295, received {}", value);
    }
    format!(
        "{}.{}.{}.{}",
        (value >> 24) & 255,
        (value >> 16) & 255,
        (value >> 8) & 255,
        value & 255
    )
}

pub fn fune_vector(args: &[Value]) -> Value {
    // A fractional number cannot become an i64 without changing it, so refuse
    // it with the wording TypeScript and Python use.
    let value = match &args[0] {
        Value::Int(i) => *i,
        other => panic!("IPv4 value must be an integer from 0 to 4294967295, received {}", other),
    };
    Value::str(&format_ipv4(value))
}

is_ipv4 11 tests

pub fn is_ipv4(text: &str) -> bool
textstringany text; true exactly when parseIpv4 would accept it
returnsbool

For example

  • is_ipv4(192.168.0.1) → true an ordinary address
  • is_ipv4(0.0.0.0) → true all zeros
  • is_ipv4(255.255.255.255) → true all ones
fune!(net.ipv4@^1);  // then call is_ipv4(…)
impl/rust/is_ipv4.rs · 11 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::net_ipv4_parse_ipv4::ipv4_value;  ← parseIpv4, another function of this group · built into the same file, even by a slim install

/// True exactly when parse_ipv4 would accept the text; never panics.
pub fn is_ipv4(text: &str) -> bool {
    ipv4_value(text).is_some()
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::Bool(is_ipv4(args[0].as_str()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. 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
Download for Rust net.ipv4-1.0.0-rust.fune · 12,531 bytes sha256 9c97b12bb41a6e0f469a3b51caee5728244160cdd35dd8206072ed109a919f1a

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

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

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

CaseArgumentsExpected
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
CaseArgumentsExpected
a fraction 1.5 → error: IPv4 value must be an integer from 0 to 4294967295

isIpv4 11 tests

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

Files

PathBytes
README.md1,303
impl/python/format_ipv4.py425
impl/python/is_ipv4.py194
impl/python/parse_ipv4.py1,540
impl/rust/format_ipv4.rs899
impl/rust/is_ipv4.rs305
impl/rust/parse_ipv4.rs1,701
impl/typescript/format_ipv4.ts563
impl/typescript/is_ipv4.ts212
impl/typescript/parse_ipv4.ts1,384
vectors.json4,414