vize_l0 0.440.0

L0 - the Davinci foundation level: ids, side tables, keys, dumps, diagnostics, passes and facts
Documentation
//! Capability negotiation: the `handshake` interface's host half.
//!
//! The rules, in the order they are checked (the first failure is the
//! refusal, and its message is exact):
//!
//! 1. the protocol version equals [`PROTOCOL_VERSION`];
//! 2. the feature list is sorted by byte order and has no duplicates, so an
//!    offer has one spelling and negotiation is deterministic;
//! 3. every feature the world requires ([`REQUIRED_FEATURES`]) is offered.
//!
//! Features this host does not know are accepted and set aside as
//! [`Negotiated::ignored`]: offering more is additive, never a refusal.

use core::fmt;

use crate::String;

use super::wire::{Capability, LANG_FEATURE_PREFIX, PROTOCOL_VERSION, REQUIRED_FEATURES};

/// Why a capability offer was refused.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HandshakeError {
    /// The guest speaks a protocol version this host does not.
    ProtocolMismatch { host: u32, guest: u32 },
    /// `features[index]` does not sort strictly after its predecessor.
    UnsortedFeatures {
        index: usize,
        feature: String,
        previous: String,
    },
    /// A feature the world requires is absent.
    MissingFeature(&'static str),
}

impl fmt::Display for HandshakeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ProtocolMismatch { host, guest } => write!(
                f,
                "protocol version mismatch: the host speaks {host}, the guest offered {guest}"
            ),
            Self::UnsortedFeatures {
                index,
                feature,
                previous,
            } => write!(
                f,
                "features must be sorted and unique: feature {index} {feature:?} does not sort after {previous:?}"
            ),
            Self::MissingFeature(feature) => {
                write!(f, "the guest does not offer required feature {feature:?}")
            }
        }
    }
}

/// A negotiated capability.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Negotiated {
    /// The protocol version both sides speak.
    pub protocol_version: u32,
    /// The `lang` values the guest declared through `lang:<value>` features.
    pub langs: Vec<String>,
    /// Offered features this host does not know, in offer order.
    pub ignored: Vec<String>,
}

impl Negotiated {
    /// Whether the guest declared `lang:<lang>`.
    #[must_use]
    pub fn lowers_lang(&self, lang: &str) -> bool {
        self.langs.iter().any(|declared| declared == lang)
    }
}

/// Negotiate a guest's offer against this host.
///
/// # Errors
///
/// The first rule the offer breaks, as a [`HandshakeError`].
pub fn negotiate(offer: &Capability) -> Result<Negotiated, HandshakeError> {
    negotiate_for(offer, REQUIRED_FEATURES)
}

/// Negotiate an offer for a world that requires `required` (sorted).
///
/// # Errors
///
/// The first rule the offer breaks, as a [`HandshakeError`].
pub fn negotiate_for(
    offer: &Capability,
    required: &'static [&'static str],
) -> Result<Negotiated, HandshakeError> {
    if offer.protocol_version != PROTOCOL_VERSION {
        return Err(HandshakeError::ProtocolMismatch {
            host: PROTOCOL_VERSION,
            guest: offer.protocol_version,
        });
    }
    for (index, pair) in offer.features.windows(2).enumerate() {
        if let [previous, feature] = pair
            && previous.as_bytes() >= feature.as_bytes()
        {
            return Err(HandshakeError::UnsortedFeatures {
                index: index + 1,
                feature: feature.clone(),
                previous: previous.clone(),
            });
        }
    }
    for feature in required {
        if !offer.features.iter().any(|offered| offered == feature) {
            return Err(HandshakeError::MissingFeature(feature));
        }
    }
    let mut langs = Vec::new();
    let mut ignored = Vec::new();
    for feature in &offer.features {
        if let Some(lang) = feature.strip_prefix(LANG_FEATURE_PREFIX) {
            langs.push(String::from(lang));
        } else if !required.contains(&feature.as_str()) {
            ignored.push(feature.clone());
        }
    }
    Ok(Negotiated {
        protocol_version: PROTOCOL_VERSION,
        langs,
        ignored,
    })
}