diff options
| author | ache <ache@ache.one> | 2026-08-04 09:43:58 +0200 |
|---|---|---|
| committer | ache <ache@ache.one> | 2026-08-04 09:43:58 +0200 |
| commit | aecb7ac234ddb38772f3a968c3fa43ec3b507315 (patch) | |
| tree | 8752022ffab608719305103b53209910801f20b4 | |
Init commit
| -rw-r--r-- | Cargo.toml | 9 | ||||
| -rw-r--r-- | src/args.rs | 43 | ||||
| -rw-r--r-- | src/ip.rs | 91 | ||||
| -rw-r--r-- | src/main.rs | 108 | ||||
| -rw-r--r-- | src/pinger.rs | 35 | ||||
| -rw-r--r-- | src/print.rs | 96 | ||||
| -rw-r--r-- | src/state.rs | 17 |
7 files changed, 399 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dbd0150 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rusty-ping" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.4", features = ["derive"] } +ping = "0.9.0" +tokio = { version = "1", features = ["full"] } diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..09de553 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,43 @@ +use clap::{Parser, ArgAction}; + +#[derive(Parser, Debug)] +#[command(name = "ping")] +#[command(about = "A ping program")] +#[command(disable_help_flag = true)] +pub struct Args { + /// Beep on each ping + #[arg(short = 'a', long = "beep", action = ArgAction::SetTrue)] + pub beep: bool, + + /// Number of ping to make + #[arg(short = 'c', long = "count", value_name = "count")] + pub count: Option<u32>, + + /// Interface to use + #[arg(short = 'I', value_name = "interface")] + pub interface: Option<String>, + + /// number of seconds to wait between 2 pings + #[arg(short = 'i', value_name = "seconds")] + pub seconds: Option<f32>, + + /// the Time-to-live + #[arg(short = 't', long = "ttl", value_name = "TTL")] + pub ttl: Option<u32>, + + /// Show the statistics + #[arg(short = 's', long = "stats", action = ArgAction::SetTrue)] + pub stats: bool, + + /// Print the Help line + #[arg(short = 'H', action = ArgAction::Help, required = false)] + pub help: Option<bool>, + + /// Print the remainder line + #[arg(short = 'l', long = "remainder", action = ArgAction::SetTrue)] + pub remainder: bool, + + /// Target IP address + #[arg(value_name = "target")] + pub target: Option<String>, +} diff --git a/src/ip.rs b/src/ip.rs new file mode 100644 index 0000000..068d38b --- /dev/null +++ b/src/ip.rs @@ -0,0 +1,91 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +fn parse_bsd_part(part: &str) -> Result<u32, String> { + let part = part.trim(); + if part.is_empty() { + return Err("empty part".into()); + } + + // Determine base and skip prefix if hex or octal + let (base, start_idx) = if part.starts_with("0x") || part.starts_with("0X") { + (16, 2) + } else if part.starts_with('0') && part.len() > 1 { + (8, 1) + } else { + (10, 0) + }; + + let num_str = &part[start_idx..]; + + // If base is 8 or 16, num_str must not be empty (e.g., "0x" or "0o" is invalid) + if (base == 8 || base == 16) && num_str.is_empty() { + return Err("invalid number format".into()); + } + + u32::from_str_radix(num_str, base).map_err(|e| format!("invalid number: {}", e)) +} + +pub fn parse_bsd_ipv4(s: &str) -> Result<Ipv4Addr, String> { + let parts: Vec<&str> = s.split('.').collect(); + let n = parts.len(); + + if n < 1 || n > 4 { + return Err("invalid number of parts".into()); + } + + let mut parsed_parts: Vec<u32> = Vec::with_capacity(n); + for part in parts { + let parsed = parse_bsd_part(part)?; + parsed_parts.push(parsed); + } + + let addr: u32 = match n { + 4 => { + if parsed_parts[0] > 0xFF || parsed_parts[1] > 0xFF || + parsed_parts[2] > 0xFF || parsed_parts[3] > 0xFF { + return Err("invalid IP part out of range".into()); + } + (parsed_parts[0] << 24) | + (parsed_parts[1] << 16) | + (parsed_parts[2] << 8) | + parsed_parts[3] + } + 3 => { + if parsed_parts[0] > 0xFF || parsed_parts[1] > 0xFF || parsed_parts[2] > 0xFFFF { + return Err("invalid IP part out of range".into()); + } + (parsed_parts[0] << 24) | + (parsed_parts[1] << 16) | + parsed_parts[2] + } + 2 => { + if parsed_parts[0] > 0xFF || parsed_parts[1] > 0xFFFFFF { + return Err("invalid IP part out of range".into()); + } + (parsed_parts[0] << 24) | + parsed_parts[1] + } + 1 => { + parsed_parts[0] + } + _ => { + return Err("invalid number of parts".into()); + } + }; + + Ok(Ipv4Addr::from(addr)) +} + +pub fn parse_ip(s: &str) -> Result<IpAddr, String> { + // First try to parse as IPv6 + if let Ok(ip) = s.parse::<Ipv6Addr>() { + return Ok(IpAddr::V6(ip)); + } + + // Try to parse as BSD valid IPv4 + if let Ok(ipv4) = parse_bsd_ipv4(s) { + return Ok(IpAddr::V4(ipv4)); + } + + Err(format!("invalid IP address: {}", s)) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cf24d9b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,108 @@ +use clap::Parser; +use std::net::IpAddr; +use tokio::sync::mpsc; +use tokio::sync::mpsc::error::TryRecvError; +use tokio::time::sleep; + +mod args; +mod ip; +mod pinger; +mod print; +mod state; +use args::Args; +use ip::parse_ip; +use pinger::{spawn_ping, PingResult}; +use print::{print_ping_line, print_legende_line}; +use state::State; + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let target: IpAddr = if let Some(target_str) = args.target.clone() { + parse_ip(&target_str).unwrap_or_else(|e| { + eprintln!("Error: {}", e); + std::process::exit(1); + }) + } else { + eprintln!("Error: target address is required"); + std::process::exit(1); + }; + + let (tx, mut rx) = mpsc::channel::<PingResult>(10); + + let mut states: Vec<State> = Vec::new(); + let mut packet_id: u32 = 0; + + + print_legende_line(); + + // Save the current cursor position + if !std::process::Command::new("tput").arg("sc").status().map(|s| s.success()).unwrap_or(false) { + println!("Process failed"); + std::process::exit(1); + } + + + // Should be quit with Ctrl + C + loop { + + let current_time = State::current_time_ms(); + for state in states.iter_mut() { + if let State::NotYetReceived { time_sent } = state { + // TODO: 2000ms Should be a configurable variable + if current_time - *time_sent > 2000 { // 2s = 2000ms + *state = State::Lost; + } + } + } + + // Send the ping + states.push(State::NotYetReceived { time_sent: current_time }); + spawn_ping(target, tx.clone(), packet_id); + packet_id += 1; + + // The update state loop + loop { + match rx.try_recv() { + // Update the state of the received packet + Ok(result) => { + match result { + PingResult::Ping { rrt, ttl, id } => { + if (id as usize) < states.len() { + states[id as usize] = State::Received { rtt: rrt, ttl }; + } + } + PingResult::NoPingErr { id, error } => { + // NOTE: Maybe we should quit, I don't know. + if (id as usize) < states.len() { + states[id as usize] = State::SendError; + } + } + } + } + Err(TryRecvError::Disconnected) => { + // NOTE: Maybe we should quit, I don't know. + break; + } + Err(TryRecvError::Empty) => { + // No more ping to read + break; + } + } + } + + // Put the cursor back up + if !std::process::Command::new("tput").arg("rc").status().map(|s| s.success()).unwrap_or(false) { + println!("Process failed too"); + std::process::exit(1); + } + + // Prints lines based on the state of each ping + print_ping_line(&states); + + + // Sleep for 1 second before the next ping + sleep(tokio::time::Duration::from_millis(1000)).await; + } +} diff --git a/src/pinger.rs b/src/pinger.rs new file mode 100644 index 0000000..ef87cbf --- /dev/null +++ b/src/pinger.rs @@ -0,0 +1,35 @@ +use std::net::IpAddr; +use std::time::Duration; +use tokio::sync::mpsc::Sender; +use tokio::task; + +pub enum PingResult { + NoPingErr { id: u32, error: String }, + Ping { rrt: u64, ttl: u32, id: u32 }, +} + +pub fn spawn_ping(target: IpAddr, tx: Sender<PingResult>, id: u32) { + // let _ = tx.try_send(PingResult::NoPingErr { id, error: "Test Ping".to_string() }); + task::spawn_blocking(move || { + let result = do_ping(target, id); + let _ = tx.try_send(result); + }); +} + +fn do_ping(target: IpAddr, id: u32) -> PingResult { + let payload: [u8; 24] = [0u8; 24]; + + let result = ping::new(target) + .timeout(Duration::from_secs(3)) + .payload(&payload) + .send(); + + match result { + Ok(reply) => PingResult::Ping { + rrt: reply.rtt.as_millis() as u64, + ttl: reply.ttl.unwrap_or(0) as u32, + id, + }, + Err(e) => PingResult::NoPingErr { id, error: format!("Ping failed: {}", e) }, + } +} diff --git a/src/print.rs b/src/print.rs new file mode 100644 index 0000000..9d71a78 --- /dev/null +++ b/src/print.rs @@ -0,0 +1,96 @@ +// src/print.rs + +use crate::state::State; + +struct SymbolTableElement { + fg: &'static str, + bg: &'static str, + character: &'static str, +} + +const GREEN_FG :&str = "\x1b[32m"; +const ORANGE_FG:&str = "\x1b[38;5;208m"; +const RED_FG:&str = "\x1b[31m"; +const BLACK_FG:&str = "\x1b[31m"; +const BLACK_BG:&str = "\x1b[40m"; +const GREEN_BG:&str = "\x1b[40m"; +const ORANGE_BG:&str = "\x1b[48;5;208m"; +const RESET :&str= "\x1b[40m"; + +const CHARS_TABLE :[SymbolTableElement; 25] = [ + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▁"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▂"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▃"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▄"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▅"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▆"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "▇"}, + SymbolTableElement{ fg: GREEN_FG, bg: BLACK_BG, character: "█"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▁"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▂"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▃"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▄"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▅"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▆"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "▇"}, + SymbolTableElement{ fg: ORANGE_FG, bg: GREEN_BG, character: "█"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▁"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▂"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▃"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▄"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▅"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▆"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "▇"}, + SymbolTableElement{ fg: RED_FG, bg: ORANGE_BG, character: "█"}, + SymbolTableElement{ fg: RED_FG, bg: BLACK_BG, character: "?"} +]; + +pub fn print_info_line() { + for n in 0..24 { + let (fg, bg, symbol) = get_symbol_and_colors_for_rrt(n as u64); + print!("{} {}{}{}\x1b033[0m ", n*10, fg, bg, symbol); + } +} + +pub fn print_legende_line() { + for (index, element) in CHARS_TABLE.iter().enumerate() { + let val = 10 * index as u64; + print!("{} {}{}{}\x1b[0m ", val, element.fg, element.bg, element.character); + } + println!(""); +} + +pub fn print_ping_line(states: &Vec<State>) { + for state in states { + match state { + State::Received { rtt, .. } => { + let (fg, bg, symbol) = get_symbol_and_colors_for_rrt(*rtt); + print!("{}{}{}{}", fg, bg, symbol, "\x1b[0m"); + } + State::Lost => { + let (fg, bg, symbol) = get_symbol_and_colors_for_rrt(250); + print!("{}{}{}{}", fg, bg, symbol, "\x1b[0m"); + } + State::NotYetReceived { .. } => { + // fg grey, bg black + print!("\x1b[35;40m?\x1b[0m"); + } + State::SendError => { + print!("!"); + } + } + } + + println!(""); +} + +fn get_symbol_and_colors_for_rrt(rrt: u64) -> (&'static str, &'static str, &'static str) { + let index: usize = if rrt >= 240 { + 24 + } else { + rrt / 10 + } as usize; + let SymbolTableElement{fg, bg, character} = CHARS_TABLE[index]; + + (fg, bg, character) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..fe1fa4a --- /dev/null +++ b/src/state.rs @@ -0,0 +1,17 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub enum State { + NotYetReceived { time_sent: u64 }, + SendError, + Received { rtt: u64, ttl: u32 }, + Lost, +} + +impl State { + pub fn current_time_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 + } +} |