From 807882b59143ad8812db9def1a0716dc09d58087 Mon Sep 17 00:00:00 2001 From: am2rican5 Date: Tue, 23 Dec 2025 15:22:28 +0900 Subject: [PATCH 1/2] feat: Add CIDR calculator tool Computes network information from CIDR notation including: - Address in string, decimal, and hex formats - Network and broadcast addresses - First/last usable hosts and total host count - Netmask and wildcard in string and hex formats --- src/main.rs | 1 + src/tools/cidr.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++++ src/tools/mod.rs | 1 + 3 files changed, 189 insertions(+) create mode 100644 src/tools/cidr.rs diff --git a/src/main.rs b/src/main.rs index 1effd54..4351843 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,7 @@ fn main() -> anyhow::Result<()> { (tools::bcrypt::BcryptTool, "bcrypt",), (tools::calc::CalcTool, "calc", "cal"), (tools::case::CaseTool, "case",), + (tools::cidr::CidrTool, "cidr",), (tools::color::ColorTool, "color",), (tools::crontab::CrontabTool, "crontab", "cron"), (tools::datetime::DateTimeTool, "datetime", "dt"), diff --git a/src/tools/cidr.rs b/src/tools/cidr.rs new file mode 100644 index 0000000..739d41b --- /dev/null +++ b/src/tools/cidr.rs @@ -0,0 +1,187 @@ +use crate::args::StringInput; +use crate::tool::{Output, Tool}; +use anyhow::{Result, bail}; +use clap::Parser; +use serde_json::json; +use std::net::Ipv4Addr; + +#[derive(Parser, Debug)] +#[command(about = "CIDR calculator - compute network information from CIDR notation")] +pub struct CidrTool { + /// CIDR notation (e.g., 192.168.1.0/24) + cidr: StringInput, +} + +fn ip_to_hex(ip: u32) -> String { + let bytes = ip.to_be_bytes(); + format!( + "{:02X}.{:02X}.{:02X}.{:02X}", + bytes[0], bytes[1], bytes[2], bytes[3] + ) +} + +impl Tool for CidrTool { + fn cli() -> clap::Command { + ::command() + } + + fn execute(&self) -> Result> { + let input = self.cidr.0.trim(); + + let parts: Vec<&str> = input.split('/').collect(); + if parts.len() != 2 { + bail!("Invalid CIDR notation. Expected format: IP/prefix (e.g., 192.168.1.0/24)"); + } + + let ip: Ipv4Addr = parts[0] + .parse() + .map_err(|_| anyhow::anyhow!("Invalid IP address: {}", parts[0]))?; + + let prefix: u8 = parts[1] + .parse() + .map_err(|_| anyhow::anyhow!("Invalid prefix length: {}", parts[1]))?; + + if prefix > 32 { + bail!("Prefix length must be between 0 and 32, got: {}", prefix); + } + + let ip_u32: u32 = ip.into(); + + let netmask: u32 = if prefix == 0 { + 0 + } else { + !0u32 << (32 - prefix) + }; + + let wildcard: u32 = !netmask; + + let network: u32 = ip_u32 & netmask; + + let broadcast: u32 = network | wildcard; + + let (first_host, last_host, total_hosts): (u32, u32, u64) = match prefix { + 32 => (ip_u32, ip_u32, 1), + 31 => (network, broadcast, 2), + _ => (network + 1, broadcast - 1, (1u64 << (32 - prefix)) - 2), + }; + + Ok(Some(Output::JsonValue(json!({ + "address": Ipv4Addr::from(ip_u32).to_string(), + "address_decimal": ip_u32, + "address_hex": ip_to_hex(ip_u32), + "network": Ipv4Addr::from(network).to_string(), + "network_decimal": network, + "network_hex": ip_to_hex(network), + "broadcast": Ipv4Addr::from(broadcast).to_string(), + "broadcast_decimal": broadcast, + "broadcast_hex": ip_to_hex(broadcast), + "first_host": Ipv4Addr::from(first_host).to_string(), + "last_host": Ipv4Addr::from(last_host).to_string(), + "total_hosts": total_hosts, + "prefix": prefix, + "netmask": Ipv4Addr::from(netmask).to_string(), + "netmask_hex": ip_to_hex(netmask), + "wildcard": Ipv4Addr::from(wildcard).to_string(), + "wildcard_hex": ip_to_hex(wildcard), + })))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn run_cidr(input: &str) -> serde_json::Value { + let tool = CidrTool { + cidr: StringInput(input.to_string()), + }; + match tool.execute().unwrap().unwrap() { + Output::JsonValue(v) => v, + _ => panic!("Expected JsonValue"), + } + } + + #[test] + fn test_class_c_network() { + let result = run_cidr("192.168.1.100/24"); + assert_eq!(result["address"], "192.168.1.100"); + assert_eq!(result["address_decimal"], 3232235876u64); + assert_eq!(result["address_hex"], "C0.A8.01.64"); + assert_eq!(result["network"], "192.168.1.0"); + assert_eq!(result["broadcast"], "192.168.1.255"); + assert_eq!(result["first_host"], "192.168.1.1"); + assert_eq!(result["last_host"], "192.168.1.254"); + assert_eq!(result["total_hosts"], 254); + assert_eq!(result["prefix"], 24); + assert_eq!(result["netmask"], "255.255.255.0"); + assert_eq!(result["netmask_hex"], "FF.FF.FF.00"); + assert_eq!(result["wildcard"], "0.0.0.255"); + assert_eq!(result["wildcard_hex"], "00.00.00.FF"); + } + + #[test] + fn test_single_host() { + let result = run_cidr("10.0.0.1/32"); + assert_eq!(result["network"], "10.0.0.1"); + assert_eq!(result["broadcast"], "10.0.0.1"); + assert_eq!(result["first_host"], "10.0.0.1"); + assert_eq!(result["last_host"], "10.0.0.1"); + assert_eq!(result["total_hosts"], 1); + assert_eq!(result["netmask"], "255.255.255.255"); + assert_eq!(result["wildcard"], "0.0.0.0"); + } + + #[test] + fn test_point_to_point() { + let result = run_cidr("10.0.0.0/31"); + assert_eq!(result["network"], "10.0.0.0"); + assert_eq!(result["broadcast"], "10.0.0.1"); + assert_eq!(result["first_host"], "10.0.0.0"); + assert_eq!(result["last_host"], "10.0.0.1"); + assert_eq!(result["total_hosts"], 2); + } + + #[test] + fn test_class_a_network() { + let result = run_cidr("10.0.0.0/8"); + assert_eq!(result["network"], "10.0.0.0"); + assert_eq!(result["broadcast"], "10.255.255.255"); + assert_eq!(result["netmask"], "255.0.0.0"); + assert_eq!(result["wildcard"], "0.255.255.255"); + assert_eq!(result["total_hosts"], 16777214u64); + } + + #[test] + fn test_all_networks() { + let result = run_cidr("0.0.0.0/0"); + assert_eq!(result["network"], "0.0.0.0"); + assert_eq!(result["broadcast"], "255.255.255.255"); + assert_eq!(result["netmask"], "0.0.0.0"); + assert_eq!(result["wildcard"], "255.255.255.255"); + assert_eq!(result["total_hosts"], 4294967294u64); + } + + #[test] + fn test_invalid_cidr_no_prefix() { + let tool = CidrTool { + cidr: StringInput("192.168.1.0".to_string()), + }; + assert!(tool.execute().is_err()); + } + + #[test] + fn test_invalid_prefix_too_large() { + let tool = CidrTool { + cidr: StringInput("192.168.1.0/33".to_string()), + }; + assert!(tool.execute().is_err()); + } + + #[test] + fn test_invalid_ip() { + let tool = CidrTool { + cidr: StringInput("256.168.1.0/24".to_string()), + }; + assert!(tool.execute().is_err()); + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index fe8b7be..609281e 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -2,6 +2,7 @@ pub mod base64; pub mod bcrypt; pub mod calc; pub mod case; +pub mod cidr; pub mod color; pub mod crontab; pub mod datetime; From 3d2c1220857f9c111b03c569c16b20db35d4b691 Mon Sep 17 00:00:00 2001 From: am2rican5 Date: Tue, 23 Dec 2025 15:24:04 +0900 Subject: [PATCH 2/2] docs: Add CIDR calculator to README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 4c159f3..eec640c 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ After setting up completions, restart your shell or source your configuration fi │ │ └── schedule │ └── datetime (dt) - Parse and convert datetimes ├── Web & Network +│ ├── cidr - CIDR calculator │ ├── http - HTTP utilities │ │ └── status │ ├── serve - Local HTTP file server @@ -399,6 +400,19 @@ echo -n "2025-10-04T15:30:00Z" | ut datetime - ### Web & Network +#### `cidr` +CIDR calculator for computing network information from CIDR notation. +- Displays address, network, and broadcast in string, decimal, and hex formats +- Calculates usable host range and total host count +- Shows netmask and wildcard masks + +```bash +ut cidr 192.168.1.100/24 +ut cidr 10.0.0.0/8 +ut cidr 172.16.0.1/16 +echo -n "192.168.1.0/24" | ut cidr - +``` + #### `http` HTTP utilities including status code lookup.