anti-common 0.1.2

Unified CLI toolbox
Documentation
//! Common types and utilities for ping operations

use std::net::{IpAddr, Ipv4Addr};
use std::time::Duration;
use thiserror::Error;

/// Errors that can occur during ping operations
#[derive(Error, Debug, Clone)]
pub enum PingError {
    #[error("Failed to create socket: {0}")]
    SocketCreation(String),

    #[error("Permission denied: {context}")]
    PermissionDenied { context: String },

    #[error("Timeout after {duration:?}")]
    Timeout { duration: Duration },

    #[error("Invalid response: {reason}")]
    InvalidResponse { reason: String },

    #[error("Network unreachable")]
    NetworkUnreachable,

    #[error("Host unreachable")]
    HostUnreachable,

    #[error("Port unreachable")]
    PortUnreachable,

    #[error("Configuration error: {message}")]
    Configuration { message: String },

    #[error("Invalid target: {0}")]
    InvalidTarget(String),
}

/// Result type for ping operations
pub type PingResult<T> = Result<T, PingError>;

/// Configuration for ping operations
#[derive(Debug, Clone)]
pub struct PingConfig {
    /// Target IP address
    pub target: Ipv4Addr,
    /// Number of ping packets to send
    pub count: u16,
    /// Timeout for each ping
    pub timeout: Duration,
    /// Interval between pings
    pub interval: Duration,
    /// Packet size in bytes
    pub packet_size: usize,
    /// Custom identifier (if None, random will be generated)
    pub identifier: Option<u16>,
}

impl Default for PingConfig {
    fn default() -> Self {
        Self {
            target: Ipv4Addr::new(127, 0, 0, 1),
            count: 4,
            timeout: Duration::from_secs(5),
            interval: Duration::from_secs(1),
            packet_size: 64,
            identifier: None,
        }
    }
}

/// Result of a single ping operation
#[derive(Debug, Clone)]
pub struct PingReply {
    /// Sequence number of the ping
    pub sequence: u16,
    /// Round-trip time
    pub rtt: Duration,
    /// Size of the response in bytes
    pub bytes_received: usize,
    /// Source address of the response
    pub from: Ipv4Addr,
    /// Time-to-live of the response packet
    pub ttl: Option<u8>,
}

/// Summary statistics for a series of ping operations
#[derive(Debug, Clone)]
pub struct PingStatistics {
    /// Total packets transmitted
    pub packets_transmitted: u32,
    /// Total packets received
    pub packets_received: u32,
    /// Packet loss percentage (0.0 to 100.0)
    pub packet_loss: f64,
    /// Minimum round-trip time
    pub min_rtt: Option<Duration>,
    /// Maximum round-trip time
    pub max_rtt: Option<Duration>,
    /// Average round-trip time
    pub avg_rtt: Option<Duration>,
    /// Standard deviation of round-trip times
    pub stddev_rtt: Option<Duration>,
}

impl PingStatistics {
    /// Create new empty statistics
    pub fn new() -> Self {
        Self {
            packets_transmitted: 0,
            packets_received: 0,
            packet_loss: 0.0,
            min_rtt: None,
            max_rtt: None,
            avg_rtt: None,
            stddev_rtt: None,
        }
    }

    /// Update statistics with a new ping reply
    pub fn add_reply(&mut self, reply: &PingReply) {
        self.packets_received += 1;

        // Update min/max
        self.min_rtt = Some(self.min_rtt.map_or(reply.rtt, |min| min.min(reply.rtt)));
        self.max_rtt = Some(self.max_rtt.map_or(reply.rtt, |max| max.max(reply.rtt)));
    }

    /// Record a transmitted packet
    pub fn add_transmitted(&mut self) {
        self.packets_transmitted += 1;
    }

    /// Finalize statistics calculations
    pub fn finalize(&mut self, rtts: &[Duration]) {
        // Calculate packet loss
        self.packet_loss = if self.packets_transmitted > 0 {
            100.0 * (1.0 - (self.packets_received as f64 / self.packets_transmitted as f64))
        } else {
            0.0
        };

        // Calculate average
        if !rtts.is_empty() {
            let total: Duration = rtts.iter().sum();
            self.avg_rtt = Some(total / rtts.len() as u32);

            // Calculate standard deviation
            if let Some(avg) = self.avg_rtt {
                let variance: f64 = rtts
                    .iter()
                    .map(|rtt| {
                        let diff = rtt.as_secs_f64() - avg.as_secs_f64();
                        diff * diff
                    })
                    .sum::<f64>()
                    / rtts.len() as f64;

                self.stddev_rtt = Some(Duration::from_secs_f64(variance.sqrt()));
            }
        }
    }
}

impl Default for PingStatistics {
    fn default() -> Self {
        Self::new()
    }
}

/// Type of ping operation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PingMode {
    /// ICMP echo request/reply
    Icmp,
    /// UDP packet with ICMP Port Unreachable response
    Udp,
    /// TCP connection attempt
    Tcp,
}

