tokio-multicast 0.7.2

Small Tokio helpers for UDP multicast send/receive.
Documentation
use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::Duration;

use tokio_multicast::{diagnose_multicast, diagnose_multicast_with_config, MulticastDiagnosticConfig};

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let json = args.iter().any(|arg| arg == "--json");
    let ipv4_only = args.iter().any(|arg| arg == "--ipv4-only");
    let ipv6_only = args.iter().any(|arg| arg == "--ipv6-only");
    let timeout_ms = parse_option(&args, "--timeout-ms").and_then(|value| value.parse::<u64>().ok());
    let ipv4_group = parse_option(&args, "--ipv4-group").and_then(|value| value.parse::<Ipv4Addr>().ok());
    let ipv6_group = parse_option(&args, "--ipv6-group").and_then(|value| value.parse::<Ipv6Addr>().ok());
    let ipv6_interface =
        parse_option(&args, "--ipv6-ifindex").and_then(|value| value.parse::<u32>().ok());

    let report = if timeout_ms.is_none()
        && ipv4_group.is_none()
        && ipv6_group.is_none()
        && ipv6_interface.is_none()
    {
        diagnose_multicast()
    } else {
        let mut config = MulticastDiagnosticConfig::default();
        if let Some(timeout_ms) = timeout_ms {
            config.timeout = Duration::from_millis(timeout_ms);
        }
        if let Some(ipv4_group) = ipv4_group {
            config.ipv4_group = ipv4_group;
        }
        if let Some(ipv6_group) = ipv6_group {
            config.ipv6_group = ipv6_group;
        }
        if let Some(ipv6_interface) = ipv6_interface {
            config.ipv6_interface = Some(ipv6_interface);
        }
        diagnose_multicast_with_config(&config)
    };

    if json {
        println!("{}", render_json(&report));
    } else {
        println!("overall supported: {}", report.supported());
        if !ipv6_only {
            print_probe(&report.ipv4);
        }
        if !ipv4_only {
            print_probe(&report.ipv6);
        }
    }

    let selected_supported = if ipv4_only && !ipv6_only {
        report.ipv4.supported
    } else if ipv6_only && !ipv4_only {
        report.ipv6.supported
    } else {
        report.supported()
    };

    if !selected_supported {
        std::process::exit(2);
    }
}

fn print_probe(probe: &tokio_multicast::ProbeResult) {
    println!("{} supported: {}", probe.label, probe.supported);
    if let Some(error_kind) = probe.error_kind {
        println!(
            "{} error kind: {}",
            probe.label,
            probe_error_kind_name(error_kind)
        );
    }
    println!(
        "{} stages: socket_created={} bound={} joined={} loopback_sent={} loopback_received={}",
        probe.label,
        probe.stages.socket_created,
        probe.stages.bound,
        probe.stages.joined,
        probe.stages.loopback_sent,
        probe.stages.loopback_received
    );
    if let Some(details) = &probe.details {
        println!("{} details: {details}", probe.label);
    }
    if let Some(error) = &probe.error {
        println!("{} error: {error}", probe.label);
    }
}

fn parse_option<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
    args.windows(2)
        .find(|pair| pair[0] == name)
        .map(|pair| pair[1].as_str())
}

fn render_json(report: &tokio_multicast::MulticastDiagnostics) -> String {
    format!(
        concat!(
            "{{",
            "\"supported\":{},",
            "\"ipv4\":{},",
            "\"ipv6\":{}",
            "}}"
        ),
        report.supported(),
        render_probe_json(&report.ipv4),
        render_probe_json(&report.ipv6)
    )
}

fn render_probe_json(probe: &tokio_multicast::ProbeResult) -> String {
    format!(
        concat!(
            "{{",
            "\"label\":\"{}\",",
            "\"supported\":{},",
            "\"error_kind\":{},",
            "\"stages\":{{",
            "\"socket_created\":{},",
            "\"bound\":{},",
            "\"joined\":{},",
            "\"loopback_sent\":{},",
            "\"loopback_received\":{}",
            "}},",
            "\"details\":{},",
            "\"error\":{}",
            "}}"
        ),
        escape_json(probe.label),
        probe.supported,
        option_error_kind_json(probe.error_kind),
        probe.stages.socket_created,
        probe.stages.bound,
        probe.stages.joined,
        probe.stages.loopback_sent,
        probe.stages.loopback_received,
        option_json(&probe.details),
        option_json(&probe.error)
    )
}

fn option_json(value: &Option<String>) -> String {
    match value {
        Some(value) => format!("\"{}\"", escape_json(value)),
        None => "null".to_string(),
    }
}

fn option_error_kind_json(value: Option<tokio_multicast::ProbeErrorKind>) -> String {
    match value {
        Some(value) => format!("\"{}\"", probe_error_kind_name(value)),
        None => "null".to_string(),
    }
}

fn probe_error_kind_name(value: tokio_multicast::ProbeErrorKind) -> &'static str {
    match value {
        tokio_multicast::ProbeErrorKind::Unavailable => "unavailable",
        tokio_multicast::ProbeErrorKind::SocketCreate => "socket_create",
        tokio_multicast::ProbeErrorKind::Bind => "bind",
        tokio_multicast::ProbeErrorKind::Join => "join",
        tokio_multicast::ProbeErrorKind::Send => "send",
        tokio_multicast::ProbeErrorKind::Receive => "receive",
        tokio_multicast::ProbeErrorKind::InvalidData => "invalid_data",
        tokio_multicast::ProbeErrorKind::Other => "other",
    }
}

fn escape_json(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
}