snapcast-proto 0.5.0

Snapcast binary protocol implementation
Documentation
//! Protocol message types.

pub mod base;
pub mod client_info;
pub mod codec_header;
pub mod error;
pub mod factory;
pub mod hello;
pub mod server_settings;
pub mod time;
pub mod wire;
pub mod wire_chunk;

/// Message type identifiers matching the C++ `message_type` enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageType {
    /// Base message (type 0), header only.
    Base,
    /// Codec header (type 1).
    CodecHeader,
    /// Encoded audio chunk (type 2).
    WireChunk,
    /// Server settings (type 3).
    ServerSettings,
    /// Time sync (type 4).
    Time,
    /// Client hello (type 5).
    Hello,
    // 6 is unused (was StreamTags)
    /// Client info (type 7).
    ClientInfo,
    /// Error (type 8).
    Error,
    /// Application-defined message (type 9+).
    #[cfg(feature = "custom-protocol")]
    Custom(u16),
}

impl MessageType {
    /// Parse from a raw `u16` value.
    pub fn from_u16(value: u16) -> Option<Self> {
        match value {
            0 => Some(Self::Base),
            1 => Some(Self::CodecHeader),
            2 => Some(Self::WireChunk),
            3 => Some(Self::ServerSettings),
            4 => Some(Self::Time),
            5 => Some(Self::Hello),
            7 => Some(Self::ClientInfo),
            8 => Some(Self::Error),
            #[cfg(feature = "custom-protocol")]
            9.. => Some(Self::Custom(value)),
            _ => None,
        }
    }
}

impl From<MessageType> for u16 {
    fn from(mt: MessageType) -> Self {
        match mt {
            MessageType::Base => 0,
            MessageType::CodecHeader => 1,
            MessageType::WireChunk => 2,
            MessageType::ServerSettings => 3,
            MessageType::Time => 4,
            MessageType::Hello => 5,
            MessageType::ClientInfo => 7,
            MessageType::Error => 8,
            #[cfg(feature = "custom-protocol")]
            MessageType::Custom(id) => id,
        }
    }
}

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

    #[test]
    fn round_trip_all_message_types() {
        let types = [
            MessageType::Base,
            MessageType::CodecHeader,
            MessageType::WireChunk,
            MessageType::ServerSettings,
            MessageType::Time,
            MessageType::Hello,
            MessageType::ClientInfo,
            MessageType::Error,
        ];
        for mt in types {
            let raw: u16 = mt.into();
            assert_eq!(MessageType::from_u16(raw), Some(mt));
        }
    }

    #[test]
    fn unknown_message_type_returns_none() {
        assert_eq!(MessageType::from_u16(6), None);
        #[cfg(not(feature = "custom-protocol"))]
        {
            assert_eq!(MessageType::from_u16(9), None);
            assert_eq!(MessageType::from_u16(u16::MAX), None);
        }
        #[cfg(feature = "custom-protocol")]
        {
            assert_eq!(MessageType::from_u16(9), Some(MessageType::Custom(9)));
            assert_eq!(
                MessageType::from_u16(u16::MAX),
                Some(MessageType::Custom(u16::MAX))
            );
        }
    }
}

/// Custom message for application-defined protocol extensions (type 9+).
#[cfg(feature = "custom-protocol")]
#[derive(Debug, Clone)]
pub struct CustomMessage {
    /// Message type ID (9+).
    pub type_id: u16,
    /// Raw payload bytes.
    pub payload: Vec<u8>,
}

#[cfg(feature = "custom-protocol")]
impl CustomMessage {
    /// Create a new custom message.
    pub fn new(type_id: u16, payload: impl Into<Vec<u8>>) -> Self {
        Self {
            type_id,
            payload: payload.into(),
        }
    }
}