Skip to content

Subnet / CIDR Calculator — Rust source

Compute IPv4 subnet details from a CIDR block - network, broadcast, host range, netmask, wildcard, host count, IP class, and private-range detection. Split a block into smaller subnets, all in your browser.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! IPv4 subnet / CIDR math: dotted-quad ⇄ u32 conversion, full subnet details
//! (network, broadcast, hosts, class, privacy), and CIDR splitting into smaller
//! blocks. The logic is pure and deterministic; validation failures are
//! reported via `Option::None` (or an empty `Vec` for the splitter) so callers
//! can render a graceful error instead of catching a panic.
//!
//! Language: Rust
//! CosmoDev polyglot showcase port of `subnet-cidr`.
//! Ported from src/lib/subnet-cidr.ts — functionally equivalent.
//!
//! Display source — part of CosmoDev's polyglot tool pages.

// IPv4 addresses are carried as u32. Rust's fixed-width unsigned type truncates
// every bitwise result to 32 bits for free. The one sharp edge is that shifting
// a u32 by its full width (>= 32) panics in debug builds, so the prefix == 0
// netmask case is handled explicitly to avoid `0xffffffff << 32`.

/// Classful IPv4 category returned by [`subnet_details`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IpClass {
    A,
    B,
    C,
    /// Class D (multicast) or E (reserved) — outside the A/B/C classful ranges.
    Other,
}

/// Computed properties of a CIDR block.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubnetInfo {
    pub network: String,
    pub broadcast: String,
    pub first_host: String,
    pub last_host: String,
    pub netmask: String,
    pub wildcard: String,
    pub host_count: u64,
    pub prefix: u32,
    pub ip_class: IpClass,
    pub is_private: bool,
}

/// One block produced by [`split_subnet`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subnet {
    pub network: String,
    pub prefix: u32,
}

/// Parse a single decimal octet (0–255): non-empty, ASCII digits only — no sign,
/// whitespace, or hex. Mirrors the TypeScript `/^\d+$/` + `<= 255` checks.
fn parse_octet(s: &str) -> Option<u32> {
    // A valid octet is 1–3 digits; any longer all-digit string is >= 1000 > 255.
    match s.len() {
        1..=3 => {}
        _ => return None,
    }
    let mut n: u32 = 0;
    for b in s.bytes() {
        if !b.is_ascii_digit() {
            return None;
        }
        n = n * 10 + (b - b'0') as u32;
    }
    if n > 255 {
        None
    } else {
        Some(n)
    }
}

/// Parse a decimal prefix length in [0, 32].
fn parse_prefix(s: &str) -> Option<u32> {
    // A valid prefix is 1–2 digits ("0".."32"); 3+ digits is >= 100 > 32.
    match s.len() {
        1..=2 => {}
        _ => return None,
    }
    let mut n: u32 = 0;
    for b in s.bytes() {
        if !b.is_ascii_digit() {
            return None;
        }
        n = n * 10 + (b - b'0') as u32;
    }
    if n > 32 {
        None
    } else {
        Some(n)
    }
}

/// Convert a dotted-quad IPv4 string to a `u32`. Returns `None` if invalid.
///
/// The loop counts octets as it goes: more than four short-circuits to `None`
/// (covers a trailing dot → five parts), and fewer than four after the loop is
/// also rejected (covers a short address or an empty string).
pub fn ip_to_int(ip: &str) -> Option<u32> {
    let mut acc: u32 = 0;
    let mut count = 0;
    for part in ip.split('.') {
        if count == 4 {
            return None; // too many octets
        }
        let octet = parse_octet(part)?;
        acc = acc * 256 + octet;
        count += 1;
    }
    if count != 4 {
        return None; // too few octets (empty string lands here too)
    }
    Some(acc)
}

/// Convert a `u32` to a dotted-quad IPv4 string.
pub fn int_to_ip(n: u32) -> String {
    format!(
        "{}.{}.{}.{}",
        (n >> 24) & 0xff,
        (n >> 16) & 0xff,
        (n >> 8) & 0xff,
        n & 0xff
    )
}

/// Return the 32-bit netmask for a prefix length in [0, 32].
/// The prefix == 0 case is special-cased because `0xffffffff_u32 << 32` is
/// undefined (panics in debug builds).
fn mask_from_prefix(prefix: u32) -> u32 {
    if prefix == 0 {
        0
    } else {
        0xffffffff_u32 << (32 - prefix)
    }
}

