mcrx-core 0.2.0

Runtime-agnostic and portable multicast receiver library for IPv4 and IPv6 ASM/SSM.
Documentation
use std::net::IpAddr;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReceiveCliArgs {
    pub(crate) group: IpAddr,
    pub(crate) dst_port: u16,
    pub(crate) source: Option<IpAddr>,
    pub(crate) interface: Option<IpAddr>,
}

pub(crate) fn parse_receive_cli_args(args: &[String]) -> Result<ReceiveCliArgs, String> {
    if args.len() < 3 {
        return Err("invalid arguments".to_string());
    }

    let group = parse_ip("group", &args[1])?;
    let dst_port = parse_port(&args[2])?;
    let remainder = &args[3..];

    let (source, interface) = parse_mixed_args(remainder)?;

    if !group.is_multicast() {
        return Err(format!("group address {group} is not multicast"));
    }

    Ok(ReceiveCliArgs {
        group,
        dst_port,
        source,
        interface,
    })
}

fn parse_flag_args(remainder: &[String]) -> Result<(Option<IpAddr>, Option<IpAddr>), String> {
    let mut source = None;
    let mut interface = None;
    let mut index = 0usize;

    while index < remainder.len() {
        match remainder[index].as_str() {
            "--source" => {
                let value = remainder
                    .get(index + 1)
                    .ok_or_else(|| "missing value after --source".to_string())?;
                source = Some(parse_ip("source", value)?);
                index += 2;
            }
            "--interface" => {
                let value = remainder
                    .get(index + 1)
                    .ok_or_else(|| "missing value after --interface".to_string())?;
                interface = Some(parse_ip("interface", value)?);
                index += 2;
            }
            other => {
                return Err(format!("unexpected argument '{other}'"));
            }
        }
    }

    Ok((source, interface))
}

fn parse_mixed_args(remainder: &[String]) -> Result<(Option<IpAddr>, Option<IpAddr>), String> {
    let mut positional = Vec::new();
    let mut flagged = Vec::new();
    let mut index = 0usize;

    while index < remainder.len() {
        if remainder[index].starts_with("--") {
            flagged.push(remainder[index].clone());
            let value = remainder
                .get(index + 1)
                .ok_or_else(|| format!("missing value after {}", remainder[index]))?;
            flagged.push(value.clone());
            index += 2;
        } else {
            positional.push(remainder[index].clone());
            index += 1;
        }
    }

    let (mut source, mut interface) = parse_flag_args(&flagged)?;

    let mut positional = positional.into_iter();

    if source.is_none()
        && let Some(value) = positional.next()
    {
        source = Some(parse_ip("source", &value)?);
    }

    if interface.is_none()
        && let Some(value) = positional.next()
    {
        interface = Some(parse_ip("interface", &value)?);
    }

    if positional.next().is_some() {
        return Err("invalid arguments".to_string());
    }

    Ok((source, interface))
}

fn parse_ip(name: &str, value: &str) -> Result<IpAddr, String> {
    value
        .parse::<IpAddr>()
        .map_err(|err| format!("invalid {name} '{value}': {err}"))
}

fn parse_port(value: &str) -> Result<u16, String> {
    let port = value
        .parse::<u16>()
        .map_err(|err| format!("invalid dst_port '{value}': {err}"))?;

    if port == 0 {
        return Err("dst_port must not be 0".to_string());
    }

    Ok(port)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{Ipv4Addr, Ipv6Addr};

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|part| (*part).to_string()).collect()
    }

    #[test]
    fn parses_legacy_positional_asm_args() {
        let args = argv(&["mcrx-recv", "239.1.2.3", "5000"]);

        let parsed = parse_receive_cli_args(&args).unwrap();

        assert_eq!(parsed.group, IpAddr::V4(Ipv4Addr::new(239, 1, 2, 3)));
        assert_eq!(parsed.dst_port, 5000);
        assert_eq!(parsed.source, None);
        assert_eq!(parsed.interface, None);
    }

    #[test]
    fn parses_flagged_interface_for_ipv6_asm() {
        let args = argv(&["mcrx-recv-meta", "ff01::1234", "5000", "--interface", "::1"]);

        let parsed = parse_receive_cli_args(&args).unwrap();

        assert_eq!(
            parsed.group,
            IpAddr::V6("ff01::1234".parse::<Ipv6Addr>().unwrap())
        );
        assert_eq!(parsed.dst_port, 5000);
        assert_eq!(parsed.source, None);
        assert_eq!(parsed.interface, Some(IpAddr::V6(Ipv6Addr::LOCALHOST)));
    }

    #[test]
    fn parses_flagged_source_and_interface() {
        let args = argv(&[
            "mcrx-recv",
            "232.1.2.3",
            "5000",
            "--source",
            "192.168.1.10",
            "--interface",
            "192.168.1.20",
        ]);

        let parsed = parse_receive_cli_args(&args).unwrap();

        assert_eq!(
            parsed.source,
            Some(IpAddr::V4("192.168.1.10".parse::<Ipv4Addr>().unwrap()))
        );
        assert_eq!(
            parsed.interface,
            Some(IpAddr::V4("192.168.1.20".parse::<Ipv4Addr>().unwrap()))
        );
    }

    #[test]
    fn parses_positional_source_with_flagged_interface() {
        let args = argv(&[
            "mcrx-recv-meta",
            "ff12::1234",
            "5000",
            "fd06:ba51:f296:0:1caf:6b66:e6f7:4b10",
            "--interface",
            "fd06:ba51:f296:0:1caf:6b66:e6f7:4b10",
        ]);

        let parsed = parse_receive_cli_args(&args).unwrap();

        assert_eq!(
            parsed.source,
            Some(IpAddr::V6(
                "fd06:ba51:f296:0:1caf:6b66:e6f7:4b10"
                    .parse::<Ipv6Addr>()
                    .unwrap()
            ))
        );
        assert_eq!(
            parsed.interface,
            Some(IpAddr::V6(
                "fd06:ba51:f296:0:1caf:6b66:e6f7:4b10"
                    .parse::<Ipv6Addr>()
                    .unwrap()
            ))
        );
    }
}