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 →