aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs43
-rw-r--r--src/ip.rs91
-rw-r--r--src/main.rs108
-rw-r--r--src/pinger.rs35
-rw-r--r--src/print.rs96
-rw-r--r--src/state.rs17
6 files changed, 390 insertions, 0 deletions
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
+ }
+}