Finish verion 0.1.0
This commit is contained in:
9
crates/wtfnet-calc/Cargo.toml
Normal file
9
crates/wtfnet-calc/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "wtfnet-calc"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ipnet = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
202
crates/wtfnet-calc/src/lib.rs
Normal file
202
crates/wtfnet-calc/src/lib.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CalcError {
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SubnetInfo {
|
||||
pub input: String,
|
||||
pub version: String,
|
||||
pub cidr: String,
|
||||
pub network: String,
|
||||
pub broadcast: Option<String>,
|
||||
pub netmask: String,
|
||||
pub hostmask: String,
|
||||
pub prefix_len: u8,
|
||||
pub total_addresses: String,
|
||||
pub usable_addresses: String,
|
||||
pub first_host: Option<String>,
|
||||
pub last_host: Option<String>,
|
||||
}
|
||||
|
||||
pub fn subnet_info(input: &str) -> Result<SubnetInfo, CalcError> {
|
||||
let net = parse_net(input)?;
|
||||
match net {
|
||||
IpNet::V4(v4) => Ok(subnet_info_v4(input, v4)),
|
||||
IpNet::V6(v6) => Ok(subnet_info_v6(input, v6)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(a: &str, b: &str) -> Result<bool, CalcError> {
|
||||
let net_a = parse_net(a)?;
|
||||
let net_b = parse_net(b)?;
|
||||
Ok(net_a.contains(&net_b))
|
||||
}
|
||||
|
||||
pub fn overlap(a: &str, b: &str) -> Result<bool, CalcError> {
|
||||
let net_a = parse_net(a)?;
|
||||
let net_b = parse_net(b)?;
|
||||
match (net_a, net_b) {
|
||||
(IpNet::V4(a), IpNet::V4(b)) => Ok(overlap_v4(a, b)),
|
||||
(IpNet::V6(a), IpNet::V6(b)) => Ok(overlap_v6(a, b)),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summarize(inputs: &[String]) -> Result<Vec<IpNet>, CalcError> {
|
||||
if inputs.is_empty() {
|
||||
return Err(CalcError::InvalidInput(
|
||||
"at least one CIDR required".to_string(),
|
||||
));
|
||||
}
|
||||
let mut nets = Vec::with_capacity(inputs.len());
|
||||
for value in inputs {
|
||||
nets.push(parse_net(value)?);
|
||||
}
|
||||
Ok(IpNet::aggregate(&nets))
|
||||
}
|
||||
|
||||
fn subnet_info_v4(input: &str, net: Ipv4Net) -> SubnetInfo {
|
||||
let total = total_addresses_v4(net.prefix_len());
|
||||
let usable = usable_addresses_v4(net.prefix_len());
|
||||
let (first, last) = first_last_v4(net);
|
||||
SubnetInfo {
|
||||
input: input.to_string(),
|
||||
version: "ipv4".to_string(),
|
||||
cidr: net.to_string(),
|
||||
network: net.network().to_string(),
|
||||
broadcast: Some(net.broadcast().to_string()),
|
||||
netmask: net.netmask().to_string(),
|
||||
hostmask: net.hostmask().to_string(),
|
||||
prefix_len: net.prefix_len(),
|
||||
total_addresses: total,
|
||||
usable_addresses: usable,
|
||||
first_host: first,
|
||||
last_host: last,
|
||||
}
|
||||
}
|
||||
|
||||
fn subnet_info_v6(input: &str, net: Ipv6Net) -> SubnetInfo {
|
||||
let total = total_addresses_v6(net.prefix_len());
|
||||
let (first, last) = first_last_v6(net);
|
||||
SubnetInfo {
|
||||
input: input.to_string(),
|
||||
version: "ipv6".to_string(),
|
||||
cidr: net.to_string(),
|
||||
network: net.network().to_string(),
|
||||
broadcast: None,
|
||||
netmask: net.netmask().to_string(),
|
||||
hostmask: net.hostmask().to_string(),
|
||||
prefix_len: net.prefix_len(),
|
||||
total_addresses: total.clone(),
|
||||
usable_addresses: total,
|
||||
first_host: first,
|
||||
last_host: last,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_net(value: &str) -> Result<IpNet, CalcError> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(CalcError::InvalidInput("empty input".to_string()));
|
||||
}
|
||||
|
||||
let mut parts = trimmed.split_whitespace();
|
||||
let first = parts.next().unwrap();
|
||||
if let Some(mask) = parts.next() {
|
||||
if parts.next().is_some() {
|
||||
return Err(CalcError::InvalidInput(
|
||||
"expected: <ip> <mask>".to_string(),
|
||||
));
|
||||
}
|
||||
return parse_ip_mask(first, mask);
|
||||
}
|
||||
|
||||
if let Some((ip, mask)) = trimmed.split_once('/') {
|
||||
if mask.contains('.') || mask.contains(':') {
|
||||
return parse_ip_mask(ip, mask);
|
||||
}
|
||||
}
|
||||
|
||||
trimmed
|
||||
.parse::<IpNet>()
|
||||
.map_err(|err| CalcError::Parse(err.to_string()))
|
||||
}
|
||||
|
||||
fn parse_ip_mask(ip: &str, mask: &str) -> Result<IpNet, CalcError> {
|
||||
let ip: IpAddr = ip
|
||||
.parse()
|
||||
.map_err(|_| CalcError::Parse(format!("invalid ip: {ip}")))?;
|
||||
let mask: IpAddr = mask
|
||||
.parse()
|
||||
.map_err(|_| CalcError::Parse(format!("invalid mask: {mask}")))?;
|
||||
IpNet::with_netmask(ip, mask).map_err(|err| CalcError::Parse(err.to_string()))
|
||||
}
|
||||
|
||||
fn total_addresses_v4(prefix: u8) -> String {
|
||||
let bits = 32u32.saturating_sub(prefix as u32);
|
||||
(1u128 << bits).to_string()
|
||||
}
|
||||
|
||||
fn usable_addresses_v4(prefix: u8) -> String {
|
||||
let total = 1u128 << (32u32.saturating_sub(prefix as u32));
|
||||
let usable = if prefix <= 30 {
|
||||
total.saturating_sub(2)
|
||||
} else {
|
||||
total
|
||||
};
|
||||
usable.to_string()
|
||||
}
|
||||
|
||||
fn total_addresses_v6(prefix: u8) -> String {
|
||||
let bits = 128u32.saturating_sub(prefix as u32);
|
||||
if bits == 128 {
|
||||
return "340282366920938463463374607431768211456".to_string();
|
||||
}
|
||||
(1u128 << bits).to_string()
|
||||
}
|
||||
|
||||
fn first_last_v4(net: Ipv4Net) -> (Option<String>, Option<String>) {
|
||||
let network = net.network();
|
||||
let broadcast = net.broadcast();
|
||||
let (first, last) = if net.prefix_len() <= 30 {
|
||||
(
|
||||
Some(Ipv4Addr::from(u32::from(network).saturating_add(1)).to_string()),
|
||||
Some(Ipv4Addr::from(u32::from(broadcast).saturating_sub(1)).to_string()),
|
||||
)
|
||||
} else {
|
||||
(Some(network.to_string()), Some(broadcast.to_string()))
|
||||
};
|
||||
(first, last)
|
||||
}
|
||||
|
||||
fn first_last_v6(net: Ipv6Net) -> (Option<String>, Option<String>) {
|
||||
(
|
||||
Some(net.network().to_string()),
|
||||
Some(net.broadcast().to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
fn overlap_v4(a: Ipv4Net, b: Ipv4Net) -> bool {
|
||||
let a_start = u32::from(a.network());
|
||||
let a_end = u32::from(a.broadcast());
|
||||
let b_start = u32::from(b.network());
|
||||
let b_end = u32::from(b.broadcast());
|
||||
a_start <= b_end && b_start <= a_end
|
||||
}
|
||||
|
||||
fn overlap_v6(a: Ipv6Net, b: Ipv6Net) -> bool {
|
||||
let a_start = u128::from(a.network());
|
||||
let a_end = u128::from(a.broadcast());
|
||||
let b_start = u128::from(b.network());
|
||||
let b_end = u128::from(b.broadcast());
|
||||
a_start <= b_end && b_start <= a_end
|
||||
}
|
||||
Reference in New Issue
Block a user