Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
187 changes: 187 additions & 0 deletions src/tools/cidr.rs
Original file line number Diff line number Diff line change
@@ -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 {
<Self as clap::CommandFactory>::command()
}

fn execute(&self) -> Result<Option<Output>> {
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());
}
}
1 change: 1 addition & 0 deletions src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down