nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Resolve domain **`tcp://`** endpoints before ZeroMQ connect (no libc **`getaddrinfo`**).

use crate::zeromq::{Socket, ZmqError};

use super::endpoint::{host_is_literal, parse_tcp_host_port, resolve_tcp_connect};
use super::error::DnsError;

#[derive(Debug, thiserror::Error)]
pub enum ZmqConnectError {
    #[error(transparent)]
    Dns(#[from] DnsError),
    #[error(transparent)]
    Zmq(#[from] ZmqError),
}

async fn resolve_endpoint(endpoint: &str) -> Result<String, DnsError> {
    if endpoint.starts_with("tcp://") {
        let (host, _) = parse_tcp_host_port(endpoint)?;
        if host_is_literal(&host) {
            return Ok(endpoint.to_string());
        }
        return resolve_tcp_connect(endpoint).await;
    }
    Ok(endpoint.to_string())
}

/// Connect after resolving domain names in **`tcp://host:port`** via Hickory.
pub async fn connect_resolved<S: Socket>(
    socket: &mut S,
    endpoint: &str,
) -> Result<(), ZmqConnectError> {
    let resolved = resolve_endpoint(endpoint).await?;
    socket.connect(&resolved).await?;
    Ok(())
}

/// Try each resolved **`tcp://`** address until one connects (A/AAAA failover).
pub async fn connect_resolved_first<S: Socket>(
    socket: &mut S,
    endpoint: &str,
) -> Result<String, ZmqConnectError> {
    let candidates = if endpoint.starts_with("tcp://") {
        let (host, _) = parse_tcp_host_port(endpoint).map_err(DnsError::Tcp)?;
        if host_is_literal(&host) {
            vec![endpoint.to_string()]
        } else {
            super::resolve_tcp_connect_all(endpoint).await?
        }
    } else {
        vec![endpoint.to_string()]
    };

    if candidates.is_empty() {
        return Err(DnsError::Tcp(super::error::TcpEndpointError::NoAddresses).into());
    }

    let mut last_err = None;
    for candidate in candidates {
        match socket.connect(&candidate).await {
            Ok(()) => return Ok(candidate),
            Err(e) => last_err = Some(e),
        }
    }
    Err(last_err.expect("non-empty candidates").into())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn resolve_endpoint_leaves_literal_unchanged() {
        let out = resolve_endpoint("tcp://10.0.0.5:5555").await.unwrap();
        assert_eq!(out, "tcp://10.0.0.5:5555");
    }

    #[tokio::test]
    async fn resolve_endpoint_passes_ipc_through() {
        let out = resolve_endpoint("ipc:///run/nautalid/bus.sock")
            .await
            .unwrap();
        assert_eq!(out, "ipc:///run/nautalid/bus.sock");
    }
}