/// Parse `"ip/prefix"` into a normalized `(String, u32)` pair with the prefix
/// in [0, 32]. Returns `None` if the input is not a valid CIDR.
pub fn parse_cidr(cidr: &str) -> Option<(String, u32)> {
    // splitn(2, '/') splits at the FIRST slash only, so any second slash stays
    // inside the prefix half — which we then reject.
    let mut parts = cidr.splitn(2, '/');
    let ip_part = parts.next()?;
    let prefix_part = parts.next()?;
    if prefix_part.contains('/') {
        return None; // multiple slashes ("a.b.c.d/24/16")
    }
    let prefix = parse_prefix(prefix_part)?;
    let ip_int = ip_to_int(ip_part)?;
    Some((int_to_ip(ip_int), prefix))
}

/// Compute the full properties of a CIDR block. Returns `None` if invalid.
pub fn subnet_details(cidr: &str) -> Option<SubnetInfo> {
    let (ip, prefix) = parse_cidr(cidr)?;
    let mask = mask_from_prefix(prefix);
    let wildcard = !mask;
    let ip_int = ip_to_int(&ip)?; // safe: parse_cidr validated and normalized the IP
    let network = ip_int & mask;
    let broadcast = network | wildcard;

    // /31 (point-to-point) and /32 (single host) have no host/broadcast split.
    let (first_host, last_host) = if prefix >= 31 {
        (network, broadcast)
    } else {
        (network + 1, broadcast - 1)
    };

    // Host count: 1 for /32, 2 for /31, otherwise 2^(32-prefix) − 2. Computed in
    // u64 so the /0 case (2^32 − 2 = 4_294_967_294) cannot overflow.
    let host_count = if prefix >= 32 {
        1
    } else if prefix == 31 {
        2
    } else {
        (1u64 << (32 - prefix)) - 2
    };

    let first_octet = (network >> 24) & 0xff;
    let ip_class = match first_octet {
        0..=127 => IpClass::A,
        128..=191 => IpClass::B,
        192..=223 => IpClass::C,
        _ => IpClass::Other,
    };

    let second_octet = (network >> 16) & 0xff;
    let is_private = first_octet == 10
        || (first_octet == 172 && (16..=31).contains(&second_octet))
        || (first_octet == 192 && second_octet == 168);

    Some(SubnetInfo {
        network: int_to_ip(network),
        broadcast: int_to_ip(broadcast),
        first_host: int_to_ip(first_host),
        last_host: int_to_ip(last_host),
        netmask: int_to_ip(mask),
        wildcard: int_to_ip(wildcard),
        host_count,
        prefix,
        ip_class,
        is_private,
    })
}

/// Split a CIDR block into smaller subnets of `new_prefix`.
/// Returns an empty `Vec` if the CIDR is invalid or `new_prefix` is outside
/// (prefix, 32]. The base IP is normalized to the network address before
/// splitting.
pub fn split_subnet(cidr: &str, new_prefix: u32) -> Vec<Subnet> {
    let (ip, prefix) = match parse_cidr(cidr) {
        Some(v) => v,
        None => return Vec::new(),
    };
    // new_prefix is a u32, so it is always integral; only the range is checked.
    if new_prefix < prefix || new_prefix > 32 {
        return Vec::new();
    }

    let mask = mask_from_prefix(prefix);
    let ip_int = ip_to_int(&ip).expect("parse_cidr validated the IP");
    let network = ip_int & mask;
    let count: u64 = 1u64 << (new_prefix - prefix); // 2^(new_prefix − prefix)
    let block_size: u64 = 1u64 << (32 - new_prefix); // 2^(32 − new_prefix)

    // For realistic splits count is small. (An aggressive /0 → /32 split would
    // be billions of blocks — caller's problem, same as the TS original.)
    let mut out = Vec::with_capacity(count as usize);
    for i in 0..count {
        // i * block_size stays under 2^32 for any valid split; the mask + cast
        // document that we operate in 32-bit address space.
        let offset = ((i * block_size) & 0xffff_ffff) as u32;
        out.push(Subnet {
            network: int_to_ip(network.wrapping_add(offset)),
            prefix: new_prefix,
        });
    }
    out
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →