Skip to main content

snapcast_proto/message/
hello.rs

1//! Hello message (type=5).
2//!
3//! Sent by the client when connecting to the server. Contains client
4//! identification and optional authentication info as a JSON payload.
5
6use std::io::{Read, Write};
7
8use serde::{Deserialize, Serialize};
9
10use crate::message::base::ProtoError;
11use crate::message::wire;
12
13/// Authentication info (optional).
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct Auth {
16    /// Authentication scheme (e.g. "Basic").
17    pub scheme: String,
18    /// Scheme-specific parameter (e.g. base64 credentials).
19    pub param: String,
20}
21
22/// Hello message JSON payload.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "PascalCase")]
25pub struct Hello {
26    /// Client MAC address.
27    #[serde(rename = "MAC")]
28    pub mac: String,
29    /// Client hostname.
30    pub host_name: String,
31    /// Client software version.
32    pub version: String,
33    /// Client application name.
34    pub client_name: String,
35    /// Client operating system.
36    #[serde(rename = "OS")]
37    pub os: String,
38    /// Client CPU architecture.
39    pub arch: String,
40    /// Client instance number.
41    pub instance: u32,
42    /// Unique client identifier.
43    #[serde(rename = "ID")]
44    pub id: String,
45    /// Protocol version supported by the client.
46    pub snap_stream_protocol_version: u32,
47    /// Optional authentication info.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub auth: Option<Auth>,
50}
51
52impl Hello {
53    /// Wire size: u32 length prefix + JSON bytes.
54    pub fn wire_size(&self) -> u32 {
55        let json = serde_json::to_string(self).unwrap_or_default();
56        wire::string_wire_size(&json)
57    }
58
59    /// Deserialize a Hello message from a reader.
60    pub fn read_from<R: Read>(r: &mut R) -> Result<Self, ProtoError> {
61        let json_str = wire::read_string(r)?;
62        serde_json::from_str(&json_str)
63            .map_err(|e| ProtoError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
64    }
65
66    /// Serialize a Hello message to a writer.
67    pub fn write_to<W: Write>(&self, w: &mut W) -> Result<(), ProtoError> {
68        let json_str = serde_json::to_string(self)
69            .map_err(|e| ProtoError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
70        wire::write_string(w, &json_str)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn sample_hello() -> Hello {
79        Hello {
80            mac: "00:11:22:33:44:55".into(),
81            host_name: "testhost".into(),
82            version: "0.32.0".into(),
83            client_name: "Snapclient".into(),
84            os: "Linux".into(),
85            arch: "x86_64".into(),
86            instance: 1,
87            id: "00:11:22:33:44:55".into(),
88            snap_stream_protocol_version: 2,
89            auth: None,
90        }
91    }
92
93    #[test]
94    fn round_trip() {
95        let original = sample_hello();
96        let mut buf = Vec::new();
97        original.write_to(&mut buf).unwrap();
98        let mut cursor = std::io::Cursor::new(&buf);
99        let decoded = Hello::read_from(&mut cursor).unwrap();
100        assert_eq!(original, decoded);
101    }
102
103    #[test]
104    fn round_trip_with_auth() {
105        let mut hello = sample_hello();
106        hello.auth = Some(Auth {
107            scheme: "Basic".into(),
108            param: "dXNlcjpwYXNz".into(),
109        });
110        let mut buf = Vec::new();
111        hello.write_to(&mut buf).unwrap();
112        let mut cursor = std::io::Cursor::new(&buf);
113        let decoded = Hello::read_from(&mut cursor).unwrap();
114        assert_eq!(hello, decoded);
115        assert_eq!(decoded.auth.unwrap().scheme, "Basic");
116    }
117
118    #[test]
119    fn json_field_names_match_cpp() {
120        let hello = sample_hello();
121        let json_str = serde_json::to_string(&hello).unwrap();
122        // Verify C++ field names are used
123        assert!(json_str.contains("\"MAC\""));
124        assert!(json_str.contains("\"HostName\""));
125        assert!(json_str.contains("\"SnapStreamProtocolVersion\""));
126        assert!(json_str.contains("\"ID\""));
127        assert!(json_str.contains("\"OS\""));
128        // Auth should be absent when None
129        assert!(!json_str.contains("\"Auth\""));
130    }
131
132    #[test]
133    fn deserialize_cpp_json() {
134        // JSON as the C++ server would produce
135        let json = r#"{"Arch":"x86_64","ClientName":"Snapclient","HostName":"myhost","ID":"aa:bb:cc:dd:ee:ff","Instance":1,"MAC":"aa:bb:cc:dd:ee:ff","OS":"Arch Linux","SnapStreamProtocolVersion":2,"Version":"0.32.0"}"#;
136        let hello: Hello = serde_json::from_str(json).unwrap();
137        assert_eq!(hello.mac, "aa:bb:cc:dd:ee:ff");
138        assert_eq!(hello.snap_stream_protocol_version, 2);
139        assert!(hello.auth.is_none());
140    }
141}