use core::fmt;
use crate::String;
use super::wire::{Capability, LANG_FEATURE_PREFIX, PROTOCOL_VERSION, REQUIRED_FEATURES};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HandshakeError {
ProtocolMismatch { host: u32, guest: u32 },
UnsortedFeatures {
index: usize,
feature: String,
previous: String,
},
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:?}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Negotiated {
pub protocol_version: u32,
pub langs: Vec<String>,
pub ignored: Vec<String>,
}
impl Negotiated {
#[must_use]
pub fn lowers_lang(&self, lang: &str) -> bool {
self.langs.iter().any(|declared| declared == lang)
}
}
pub fn negotiate(offer: &Capability) -> Result<Negotiated, HandshakeError> {
negotiate_for(offer, REQUIRED_FEATURES)
}
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,
})
}