bloop-protocol 1.0.0

Core implementation of the Bloop wire protocol
//! Message sets and opcode dispatch.
//!
//! A [`MessageSet`] is a group of messages sharing one wire direction,
//! dispatched by opcode. The standard protocol sets are
//! [`ClientMessage`](crate::message::ClientMessage) and
//! [`ServerMessage`](crate::message::ServerMessage); protocol extensions
//! derive their own sets and plug them into the standard ones through the
//! `Ext` type parameter.
//!
//! Extension opcodes must be `0x80` or above. The range below is reserved
//! for the standard protocol, and standard messages always win dispatch; the
//! `MessageSet` derive rejects reserved opcodes at compile time.

use thiserror::Error;

use crate::codec::{Decode, DecodeError, Encode, EncodeError, decode_payload, encode_payload};
use crate::frame::RawMessage;

/// A message payload with a fixed opcode.
pub trait Payload {
    /// The opcode identifying this message on the wire.
    const OPCODE: u8;
}

/// Errors that can occur while decoding a message from a raw frame.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum MessageSetError {
    /// No message in the set matches the opcode.
    ///
    /// This is recoverable: dispatch can fall through to another set, and a
    /// server answers it with an `UnexpectedMessage` error rather than
    /// treating the connection as broken.
    #[error("unknown opcode 0x{0:02x}")]
    UnknownOpcode(u8),

    /// The opcode matched a message, but its payload does not decode.
    #[error("malformed payload for opcode 0x{opcode:02x}")]
    Malformed {
        opcode: u8,
        #[source]
        source: DecodeError,
    },
}

/// A set of messages sharing one wire direction, dispatched by opcode.
pub trait MessageSet: Sized {
    /// Returns the opcode of the contained message.
    fn opcode(&self) -> u8;

    /// Encodes the message into a raw frame.
    ///
    /// # Errors
    ///
    /// Returns an [`EncodeError`] if a length or count in the payload does
    /// not fit its wire prefix.
    fn encode(self) -> Result<RawMessage, EncodeError>;

    /// Decodes a message from a raw frame.
    ///
    /// # Errors
    ///
    /// Returns [`MessageSetError::UnknownOpcode`] if no message in the set
    /// matches, or [`MessageSetError::Malformed`] if the payload does not
    /// decode (including trailing bytes).
    fn decode(message: &RawMessage) -> Result<Self, MessageSetError>;
}

/// Encodes a single payload into a raw frame carrying its opcode.
///
/// # Errors
///
/// Returns an [`EncodeError`] if a length or count in the payload does not
/// fit its wire prefix.
pub fn encode_message<M>(message: &M) -> Result<RawMessage, EncodeError>
where
    M: Payload + Encode,
{
    Ok(RawMessage::new(M::OPCODE, encode_payload(message)?))
}

/// Decodes a single payload from a raw frame, verifying its opcode.
///
/// # Errors
///
/// Returns [`MessageSetError::UnknownOpcode`] if the frame carries a
/// different opcode, or [`MessageSetError::Malformed`] if the payload does
/// not decode (including trailing bytes).
pub fn decode_message<M>(message: &RawMessage) -> Result<M, MessageSetError>
where
    M: Payload + Decode,
{
    if message.message_type != M::OPCODE {
        return Err(MessageSetError::UnknownOpcode(message.message_type));
    }

    decode_payload(&message.payload).map_err(|source| MessageSetError::Malformed {
        opcode: message.message_type,
        source,
    })
}

/// An uninhabited message set for endpoints without protocol extensions.
///
/// Used as the default `Ext` parameter of the standard message sets: it can
/// never be constructed, and decoding always reports an unknown opcode.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum NoExtension {}

impl MessageSet for NoExtension {
    fn opcode(&self) -> u8 {
        match *self {}
    }

    fn encode(self) -> Result<RawMessage, EncodeError> {
        match self {}
    }

    fn decode(message: &RawMessage) -> Result<Self, MessageSetError> {
        Err(MessageSetError::UnknownOpcode(message.message_type))
    }
}

/// The catch-all set: passes any frame through undecoded.
///
/// Useful where an endpoint wants to observe or forward messages without
/// interpreting them.
impl MessageSet for RawMessage {
    fn opcode(&self) -> u8 {
        self.message_type
    }

    fn encode(self) -> Result<RawMessage, EncodeError> {
        Ok(self)
    }

    fn decode(message: &RawMessage) -> Result<Self, MessageSetError> {
        Ok(message.clone())
    }
}

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

    #[test]
    fn no_extension_never_decodes() {
        let message = RawMessage::new(0x80, vec![]);
        let error = NoExtension::decode(&message).unwrap_err();

        assert!(matches!(error, MessageSetError::UnknownOpcode(0x80)));
    }

    #[test]
    fn raw_message_passes_through() {
        let message = RawMessage::new(0xab, vec![9, 8, 7]);

        let decoded = RawMessage::decode(&message).unwrap();
        assert_eq!(decoded, message);
        assert_eq!(decoded.opcode(), 0xab);

        let encoded = decoded.encode().unwrap();
        assert_eq!(encoded, message);
    }
}