impl std::fmt::Display for PingMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PingMode::Icmp => write!(f, "ICMP"),
            PingMode::Udp => write!(f, "UDP"),
            PingMode::Tcp => write!(f, "TCP"),
        }
    }
}

/// Calculate internet checksum for a byte array
pub fn calculate_checksum(data: &[u8]) -> u16 {
    let mut sum = 0u32;

    // Sum all 16-bit words
    for chunk in data.chunks(2) {
        if chunk.len() == 2 {
            sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
        } else {
            // Handle odd-length data
            sum += (chunk[0] as u32) << 8;
        }
    }

    // Add carry bits
    while (sum >> 16) != 0 {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }

    // One's complement
    !sum as u16
}

/// Resolve hostname to IPv4 address
pub fn resolve_hostname(hostname: &str) -> PingResult<Ipv4Addr> {
    // Try to parse as IP address first
    if let Ok(ip) = hostname.parse::<Ipv4Addr>() {
        return Ok(ip);
    }

    // Handle localhost specially
    if hostname == "localhost" {
        return Ok(Ipv4Addr::new(127, 0, 0, 1));
    }

    // Use proper DNS resolution
    use std::net::ToSocketAddrs;
    let hostname_with_port = format!("{}:80", hostname);
    match hostname_with_port.to_socket_addrs() {
        Ok(mut addrs) => {
            if let Some(addr) = addrs.next() {
                if let std::net::IpAddr::V4(ipv4) = addr.ip() {
                    return Ok(ipv4);
                }
            }
            Err(PingError::Configuration {
                message: "Could not resolve to IPv4 address".to_string(),
            })
        }
        Err(_) => Err(PingError::Configuration {
            message: format!("Cannot resolve hostname: {}", hostname),
        }),
    }
}

/// Resolve a hostname to all associated IPv4 and IPv6 addresses
pub fn resolve_hostnames(hostname: &str) -> PingResult<Vec<IpAddr>> {
    use std::net::{Ipv6Addr, ToSocketAddrs};

    // Direct IP
    if let Ok(ip) = hostname.parse::<IpAddr>() {
        return Ok(vec![ip]);
    }

    if hostname == "localhost" {
        return Ok(vec![
            IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
            IpAddr::V6(Ipv6Addr::LOCALHOST),
        ]);
    }

    let host_port = format!("{}:0", hostname);
    match host_port.to_socket_addrs() {
        Ok(addrs) => {
            let mut ips: Vec<IpAddr> = addrs.map(|a| a.ip()).collect();
            ips.sort();
            ips.dedup();
            if ips.is_empty() {
                Err(PingError::Configuration {
                    message: "Could not resolve hostname".to_string(),
                })
            } else {
                Ok(ips)
            }
        }
        Err(_) => Err(PingError::Configuration {
            message: format!("Cannot resolve hostname: {}", hostname),
        }),
    }
}

/// ICMP packet constants
pub mod icmp {
    pub const ECHO_REQUEST: u8 = 8;
    pub const ECHO_REPLY: u8 = 0;
    pub const DEST_UNREACHABLE: u8 = 3;
    pub const PORT_UNREACHABLE: u8 = 3;
}

/// Common UDP ports for testing
pub mod ports {
    pub const DNS: u16 = 53;
    pub const HTTP: u16 = 80;
    pub const HTTPS: u16 = 443;
    pub const NTP: u16 = 123;
    pub const SNMP: u16 = 161;
    pub const SSH: u16 = 22;
    pub const TRACEROUTE_BASE: u16 = 33434;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_checksum() {
        let data = [0x45, 0x00, 0x00, 0x3c];
        let checksum = calculate_checksum(&data);
        assert_ne!(checksum, 0);
    }

    #[test]
    fn test_ping_statistics() {
        let mut stats = PingStatistics::new();

        stats.add_transmitted();
        stats.add_transmitted();

        let reply1 = PingReply {
            sequence: 1,
            rtt: Duration::from_millis(10),
            bytes_received: 64,
            from: Ipv4Addr::new(8, 8, 8, 8),
            ttl: Some(64),
        };

        stats.add_reply(&reply1);

        let rtts = vec![Duration::from_millis(10)];
        stats.finalize(&rtts);

        assert_eq!(stats.packets_transmitted, 2);
        assert_eq!(stats.packets_received, 1);
        assert_eq!(stats.packet_loss, 50.0);
    }

    #[test]
    fn test_resolve_localhost() {
        assert_eq!(
            resolve_hostname("localhost").unwrap(),
            Ipv4Addr::new(127, 0, 0, 1)
        );
    }

    #[test]
    fn test_resolve_ip() {
        assert_eq!(
            resolve_hostname("8.8.8.8").unwrap(),
            Ipv4Addr::new(8, 8, 8, 8)
        );
    }

    #[test]
    fn test_resolve_hostnames_local() {
        let ips = resolve_hostnames("localhost").unwrap();
        assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
    }
}