mumble_server_runtime_protocol/
udp.rs1use prost::Message;
23use thiserror::Error;
24
25use crate::messages::udp;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u8)]
31pub enum UdpMessageType {
32 Audio = 0,
33 Ping = 1,
34}
35
36impl From<UdpMessageType> for u8 {
37 fn from(value: UdpMessageType) -> Self {
38 value as u8
39 }
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub enum UdpMessage {
45 Audio(udp::Audio),
46 Ping(udp::Ping),
47}
48
49#[derive(Debug, Error)]
51pub enum UdpDecodeError {
52 #[error("UDP packet too short: {len} byte(s), need a header plus payload")]
55 TooShort { len: usize },
56
57 #[error(
60 "unsupported UDP packet type {0}; only protobuf Audio (0) and Ping (1) \
61 are implemented (legacy is out per ADR-0001)"
62 )]
63 UnsupportedType(u8),
64
65 #[error("protobuf decode failed for UDP {message_type:?} ({payload_len} bytes): {source}")]
67 Protobuf {
68 message_type: UdpMessageType,
69 payload_len: usize,
70 source: prost::DecodeError,
71 },
72}
73
74pub fn decode_udp(packet: &[u8]) -> Result<UdpMessage, UdpDecodeError> {
76 let (&header, payload) = match packet.split_first() {
78 Some(split) if !split.1.is_empty() => split,
79 _ => return Err(UdpDecodeError::TooShort { len: packet.len() }),
80 };
81
82 let message_type = match header {
83 0 => UdpMessageType::Audio,
84 1 => UdpMessageType::Ping,
85 other => return Err(UdpDecodeError::UnsupportedType(other)),
86 };
87
88 Ok(match message_type {
89 UdpMessageType::Audio => UdpMessage::Audio(decode_pb(message_type, payload)?),
90 UdpMessageType::Ping => UdpMessage::Ping(decode_pb(message_type, payload)?),
91 })
92}
93
94pub fn encode_udp(message: &UdpMessage) -> Vec<u8> {
99 let (message_type, body) = match message {
100 UdpMessage::Audio(audio) => (UdpMessageType::Audio, audio.encode_to_vec()),
101 UdpMessage::Ping(ping) => (UdpMessageType::Ping, ping.encode_to_vec()),
102 };
103 let mut packet = Vec::with_capacity(1 + body.len());
105 packet.push(u8::from(message_type));
106 packet.extend_from_slice(&body);
107 packet
108}
109
110fn decode_pb<M: Message + Default>(
112 message_type: UdpMessageType,
113 payload: &[u8],
114) -> Result<M, UdpDecodeError> {
115 M::decode(payload).map_err(|source| UdpDecodeError::Protobuf {
116 message_type,
117 payload_len: payload.len(),
118 source,
119 })
120}
121
122#[cfg(test)]
123mod tests {
124 #![allow(clippy::expect_used)]
126
127 use super::*;
128
129 fn envelope(message_type: UdpMessageType, body: &[u8]) -> Vec<u8> {
130 let mut packet = vec![u8::from(message_type)];
131 packet.extend_from_slice(body);
132 packet
133 }
134
135 #[test]
136 fn decodes_audio_envelope_and_keeps_opus_raw() {
137 let audio = udp::Audio {
138 header: Some(udp::audio::Header::Target(0)),
139 sender_session: 5,
140 frame_number: 100,
141 opus_data: vec![0xAA, 0xBB, 0xCC], positional_data: vec![],
143 volume_adjustment: 0.0,
144 is_terminator: false,
145 };
146 let packet = envelope(UdpMessageType::Audio, &audio.encode_to_vec());
147 match decode_udp(&packet).expect("decode audio") {
148 UdpMessage::Audio(decoded) => {
149 assert_eq!(decoded, audio);
150 assert_eq!(decoded.opus_data, vec![0xAA, 0xBB, 0xCC]);
152 }
153 other => panic!("expected Audio, got {other:?}"),
154 }
155 }
156
157 #[test]
158 fn decodes_ping_envelope() {
159 let ping = udp::Ping {
160 timestamp: 987_654,
161 ..Default::default()
162 };
163 let packet = envelope(UdpMessageType::Ping, &ping.encode_to_vec());
164 match decode_udp(&packet).expect("decode ping") {
165 UdpMessage::Ping(decoded) => assert_eq!(decoded, ping),
166 other => panic!("expected Ping, got {other:?}"),
167 }
168 }
169
170 #[test]
171 fn encode_then_decode_roundtrips_audio_and_ping() {
172 let audio = UdpMessage::Audio(udp::Audio {
173 header: Some(udp::audio::Header::Context(0)),
174 sender_session: 7,
175 frame_number: 690,
176 opus_data: vec![0xD8, 0xED, 0x5C],
177 positional_data: vec![],
178 volume_adjustment: 0.0,
179 is_terminator: false,
180 });
181 assert_eq!(
182 decode_udp(&encode_udp(&audio)).expect("re-decode audio"),
183 audio
184 );
185
186 let ping = UdpMessage::Ping(udp::Ping {
187 timestamp: 68_900,
188 ..Default::default()
189 });
190 assert_eq!(
191 decode_udp(&encode_udp(&ping)).expect("re-decode ping"),
192 ping
193 );
194 }
195
196 #[test]
197 fn encoded_header_byte_matches_message_type() {
198 let ping = UdpMessage::Ping(udp::Ping {
199 timestamp: 1,
200 ..Default::default()
201 });
202 assert_eq!(encode_udp(&ping)[0], u8::from(UdpMessageType::Ping));
203 }
204
205 #[test]
206 fn empty_and_header_only_packets_are_too_short() {
207 match decode_udp(&[]) {
208 Err(UdpDecodeError::TooShort { len: 0 }) => {}
209 other => panic!("expected TooShort(0), got {other:?}"),
210 }
211 match decode_udp(&[0x00]) {
213 Err(UdpDecodeError::TooShort { len: 1 }) => {}
214 other => panic!("expected TooShort(1), got {other:?}"),
215 }
216 }
217
218 #[test]
219 fn legacy_or_unknown_header_fails_closed() {
220 match decode_udp(&[0x80, 0x01, 0x02]) {
223 Err(UdpDecodeError::UnsupportedType(0x80)) => {}
224 other => panic!("expected UnsupportedType(0x80), got {other:?}"),
225 }
226 match decode_udp(&[0x05, 0xFF]) {
227 Err(UdpDecodeError::UnsupportedType(5)) => {}
228 other => panic!("expected UnsupportedType(5), got {other:?}"),
229 }
230 }
231
232 #[test]
233 fn malformed_audio_payload_is_an_error() {
234 match decode_udp(&[0x00, 0x08]) {
236 Err(UdpDecodeError::Protobuf {
237 message_type: UdpMessageType::Audio,
238 ..
239 }) => {}
240 other => panic!("expected protobuf error, got {other:?}"),
241 }
242 }
243}