netscli-core 0.3.4

Core networking library: discovery, scanning, DNS, ARP, PCAP, and OUI resolution
Documentation
use pcap::Device;
use std::{collections::HashSet, net::IpAddr};

use crate::error::{Error, Result};
use crate::NetworkManager;

pub(super) fn resolve_capture_device(devices: &[Device], requested: &str) -> Result<Device> {
    let requested = requested.trim();
    if requested.is_empty() {
        return Err(Error::invalid_input("capture interface is required"));
    }

    if let Some(device) = devices
        .iter()
        .find(|device| device_matches_label(device, requested))
    {
        return Ok(device.clone());
    }

    if let Some(device) = resolve_by_platform_adapter(devices, requested) {
        return Ok(device);
    }

    if let Some(device) = resolve_by_interface_addresses(devices, requested) {
        return Ok(device);
    }

    let available = devices
        .iter()
        .map(device_display_name)
        .collect::<Vec<_>>()
        .join(", ");
    Err(Error::invalid_input(format!(
        "Interface not found: {requested}. Available capture interfaces: {available}",
    )))
}

fn device_matches_label(device: &Device, requested: &str) -> bool {
    let requested = normalized_name(requested);
    if normalized_name(&device.name) == requested {
        return true;
    }

    device
        .desc
        .as_deref()
        .map(normalized_name)
        .is_some_and(|desc| desc == requested || desc.contains(&requested))
}

fn resolve_by_interface_addresses(devices: &[Device], requested: &str) -> Option<Device> {
    let requested = normalized_name(requested);
    let iface = NetworkManager::get_interfaces()
        .into_iter()
        .find(|iface| normalized_name(&iface.name) == requested)?;
    let ips = iface.ips.iter().map(|ip| ip.addr()).collect::<HashSet<_>>();

    device_by_addresses(devices, &ips)
}

fn device_by_addresses(devices: &[Device], ips: &HashSet<IpAddr>) -> Option<Device> {
    if ips.is_empty() {
        return None;
    }

    devices
        .iter()
        .find(|device| {
            device
                .addresses
                .iter()
                .any(|address| ips.contains(&address.addr))
        })
        .cloned()
}

#[cfg(target_os = "windows")]
fn resolve_by_platform_adapter(devices: &[Device], requested: &str) -> Option<Device> {
    let requested = normalized_name(requested);
    let adapters = ipconfig::get_adapters().ok()?;

    for adapter in adapters {
        let friendly_name = adapter.friendly_name();
        let adapter_name = adapter.adapter_name();
        let requested_matches_adapter = normalized_name(friendly_name) == requested
            || normalized_name(adapter_name) == requested;

        if !requested_matches_adapter {
            continue;
        }

        let adapter_token = adapter_name.trim_matches(|c| c == '{' || c == '}');
        if let Some(device) = devices.iter().find(|device| {
            device.name.contains(adapter_name)
                || device.name.contains(adapter_token)
                || device_matches_label(device, friendly_name)
        }) {
            return Some(device.clone());
        }

        let ips = adapter
            .ip_addresses()
            .iter()
            .copied()
            .collect::<HashSet<IpAddr>>();
        if let Some(device) = device_by_addresses(devices, &ips) {
            return Some(device);
        }
    }

    None
}

#[cfg(not(target_os = "windows"))]
fn resolve_by_platform_adapter(_devices: &[Device], _requested: &str) -> Option<Device> {
    None
}

fn normalized_name(value: &str) -> String {
    value.trim().to_ascii_lowercase()
}

fn device_display_name(device: &Device) -> String {
    match device
        .desc
        .as_deref()
        .filter(|desc| !desc.trim().is_empty())
    {
        Some(desc) => format!("{desc} ({})", device.name),
        None => device.name.clone(),
    }
}