pimconf 0.1.0

CLI and lib to discover PIM-related services
//! # Combined RFC 6764 SRV discovery coroutine
//!
//! [`DiscoveryRfc6764`] runs the four RFC 6764 ยง3 SRV queries
//! (`_caldav._tcp.<domain>`, `_caldavs._tcp.<domain>`,
//! `_carddav._tcp.<domain>`, `_carddavs._tcp.<domain>`) in series,
//! picks the best record per service (lowest priority, highest weight
//! on ties; already sorted by [`DiscoveryDnsSrv`]), and yields a
//! single [`Rfc6764Report`] when all four steps have completed.
//!
//! A per-service DNS failure (`InvalidQname`, `QueryTooLarge`,
//! `InvalidResponse`) terminates the whole coroutine with
//! [`DiscoveryRfc6764Error`]; empty SRV answers do not, the matching
//! slot is simply left as `None` in the report.

use core::mem;

use alloc::{
    format,
    string::{String, ToString},
};

use domain::new::{
    base::{
        Record,
        name::{NameBuf, RevNameBuf},
    },
    rdata::Srv,
};
use thiserror::Error;
use url::Url;

use crate::{
    coroutine::{DiscoveryCoroutine, DiscoveryCoroutineState, DiscoveryYield},
    rfc6186::types::SrvService,
    rfc6764::{
        srv::{DiscoveryDnsSrv, DiscoveryDnsSrvError},
        types::Rfc6764Report,
    },
};

/// Errors emitted by [`DiscoveryRfc6764`].
#[derive(Debug, Error)]
pub enum DiscoveryRfc6764Error {
    #[error("DNS SRV lookup for `_caldav._tcp` failed: {0}")]
    Caldav(#[source] DiscoveryDnsSrvError),
    #[error("DNS SRV lookup for `_caldavs._tcp` failed: {0}")]
    Caldavs(#[source] DiscoveryDnsSrvError),
    #[error("DNS SRV lookup for `_carddav._tcp` failed: {0}")]
    Carddav(#[source] DiscoveryDnsSrvError),
    #[error("DNS SRV lookup for `_carddavs._tcp` failed: {0}")]
    Carddavs(#[source] DiscoveryDnsSrvError),
}

#[derive(Default)]
enum State {
    Caldav(DiscoveryDnsSrv),
    Caldavs(DiscoveryDnsSrv),
    Carddav(DiscoveryDnsSrv),
    Carddavs(DiscoveryDnsSrv),
    #[default]
    Done,
}

/// I/O-free combined coroutine that runs the four RFC 6764 SRV
/// queries and assembles their best records into a [`Rfc6764Report`].
pub struct DiscoveryRfc6764 {
    state: State,
    domain: String,
    resolver: Url,
    report: Rfc6764Report,
}

impl DiscoveryRfc6764 {
    /// Builds the orchestrator. `resolver` must be a `tcp://host:port`
    /// URL pointing at a DNS-over-TCP resolver; it is yielded back on
    /// every `WantsRead` / `WantsWrite` so the runtime can route the
    /// bytes to the correct stream.
    pub fn new(domain: impl AsRef<str>, resolver: Url) -> Self {
        let domain = domain.as_ref().trim_matches('.').to_string();
        let caldav = DiscoveryDnsSrv::new(format!("_caldav._tcp.{domain}"), resolver.clone());

        Self {
            state: State::Caldav(caldav),
            domain,
            resolver,
            report: Rfc6764Report::default(),
        }
    }
}

impl DiscoveryCoroutine for DiscoveryRfc6764 {
    type Yield = DiscoveryYield;
    type Return = Result<Rfc6764Report, DiscoveryRfc6764Error>;

    fn resume(&mut self, arg: Option<&[u8]>) -> DiscoveryCoroutineState<Self::Yield, Self::Return> {
        match mem::take(&mut self.state) {
            State::Caldav(mut srv) => match srv.resume(arg) {
                DiscoveryCoroutineState::Complete(Ok(records)) => {
                    self.report.caldav = records.into_iter().next().map(into_service);
                    self.state = State::Caldavs(DiscoveryDnsSrv::new(
                        format!("_caldavs._tcp.{}", self.domain),
                        self.resolver.clone(),
                    ));
                    self.resume(None)
                }
                DiscoveryCoroutineState::Yielded(y) => {
                    self.state = State::Caldav(srv);
                    DiscoveryCoroutineState::Yielded(y)
                }
                DiscoveryCoroutineState::Complete(Err(err)) => {
                    DiscoveryCoroutineState::Complete(Err(DiscoveryRfc6764Error::Caldav(err)))
                }
            },
            State::Caldavs(mut srv) => match srv.resume(arg) {
                DiscoveryCoroutineState::Complete(Ok(records)) => {
                    self.report.caldavs = records.into_iter().next().map(into_service);
                    self.state = State::Carddav(DiscoveryDnsSrv::new(
                        format!("_carddav._tcp.{}", self.domain),
                        self.resolver.clone(),
                    ));
                    self.resume(None)
                }
                DiscoveryCoroutineState::Yielded(y) => {
                    self.state = State::Caldavs(srv);
                    DiscoveryCoroutineState::Yielded(y)
                }
                DiscoveryCoroutineState::Complete(Err(err)) => {
                    DiscoveryCoroutineState::Complete(Err(DiscoveryRfc6764Error::Caldavs(err)))
                }
            },
            State::Carddav(mut srv) => match srv.resume(arg) {
                DiscoveryCoroutineState::Complete(Ok(records)) => {
                    self.report.carddav = records.into_iter().next().map(into_service);
                    self.state = State::Carddavs(DiscoveryDnsSrv::new(
                        format!("_carddavs._tcp.{}", self.domain),
                        self.resolver.clone(),
                    ));
                    self.resume(None)
                }
                DiscoveryCoroutineState::Yielded(y) => {
                    self.state = State::Carddav(srv);
                    DiscoveryCoroutineState::Yielded(y)
                }
                DiscoveryCoroutineState::Complete(Err(err)) => {
                    DiscoveryCoroutineState::Complete(Err(DiscoveryRfc6764Error::Carddav(err)))
                }
            },
            State::Carddavs(mut srv) => match srv.resume(arg) {
                DiscoveryCoroutineState::Complete(Ok(records)) => {
                    self.report.carddavs = records.into_iter().next().map(into_service);
                    DiscoveryCoroutineState::Complete(Ok(mem::take(&mut self.report)))
                }
                DiscoveryCoroutineState::Yielded(y) => {
                    self.state = State::Carddavs(srv);
                    DiscoveryCoroutineState::Yielded(y)
                }
                DiscoveryCoroutineState::Complete(Err(err)) => {
                    DiscoveryCoroutineState::Complete(Err(DiscoveryRfc6764Error::Carddavs(err)))
                }
            },
            State::Done => panic!("DiscoveryRfc6764::resume called after completion"),
        }
    }
}

fn into_service(record: Record<RevNameBuf, Srv<NameBuf>>) -> SrvService {
    SrvService {
        host: record
            .rdata
            .target
            .to_string()
            .trim_end_matches('.')
            .to_string(),
        port: record.rdata.port.get(),
        priority: record.rdata.priority.get(),
        weight: record.rdata.weight.get(),
    }
}