1use crate::header::DStarHeader;
13use crate::types::{Callsign, Module, ProtocolKind, StreamId, Suffix};
14use crate::validator::{Diagnostic, DiagnosticSink};
15use crate::voice::VoiceFrame;
16
17use super::consts::{
18 CONNECT_ACK_TAG, CONNECT_NAK_TAG, CONNECT_REPLY_LEN, LINK_LEN, POLL_LEN, UNLINK_LEN, VOICE_LEN,
19 VOICE_MAGIC,
20};
21use super::error::DcsError;
22use super::packet::{ClientPacket, GatewayType, ServerPacket};
23
24pub fn decode_client_to_server(
42 bytes: &[u8],
43 sink: &mut dyn DiagnosticSink,
44) -> Result<ClientPacket, DcsError> {
45 let len = bytes.len();
46 match len {
47 POLL_LEN => Ok(decode_client_poll(bytes)),
48 UNLINK_LEN => decode_client_unlink(bytes),
49 LINK_LEN => decode_client_link(bytes),
50 VOICE_LEN => decode_client_voice(bytes, sink),
51 _ => Err(DcsError::UnknownPacketLength { got: len }),
52 }
53}
54
55pub fn decode_server_to_client(
69 bytes: &[u8],
70 sink: &mut dyn DiagnosticSink,
71) -> Result<ServerPacket, DcsError> {
72 let len = bytes.len();
73 match len {
74 POLL_LEN => Ok(decode_server_poll(bytes)),
75 CONNECT_REPLY_LEN => decode_server_connect_reply(bytes),
76 VOICE_LEN => decode_server_voice(bytes, sink),
77 _ => Err(DcsError::UnknownPacketLength { got: len }),
78 }
79}
80
81fn extract_callsign(src: &[u8]) -> Callsign {
97 let mut buf = [b' '; 8];
98 let take = src.len().min(8);
99 if let Some(dst) = buf.get_mut(..take)
100 && let Some(s) = src.get(..take)
101 {
102 dst.copy_from_slice(s);
103 }
104 for b in &mut buf {
105 if *b == 0 {
106 *b = b' ';
107 }
108 }
109 Callsign::from_wire_bytes(buf)
110}
111
112fn decode_client_link(bytes: &[u8]) -> Result<ClientPacket, DcsError> {
114 let callsign = extract_callsign(bytes.get(..8).unwrap_or(&[]));
115 let client_byte = bytes.get(8).copied().unwrap_or(0);
116 let client_module =
117 Module::try_from_byte(client_byte).map_err(|_| DcsError::InvalidModuleByte {
118 offset: 8,
119 byte: client_byte,
120 })?;
121 let reflector_byte = bytes.get(9).copied().unwrap_or(0);
122 let reflector_module =
123 Module::try_from_byte(reflector_byte).map_err(|_| DcsError::InvalidModuleByte {
124 offset: 9,
125 byte: reflector_byte,
126 })?;
127 let reflector_callsign = extract_callsign(bytes.get(11..19).unwrap_or(&[]));
128 Ok(ClientPacket::Link {
130 callsign,
131 client_module,
132 reflector_module,
133 reflector_callsign,
134 gateway_type: GatewayType::Repeater,
135 })
136}
137
138fn decode_client_unlink(bytes: &[u8]) -> Result<ClientPacket, DcsError> {
140 let callsign = extract_callsign(bytes.get(..8).unwrap_or(&[]));
141 let client_byte = bytes.get(8).copied().unwrap_or(0);
142 let client_module =
143 Module::try_from_byte(client_byte).map_err(|_| DcsError::InvalidModuleByte {
144 offset: 8,
145 byte: client_byte,
146 })?;
147 let marker = bytes.get(9).copied().unwrap_or(0);
148 if marker != b' ' {
149 return Err(DcsError::UnlinkModuleByteInvalid { byte: marker });
150 }
151 let reflector_callsign = extract_callsign(bytes.get(11..19).unwrap_or(&[]));
152 Ok(ClientPacket::Unlink {
153 callsign,
154 client_module,
155 reflector_callsign,
156 })
157}
158
159fn decode_client_poll(bytes: &[u8]) -> ClientPacket {
161 let callsign = extract_callsign(bytes.get(..8).unwrap_or(&[]));
162 let reflector_callsign = extract_callsign(bytes.get(9..17).unwrap_or(&[]));
163 ClientPacket::Poll {
164 callsign,
165 reflector_callsign,
166 }
167}
168
169fn decode_server_poll(bytes: &[u8]) -> ServerPacket {
171 let callsign = extract_callsign(bytes.get(..8).unwrap_or(&[]));
172 let reflector_callsign = extract_callsign(bytes.get(9..17).unwrap_or(&[]));
173 ServerPacket::PollEcho {
174 callsign,
175 reflector_callsign,
176 }
177}
178
179fn decode_server_connect_reply(bytes: &[u8]) -> Result<ServerPacket, DcsError> {
181 let callsign = extract_callsign(bytes.get(..8).unwrap_or(&[]));
182 let module_byte = bytes.get(9).copied().unwrap_or(0);
183 let reflector_module =
184 Module::try_from_byte(module_byte).map_err(|_| DcsError::InvalidModuleByte {
185 offset: 9,
186 byte: module_byte,
187 })?;
188 let mut tag = [0u8; 3];
191 if let Some(src) = bytes.get(10..13) {
192 tag.copy_from_slice(src);
193 }
194 if tag == CONNECT_ACK_TAG {
195 Ok(ServerPacket::ConnectAck {
196 callsign,
197 reflector_module,
198 })
199 } else if tag == CONNECT_NAK_TAG {
200 Ok(ServerPacket::ConnectNak {
201 callsign,
202 reflector_module,
203 })
204 } else {
205 Err(DcsError::UnknownConnectTag { tag })
206 }
207}
208
209fn decode_client_voice(
211 bytes: &[u8],
212 sink: &mut dyn DiagnosticSink,
213) -> Result<ClientPacket, DcsError> {
214 let (header, stream_id, seq, frame, is_end) = parse_voice(bytes, sink)?;
215 Ok(ClientPacket::Voice {
216 header,
217 stream_id,
218 seq,
219 frame,
220 is_end,
221 })
222}
223
224fn decode_server_voice(
226 bytes: &[u8],
227 sink: &mut dyn DiagnosticSink,
228) -> Result<ServerPacket, DcsError> {
229 let (header, stream_id, seq, frame, is_end) = parse_voice(bytes, sink)?;
230 Ok(ServerPacket::Voice {
231 header,
232 stream_id,
233 seq,
234 frame,
235 is_end,
236 })
237}
238
239fn parse_voice(
243 bytes: &[u8],
244 sink: &mut dyn DiagnosticSink,
245) -> Result<(DStarHeader, StreamId, u8, VoiceFrame, bool), DcsError> {
246 let magic = bytes
248 .get(..4)
249 .ok_or(DcsError::UnknownPacketLength { got: bytes.len() })?;
250 if magic != VOICE_MAGIC.as_slice() {
251 let mut got = [0u8; 4];
252 got.copy_from_slice(magic);
253 return Err(DcsError::VoiceMagicMissing { got });
254 }
255
256 let lo = bytes.get(43).copied().unwrap_or(0);
258 let hi = bytes.get(44).copied().unwrap_or(0);
259 let raw = u16::from_le_bytes([lo, hi]);
260 let stream_id = StreamId::new(raw).ok_or(DcsError::StreamIdZero)?;
261
262 let seq_raw = bytes.get(45).copied().unwrap_or(0);
267 let eot_bit = (seq_raw & 0x40) != 0;
268 let seq = seq_raw & 0x3F;
269
270 let mut ambe = [0u8; 9];
272 if let Some(src) = bytes.get(46..55) {
273 ambe.copy_from_slice(src);
274 }
275 let mut slow = [0u8; 3];
277 if let Some(src) = bytes.get(55..58) {
278 slow.copy_from_slice(src);
279 }
280 let eot_marker = slow == [0x55, 0x55, 0x55];
281 let is_end = eot_bit || eot_marker;
282
283 let frame = VoiceFrame {
284 ambe,
285 slow_data: slow,
286 };
287
288 let header = decode_dcs_header_from_voice(bytes);
290 if header.flag1 != 0 || header.flag2 != 0 || header.flag3 != 0 {
291 sink.record(Diagnostic::HeaderFlagsNonZero {
292 protocol: ProtocolKind::Dcs,
293 flag1: header.flag1,
294 flag2: header.flag2,
295 flag3: header.flag3,
296 });
297 }
298
299 Ok((header, stream_id, seq, frame, is_end))
300}
301
302fn decode_dcs_header_from_voice(bytes: &[u8]) -> DStarHeader {
311 let flag1 = bytes.get(4).copied().unwrap_or(0);
312 let flag2 = bytes.get(5).copied().unwrap_or(0);
313 let flag3 = bytes.get(6).copied().unwrap_or(0);
314
315 let mut rpt2 = [b' '; 8];
316 if let Some(src) = bytes.get(7..15) {
317 rpt2.copy_from_slice(src);
318 }
319 let mut rpt1 = [b' '; 8];
320 if let Some(src) = bytes.get(15..23) {
321 rpt1.copy_from_slice(src);
322 }
323 let mut ur = [b' '; 8];
324 if let Some(src) = bytes.get(23..31) {
325 ur.copy_from_slice(src);
326 }
327 let mut my = [b' '; 8];
328 if let Some(src) = bytes.get(31..39) {
329 my.copy_from_slice(src);
330 }
331 let mut sfx = [b' '; 4];
332 if let Some(src) = bytes.get(39..43) {
333 sfx.copy_from_slice(src);
334 }
335
336 DStarHeader {
337 flag1,
338 flag2,
339 flag3,
340 rpt2: Callsign::from_wire_bytes(rpt2),
341 rpt1: Callsign::from_wire_bytes(rpt1),
342 ur_call: Callsign::from_wire_bytes(ur),
343 my_call: Callsign::from_wire_bytes(my),
344 my_suffix: Suffix::from_wire_bytes(sfx),
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::codec::dcs::encode::{
352 encode_connect_ack, encode_connect_link, encode_connect_nak, encode_connect_unlink,
353 encode_poll_reply, encode_poll_request, encode_voice,
354 };
355 use crate::validator::NullSink;
356
357 type TestResult = Result<(), Box<dyn std::error::Error>>;
358
359 const fn cs(bytes: [u8; 8]) -> Callsign {
360 Callsign::from_wire_bytes(bytes)
361 }
362
363 #[expect(clippy::unwrap_used, reason = "compile-time validated: n != 0")]
364 const fn sid(n: u16) -> StreamId {
365 StreamId::new(n).unwrap()
366 }
367
368 fn test_header() -> DStarHeader {
369 DStarHeader {
370 flag1: 0,
371 flag2: 0,
372 flag3: 0,
373 rpt2: cs(*b"DCS001 G"),
374 rpt1: cs(*b"DCS001 C"),
375 ur_call: cs(*b"CQCQCQ "),
376 my_call: cs(*b"W1AW "),
377 my_suffix: Suffix::EMPTY,
378 }
379 }
380
381 #[test]
383 fn link_client_roundtrip() -> TestResult {
384 let mut buf = [0u8; 600];
385 let n = encode_connect_link(
386 &mut buf,
387 &cs(*b"W1AW "),
388 Module::B,
389 Module::C,
390 &cs(*b"DCS001 "),
391 GatewayType::Repeater,
392 )?;
393 let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
394 match pkt {
395 ClientPacket::Link {
396 callsign,
397 client_module,
398 reflector_module,
399 reflector_callsign,
400 gateway_type,
401 } => {
402 assert_eq!(callsign, cs(*b"W1AW "));
403 assert_eq!(client_module, Module::B);
404 assert_eq!(reflector_module, Module::C);
405 assert_eq!(reflector_callsign, cs(*b"DCS001 "));
406 assert_eq!(gateway_type, GatewayType::Repeater);
407 }
408 other => return Err(format!("expected Link, got {other:?}").into()),
409 }
410 Ok(())
411 }
412
413 #[test]
414 fn unlink_client_roundtrip() -> TestResult {
415 let mut buf = [0u8; 32];
416 let n = encode_connect_unlink(&mut buf, &cs(*b"W1AW "), Module::B, &cs(*b"DCS001 "))?;
417 let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
418 match pkt {
419 ClientPacket::Unlink {
420 callsign,
421 client_module,
422 reflector_callsign,
423 } => {
424 assert_eq!(callsign, cs(*b"W1AW "));
425 assert_eq!(client_module, Module::B);
426 assert_eq!(reflector_callsign, cs(*b"DCS001 "));
427 }
428 other => return Err(format!("expected Unlink, got {other:?}").into()),
429 }
430 Ok(())
431 }
432
433 #[test]
434 fn poll_client_roundtrip() -> TestResult {
435 let mut buf = [0u8; 32];
436 let n = encode_poll_request(&mut buf, &cs(*b"W1AW "), &cs(*b"DCS001 "))?;
437 let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
438 match pkt {
439 ClientPacket::Poll {
440 callsign,
441 reflector_callsign,
442 } => {
443 assert_eq!(callsign, cs(*b"W1AW "));
444 assert_eq!(reflector_callsign, cs(*b"DCS001 "));
445 }
446 other => return Err(format!("expected Poll, got {other:?}").into()),
447 }
448 Ok(())
449 }
450
451 #[test]
452 fn voice_client_roundtrip() -> TestResult {
453 let mut buf = [0u8; 128];
454 let frame = VoiceFrame {
455 ambe: [0x11; 9],
456 slow_data: [0x22; 3],
457 };
458 let n = encode_voice(&mut buf, &test_header(), sid(0xCAFE), 5, &frame, false)?;
459 let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
460 match pkt {
461 ClientPacket::Voice {
462 header,
463 stream_id,
464 seq,
465 frame: f,
466 is_end,
467 } => {
468 assert_eq!(stream_id, sid(0xCAFE));
469 assert_eq!(seq, 5);
470 assert_eq!(f.ambe, [0x11; 9]);
471 assert_eq!(f.slow_data, [0x22; 3]);
472 assert!(!is_end);
473 assert_eq!(header.my_call, test_header().my_call);
474 assert_eq!(header.rpt2, test_header().rpt2);
475 assert_eq!(header.ur_call, test_header().ur_call);
476 }
477 other => return Err(format!("expected Voice, got {other:?}").into()),
478 }
479 Ok(())
480 }
481
482 #[test]
483 fn voice_eot_client_roundtrip() -> TestResult {
484 let mut buf = [0u8; 128];
485 let frame = VoiceFrame {
486 ambe: [0x11; 9],
487 slow_data: [0x22; 3],
488 };
489 let n = encode_voice(&mut buf, &test_header(), sid(0x1234), 7, &frame, true)?;
490 let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
491 match pkt {
492 ClientPacket::Voice {
493 stream_id,
494 seq,
495 is_end,
496 ..
497 } => {
498 assert_eq!(stream_id, sid(0x1234));
499 assert_eq!(seq, 7);
500 assert!(is_end, "is_end should be true");
501 }
502 other => return Err(format!("expected Voice, got {other:?}").into()),
503 }
504 Ok(())
505 }
506
507 #[test]
509 fn connect_ack_server_roundtrip() -> TestResult {
510 let mut buf = [0u8; 32];
511 let n = encode_connect_ack(&mut buf, &cs(*b"DCS001 "), Module::C)?;
512 let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
513 match pkt {
514 ServerPacket::ConnectAck {
515 callsign,
516 reflector_module,
517 } => {
518 assert_eq!(callsign, cs(*b"DCS001 "));
519 assert_eq!(reflector_module, Module::C);
520 }
521 other => return Err(format!("expected ConnectAck, got {other:?}").into()),
522 }
523 Ok(())
524 }
525
526 #[test]
527 fn connect_nak_server_roundtrip() -> TestResult {
528 let mut buf = [0u8; 32];
529 let n = encode_connect_nak(&mut buf, &cs(*b"DCS001 "), Module::C)?;
530 let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
531 match pkt {
532 ServerPacket::ConnectNak {
533 callsign,
534 reflector_module,
535 } => {
536 assert_eq!(callsign, cs(*b"DCS001 "));
537 assert_eq!(reflector_module, Module::C);
538 }
539 other => return Err(format!("expected ConnectNak, got {other:?}").into()),
540 }
541 Ok(())
542 }
543
544 #[test]
545 fn poll_echo_server_roundtrip() -> TestResult {
546 let mut buf = [0u8; 32];
547 let n = encode_poll_reply(&mut buf, &cs(*b"DCS001 "), &cs(*b"DCS001 "))?;
548 let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
549 match pkt {
550 ServerPacket::PollEcho {
551 callsign,
552 reflector_callsign,
553 } => {
554 assert_eq!(callsign, cs(*b"DCS001 "));
555 assert_eq!(reflector_callsign, cs(*b"DCS001 "));
556 }
557 other => return Err(format!("expected PollEcho, got {other:?}").into()),
558 }
559 Ok(())
560 }
561
562 #[test]
563 fn voice_server_roundtrip() -> TestResult {
564 let mut buf = [0u8; 128];
565 let frame = VoiceFrame {
566 ambe: [0x33; 9],
567 slow_data: [0x44; 3],
568 };
569 let n = encode_voice(&mut buf, &test_header(), sid(0x4321), 9, &frame, false)?;
570 let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut NullSink)?;
571 match pkt {
572 ServerPacket::Voice {
573 stream_id,
574 seq,
575 frame: f,
576 is_end,
577 ..
578 } => {
579 assert_eq!(stream_id, sid(0x4321));
580 assert_eq!(seq, 9);
581 assert_eq!(f.ambe, [0x33; 9]);
582 assert_eq!(f.slow_data, [0x44; 3]);
583 assert!(!is_end);
584 }
585 other => return Err(format!("expected Voice, got {other:?}").into()),
586 }
587 Ok(())
588 }
589
590 #[test]
592 fn unknown_length_returns_error() -> TestResult {
593 let Err(err) = decode_client_to_server(&[0u8; 12], &mut NullSink) else {
594 return Err("expected error for bad length".into());
595 };
596 assert!(matches!(err, DcsError::UnknownPacketLength { got: 12 }));
597 Ok(())
598 }
599
600 #[test]
601 fn server_rejects_19_byte_client_unlink() -> TestResult {
602 let Err(err) = decode_server_to_client(&[0u8; 19], &mut NullSink) else {
604 return Err("expected error for server rejecting 19-byte".into());
605 };
606 assert!(matches!(err, DcsError::UnknownPacketLength { got: 19 }));
607 Ok(())
608 }
609
610 #[test]
611 fn client_rejects_14_byte_server_reply() -> TestResult {
612 let Err(err) = decode_client_to_server(&[0u8; 14], &mut NullSink) else {
614 return Err("expected error for client rejecting 14-byte".into());
615 };
616 assert!(matches!(err, DcsError::UnknownPacketLength { got: 14 }));
617 Ok(())
618 }
619
620 #[test]
621 fn link_with_invalid_client_module_byte() -> TestResult {
622 let mut buf = [0u8; 600];
624 let _n = encode_connect_link(
625 &mut buf,
626 &cs(*b"W1AW "),
627 Module::B,
628 Module::C,
629 &cs(*b"DCS001 "),
630 GatewayType::Repeater,
631 )?;
632 buf[8] = b'b'; let Err(err) = decode_client_to_server(&buf[..LINK_LEN], &mut NullSink) else {
634 return Err("expected error for invalid module byte".into());
635 };
636 assert!(matches!(
637 err,
638 DcsError::InvalidModuleByte {
639 offset: 8,
640 byte: b'b'
641 }
642 ));
643 Ok(())
644 }
645
646 #[test]
647 fn link_with_invalid_reflector_module_byte() -> TestResult {
648 let mut buf = [0u8; 600];
649 let _n = encode_connect_link(
650 &mut buf,
651 &cs(*b"W1AW "),
652 Module::B,
653 Module::C,
654 &cs(*b"DCS001 "),
655 GatewayType::Repeater,
656 )?;
657 buf[9] = b'1'; let Err(err) = decode_client_to_server(&buf[..LINK_LEN], &mut NullSink) else {
659 return Err("expected error for invalid reflector module byte".into());
660 };
661 assert!(matches!(
662 err,
663 DcsError::InvalidModuleByte {
664 offset: 9,
665 byte: b'1'
666 }
667 ));
668 Ok(())
669 }
670
671 #[test]
672 fn unlink_with_non_space_at_position_9() -> TestResult {
673 let mut buf = [0u8; 32];
674 let _n = encode_connect_unlink(&mut buf, &cs(*b"W1AW "), Module::B, &cs(*b"DCS001 "))?;
675 buf[9] = b'C'; let Err(err) = decode_client_to_server(&buf[..UNLINK_LEN], &mut NullSink) else {
677 return Err("expected error for non-space marker at position 9".into());
678 };
679 assert!(matches!(
680 err,
681 DcsError::UnlinkModuleByteInvalid { byte: b'C' }
682 ));
683 Ok(())
684 }
685
686 #[test]
687 fn voice_with_zero_stream_id_rejected() -> TestResult {
688 let mut buf = [0u8; 100];
689 buf[..4].copy_from_slice(b"0001");
690 let Err(err) = decode_client_to_server(&buf, &mut NullSink) else {
692 return Err("expected error for zero stream id".into());
693 };
694 assert!(matches!(err, DcsError::StreamIdZero));
695 Ok(())
696 }
697
698 #[test]
699 fn voice_missing_magic_rejected() -> TestResult {
700 let mut buf = [0u8; 100];
701 buf[..4].copy_from_slice(b"XXXX"); buf[43] = 0x34;
703 buf[44] = 0x12;
704 let Err(err) = decode_client_to_server(&buf, &mut NullSink) else {
705 return Err("expected error for bad voice magic".into());
706 };
707 assert!(matches!(err, DcsError::VoiceMagicMissing { .. }));
708 Ok(())
709 }
710
711 #[test]
712 fn connect_reply_with_unknown_tag() -> TestResult {
713 let mut buf = [0u8; 14];
714 buf[..8].copy_from_slice(b"DCS001 ");
715 buf[8] = b'C';
716 buf[9] = b'C';
717 buf[10..13].copy_from_slice(b"FOO");
719 buf[13] = 0x00;
720 let Err(err) = decode_server_to_client(&buf, &mut NullSink) else {
721 return Err("expected error for unknown connect tag".into());
722 };
723 assert!(matches!(err, DcsError::UnknownConnectTag { .. }));
724 Ok(())
725 }
726
727 #[test]
728 fn voice_eot_marker_alone_also_detected() -> TestResult {
729 let mut buf = [0u8; 128];
733 let frame = VoiceFrame {
734 ambe: [0x11; 9],
735 slow_data: [0; 3],
736 };
737 let _n = encode_voice(&mut buf, &test_header(), sid(0x1234), 3, &frame, false)?;
738 buf[55] = 0x55;
740 buf[56] = 0x55;
741 buf[57] = 0x55;
742 let pkt = decode_client_to_server(
743 buf.get(..VOICE_LEN).ok_or("VOICE_LEN within buf")?,
744 &mut NullSink,
745 )?;
746 match pkt {
747 ClientPacket::Voice { is_end, seq, .. } => {
748 assert_eq!(seq, 3);
749 assert!(is_end, "EOT marker alone should flag is_end");
750 }
751 other => return Err(format!("expected Voice, got {other:?}").into()),
752 }
753 Ok(())
754 }
755
756 #[test]
757 fn voice_flag_bytes_non_zero_raises_diagnostic() -> TestResult {
758 use crate::validator::VecSink;
759
760 let header = DStarHeader {
761 flag1: 0xAA,
762 ..test_header()
763 };
764 let mut buf = [0u8; 128];
765 let frame = VoiceFrame {
766 ambe: [0; 9],
767 slow_data: [0; 3],
768 };
769 let _n = encode_voice(&mut buf, &header, sid(1), 0, &frame, false)?;
770 let mut sink = VecSink::default();
771 let pkt = decode_client_to_server(
772 buf.get(..VOICE_LEN).ok_or("VOICE_LEN within buf")?,
773 &mut sink,
774 )?;
775 assert!(matches!(pkt, ClientPacket::Voice { .. }));
776 assert_eq!(sink.len(), 1, "expected one diagnostic");
777 Ok(())
778 }
779}