rs-utilities 0.5.1

Some utilities
Documentation
use anyhow::{Context, Result};

#[cfg(any(
    target_os = "android",
    not(any(
        target_os = "linux",
        target_os = "macos",
        target_os = "ios",
        target_os = "android"
    ))
))]
use std::net::{Ipv6Addr, SocketAddr, UdpSocket};
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios"))]
use std::process::Command;

pub fn has_usable_ipv6_route() -> bool {
    has_usable_ipv6_route_inner().unwrap_or(false)
}

#[cfg(target_os = "linux")]
fn has_usable_ipv6_route_inner() -> Result<bool> {
    let output = Command::new("ip")
        .args(["-6", "route", "get", "2001:4860:4860::8888"])
        .output()
        .context("execute ip -6 route get 2001:4860:4860::8888")?;
    if !output.status.success() {
        return Ok(false);
    }
    Ok(parse_linux_route_dev(&String::from_utf8_lossy(&output.stdout)).is_some())
}

#[cfg(any(target_os = "macos", target_os = "ios"))]
fn has_usable_ipv6_route_inner() -> Result<bool> {
    let output = Command::new("route")
        .args(["-n", "get", "-inet6", "default"])
        .output()
        .context("execute route -n get -inet6 default")?;
    if !output.status.success() {
        return Ok(false);
    }
    Ok(parse_bsd_route_gateway(&String::from_utf8_lossy(&output.stdout)).is_some())
}

#[cfg(target_os = "android")]
fn has_usable_ipv6_route_inner() -> Result<bool> {
    probe_ipv6_udp_route()
}

#[cfg(not(any(
    target_os = "linux",
    target_os = "macos",
    target_os = "ios",
    target_os = "android"
)))]
fn has_usable_ipv6_route_inner() -> Result<bool> {
    probe_ipv6_udp_route().or(Ok(false))
}

#[cfg(target_os = "linux")]
fn parse_linux_route_dev(output: &str) -> Option<String> {
    for line in output.lines() {
        let mut parts = line.split_whitespace();
        while let Some(part) = parts.next() {
            if part == "dev"
                && let Some(name) = parts.next()
                && !name.is_empty()
            {
                return Some(name.to_string());
            }
        }
    }
    None
}

#[cfg(any(target_os = "macos", target_os = "ios"))]
fn parse_bsd_route_gateway(output: &str) -> Option<String> {
    for line in output.lines() {
        let Some(rest) = line.trim_start().strip_prefix("gateway:") else {
            continue;
        };
        let value = rest.trim();
        if value.is_empty() {
            continue;
        }
        let gateway = value.split_once('%').map_or(value, |(ip, _)| ip).trim();
        if !gateway.is_empty() {
            return Some(gateway.to_string());
        }
    }
    None
}

#[cfg(any(
    target_os = "android",
    not(any(
        target_os = "linux",
        target_os = "macos",
        target_os = "ios",
        target_os = "android"
    ))
))]
fn probe_ipv6_udp_route() -> Result<bool> {
    let socket = UdpSocket::bind(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0))
        .context("bind IPv6 UDP probe socket")?;
    Ok(socket
        .connect(SocketAddr::new(
            Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888).into(),
            53,
        ))
        .is_ok())
}

#[cfg(test)]
mod tests {
    #[cfg(any(target_os = "macos", target_os = "ios"))]
    use super::parse_bsd_route_gateway;
    #[cfg(target_os = "linux")]
    use super::parse_linux_route_dev;

    #[cfg(target_os = "linux")]
    #[test]
    fn parse_linux_route_dev_extracts_interface_name() {
        let output =
            "2001:4860:4860::8888 from :: via fe80::1 dev eth0 proto ra metric 100 pref medium\n";
        assert_eq!(parse_linux_route_dev(output), Some("eth0".to_string()));
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn parse_linux_route_dev_returns_none_without_interface() {
        let output = "unreachable 2001:4860:4860::8888 from :: metric 1024 error -101\n";
        assert_eq!(parse_linux_route_dev(output), None);
    }

    #[cfg(any(target_os = "macos", target_os = "ios"))]
    #[test]
    fn parse_bsd_route_gateway_extracts_gateway() {
        let output = "   route to: default\n destination: default\n    gateway: fe80::1%en0\n";
        assert_eq!(parse_bsd_route_gateway(output), Some("fe80::1".to_string()));
    }

    #[cfg(any(target_os = "macos", target_os = "ios"))]
    #[test]
    fn parse_bsd_route_gateway_returns_none_without_gateway() {
        let output = "   route to: default\n destination: default\n";
        assert_eq!(parse_bsd_route_gateway(output), None);
    }
}