prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::interface_discovery::{
    AdvertisedInterfaceType, AdvertisementDetails, DiscoveredInterface, DiscoveredInterfaceId,
};

use super::file::encode_hex;

pub fn manual_configuration(interface: &DiscoveredInterface) -> Option<String> {
    let name = manual_interface_name(&interface.name, interface.id);
    let transport_identity =
        encode_hex(interface.advertisement.transport.transport_id().as_bytes());
    let mut lines = Vec::new();
    lines.push(format!("[[{name}]]"));
    match (
        interface.advertisement.interface_type,
        &interface.advertisement.details,
    ) {
        (AdvertisedInterfaceType::Backbone, AdvertisementDetails::Reachable { host, port }) => {
            lines.push(String::from("  type = BackboneClientInterface"));
            lines.push(format!("  target_host = {}", config_scalar(host)?));
            lines.push(format!("  target_port = {port}"));
        }
        (AdvertisedInterfaceType::TcpServer, AdvertisementDetails::Reachable { host, port }) => {
            lines.push(String::from("  type = TCPClientInterface"));
            lines.push(format!("  target_host = {}", config_scalar(host)?));
            lines.push(format!("  target_port = {port}"));
        }
        (AdvertisedInterfaceType::I2p, AdvertisementDetails::I2p { address }) => {
            lines.push(String::from("  type = I2PInterface"));
            lines.push(format!("  peers = {}", config_scalar(address)?));
        }
        (
            AdvertisedInterfaceType::RNode,
            AdvertisementDetails::RNode {
                frequency_hz,
                bandwidth_hz,
                spreading_factor,
                coding_rate,
            },
        ) => {
            lines.push(String::from("  type = RNodeInterface"));
            lines.push(String::from("  port = "));
            lines.push(format!("  frequency = {frequency_hz}"));
            lines.push(format!("  bandwidth = {bandwidth_hz}"));
            lines.push(format!("  spreadingfactor = {spreading_factor}"));
            lines.push(format!("  codingrate = {coding_rate}"));
            lines.push(String::from("  txpower = "));
        }
        (
            AdvertisedInterfaceType::Weave,
            AdvertisementDetails::Weave {
                frequency_hz,
                bandwidth_hz,
                channel,
                modulation,
            },
        ) => {
            lines.push(String::from("  type = WeaveInterface"));
            lines.push(String::from("  port = "));
            lines.push(format!("  frequency = {frequency_hz}"));
            lines.push(format!("  bandwidth = {bandwidth_hz}"));
            lines.push(format!("  channel = {channel}"));
            lines.push(format!("  modulation = {}", config_scalar(modulation)?));
        }
        (
            AdvertisedInterfaceType::Kiss,
            AdvertisementDetails::Kiss {
                frequency_hz,
                bandwidth_hz,
                modulation,
            },
        ) => {
            lines.push(String::from("  type = KISSInterface"));
            lines.push(String::from("  port = "));
            lines.push(format!("  frequency = {frequency_hz}"));
            lines.push(format!("  bandwidth = {bandwidth_hz}"));
            lines.push(format!("  modulation = {}", config_scalar(modulation)?));
        }
        (AdvertisedInterfaceType::TcpClient, AdvertisementDetails::None)
        | (AdvertisedInterfaceType::Backbone, _)
        | (AdvertisedInterfaceType::TcpServer, _)
        | (AdvertisedInterfaceType::TcpClient, _)
        | (AdvertisedInterfaceType::I2p, _)
        | (AdvertisedInterfaceType::RNode, _)
        | (AdvertisedInterfaceType::Weave, _)
        | (AdvertisedInterfaceType::Kiss, _) => return None,
    }
    lines.push(String::from("  enabled = Yes"));
    lines.push(format!("  transport_identity = {transport_identity}"));
    if let Some(ifac) = &interface.advertisement.published_ifac {
        if let Some(network_name) = &ifac.network_name {
            lines.push(format!("  network_name = {}", config_scalar(network_name)?));
        }
        if let Some(passphrase) = &ifac.passphrase {
            lines.push(format!("  passphrase = {}", config_scalar(passphrase)?));
        }
    }
    Some(lines.join("\n"))
}

fn manual_interface_name(name: &str, id: DiscoveredInterfaceId) -> String {
    let mut safe = name
        .chars()
        .map(|character| {
            if character.is_ascii_alphanumeric()
                || matches!(character, ' ' | '-' | '_' | '.' | '(' | ')')
            {
                character
            } else {
                '_'
            }
        })
        .collect::<String>();
    if safe.trim().is_empty() {
        safe = String::from("Discovered Interface");
    }
    let id = encode_hex(id.as_bytes());
    format!("{} ({})", safe.trim(), &id[..12])
}

fn config_scalar(value: &str) -> Option<String> {
    if !value
        .chars()
        .any(|character| matches!(character, '\r' | '\n' | '\''))
    {
        return Some(format!("'{value}'"));
    }
    if !value
        .chars()
        .any(|character| matches!(character, '\r' | '\n' | '"'))
    {
        return Some(format!("\"{value}\""));
    }
    if !value.contains("\"\"\"") {
        return Some(format!("\"\"\"{value}\"\"\""));
    }
    if !value.contains("'''") {
        return Some(format!("'''{value}'''"));
    }
    None
}