Skip to main content

snapcast_proto/message/
base.rs

1//! Base message header — the first 26 bytes of every Snapcast protocol message.
2
3use std::io::{self, Read, Write};
4
5use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
6use thiserror::Error;
7
8use super::MessageType;
9use crate::types::Timeval;
10
11/// Errors that can occur during message parsing.
12#[derive(Debug, Error)]
13pub enum ProtoError {
14    /// An I/O error occurred during read or write.
15    #[error("I/O error: {0}")]
16    Io(#[from] io::Error),
17    /// Received an unrecognized message type value.
18    #[error("unknown message type: {0}")]
19    UnknownMessageType(u16),
20}
21
22/// Base message header (26 bytes).
23///
24/// Every Snapcast protocol message starts with this header, followed by
25/// `size` bytes of typed payload.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct BaseMessage {
28    /// Type of the message payload.
29    pub msg_type: MessageType,
30    /// Sequence identifier.
31    pub id: u16,
32    /// ID of the message this is a response to.
33    pub refers_to: u16,
34    /// Timestamp when the message was sent.
35    pub sent: Timeval,
36    /// Timestamp when the message was received.
37    pub received: Timeval,
38    /// Size of the payload in bytes.
39    pub size: u32,
40}
41
42impl BaseMessage {
43    /// Size of the serialized header in bytes.
44    pub const HEADER_SIZE: usize = 26;
45
46    /// Deserialize a base message header from a reader.
47    pub fn read_from<R: Read>(r: &mut R) -> Result<Self, ProtoError> {
48        let raw_type = r.read_u16::<LittleEndian>()?;
49        let msg_type =
50            MessageType::from_u16(raw_type).ok_or(ProtoError::UnknownMessageType(raw_type))?;
51        let id = r.read_u16::<LittleEndian>()?;
52        let refers_to = r.read_u16::<LittleEndian>()?;
53        let sent = Timeval::read_from(r)?;
54        let received = Timeval::read_from(r)?;
55        let size = r.read_u32::<LittleEndian>()?;
56        Ok(Self {
57            msg_type,
58            id,
59            refers_to,
60            sent,
61            received,
62            size,
63        })
64    }
65
66    /// Serialize a base message header to a writer.
67    pub fn write_to<W: Write>(&self, w: &mut W) -> Result<(), ProtoError> {
68        w.write_u16::<LittleEndian>(self.msg_type.into())?;
69        w.write_u16::<LittleEndian>(self.id)?;
70        w.write_u16::<LittleEndian>(self.refers_to)?;
71        self.sent.write_to(w)?;
72        self.received.write_to(w)?;
73        w.write_u32::<LittleEndian>(self.size)?;
74        Ok(())
75    }
76
77    /// Serialize to a new `Vec<u8>`.
78    pub fn to_bytes(&self) -> Result<Vec<u8>, ProtoError> {
79        let mut buf = Vec::with_capacity(Self::HEADER_SIZE);
80        self.write_to(&mut buf)?;
81        Ok(buf)
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    /// Test vector: a Hello message header as the C++ code would serialize it.
90    ///
91    /// Fields (all little-endian):
92    ///   type=5 (Hello), id=1, refersTo=0,
93    ///   sent.sec=1000, sent.usec=500000,
94    ///   received.sec=0, received.usec=0,
95    ///   size=42
96    const HELLO_HEADER_BYTES: [u8; 26] = [
97        0x05, 0x00, // type = 5 (Hello)
98        0x01, 0x00, // id = 1
99        0x00, 0x00, // refersTo = 0
100        0xE8, 0x03, 0x00, 0x00, // sent.sec = 1000
101        0x20, 0xA1, 0x07, 0x00, // sent.usec = 500000
102        0x00, 0x00, 0x00, 0x00, // received.sec = 0
103        0x00, 0x00, 0x00, 0x00, // received.usec = 0
104        0x2A, 0x00, 0x00, 0x00, // size = 42
105    ];
106
107    fn hello_header() -> BaseMessage {
108        BaseMessage {
109            msg_type: MessageType::Hello,
110            id: 1,
111            refers_to: 0,
112            sent: Timeval {
113                sec: 1000,
114                usec: 500_000,
115            },
116            received: Timeval { sec: 0, usec: 0 },
117            size: 42,
118        }
119    }
120
121    #[test]
122    fn serialize_hello_header() {
123        let msg = hello_header();
124        let bytes = msg.to_bytes().unwrap();
125        assert_eq!(bytes.as_slice(), &HELLO_HEADER_BYTES);
126    }
127
128    #[test]
129    fn deserialize_hello_header() {
130        let mut cursor = io::Cursor::new(&HELLO_HEADER_BYTES);
131        let msg = BaseMessage::read_from(&mut cursor).unwrap();
132        assert_eq!(msg, hello_header());
133    }
134
135    #[test]
136    fn round_trip() {
137        let original = hello_header();
138        let bytes = original.to_bytes().unwrap();
139        let mut cursor = io::Cursor::new(&bytes);
140        let decoded = BaseMessage::read_from(&mut cursor).unwrap();
141        assert_eq!(original, decoded);
142    }
143
144    #[test]
145    fn header_size_is_26_bytes() {
146        let msg = hello_header();
147        let bytes = msg.to_bytes().unwrap();
148        assert_eq!(bytes.len(), BaseMessage::HEADER_SIZE);
149    }
150
151    #[test]
152    fn unknown_type_returns_error() {
153        let mut bad_bytes = HELLO_HEADER_BYTES;
154        bad_bytes[0] = 0xFF;
155        bad_bytes[1] = 0xFF;
156        let mut cursor = io::Cursor::new(&bad_bytes);
157        #[cfg(not(feature = "custom-protocol"))]
158        {
159            let err = BaseMessage::read_from(&mut cursor).unwrap_err();
160            assert!(matches!(err, ProtoError::UnknownMessageType(0xFFFF)));
161        }
162        #[cfg(feature = "custom-protocol")]
163        {
164            let msg = BaseMessage::read_from(&mut cursor).unwrap();
165            assert_eq!(msg.msg_type, MessageType::Custom(0xFFFF));
166        }
167    }
168}