dstar_gateway_core/codec/dcs/
decode.rs

1//! `DCS` packet decoders.
2//!
3//! Both directions — `decode_client_to_server` parses packets a
4//! client would send, `decode_server_to_client` parses packets a
5//! reflector would send.
6//!
7//! Lenient: recoverable malformations push `Diagnostic`s to the
8//! supplied sink but still return a parsed packet. Only fatal errors
9//! (wrong length, missing magic, zero stream id, invalid module byte)
10//! return `Err`.
11
12use 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
24/// Decode a UDP datagram sent from a `DCS` client (client → server).
25///
26/// # Errors
27///
28/// - [`DcsError::UnknownPacketLength`] for unrecognized lengths
29/// - [`DcsError::VoiceMagicMissing`] for 100-byte packets without `b"0001"` magic
30/// - [`DcsError::StreamIdZero`] for voice packets with stream id 0
31/// - [`DcsError::InvalidModuleByte`] for LINK with a non-A-Z module byte
32///   at `[8]` or `[9]`
33/// - [`DcsError::UnlinkModuleByteInvalid`] for UNLINK with byte `[9]` ≠
34///   `0x20`
35///
36/// # See also
37///
38/// `ircDDBGateway/Common/DCSProtocolHandler.cpp` — the reference
39/// parser this decoder mirrors. `xlxd/src/cdcsprotocol.cpp` is
40/// a mirror reference.
41pub 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
55/// Decode a UDP datagram sent from a `DCS` reflector (server → client).
56///
57/// # Errors
58///
59/// Same kinds as [`decode_client_to_server`], but produces
60/// [`ServerPacket`] variants. 14-byte ACK/NAK and 17-byte poll replies
61/// are server-side packets; 100-byte voice frames are forwarded
62/// bidirectionally.
63///
64/// # See also
65///
66/// `ircDDBGateway/Common/DCSProtocolHandler.cpp` — the reference
67/// parser for the server side of the `DCS` wire format.
68pub 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
81/// Extract an 8-byte callsign from a byte range, substituting spaces
82/// for embedded zero bytes (so the wire representation stays clean
83/// through `Callsign::from_wire_bytes`).
84///
85/// This reader is used for every DCS callsign slot — poll,
86/// reflector-callsign at `[11..19]`, and the connect-packet
87/// prefix at `[0..8]`. The wire format has byte `[7]` as the
88/// `memset` pad slot and byte `[8]` outside the 8-byte window
89/// holding the module letter (for LINK/ACK/NAK) or `0x00` (for
90/// poll). Our Rust API exposes the module as a separate `Module`
91/// field, so this reader deliberately does NOT splice byte `[8]`
92/// into the callsign. Keeping byte `[7]` as the plain space from
93/// the wire matches what the 17-byte poll decoder sees and lets
94/// the server session compare stored callsigns against incoming
95/// polls byte-for-byte.
96fn 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
112/// Decode a 519-byte LINK packet from the client side.
113fn 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    // We don't parse the HTML payload — default to Repeater.
129    Ok(ClientPacket::Link {
130        callsign,
131        client_module,
132        reflector_module,
133        reflector_callsign,
134        gateway_type: GatewayType::Repeater,
135    })
136}
137
138/// Decode a 19-byte UNLINK packet from the client side.
139fn 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
159/// Decode a 17-byte poll packet from the client side.
160fn 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
169/// Decode a 17-byte poll echo from the server side.
170fn 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
179/// Decode a 14-byte ACK or NAK reply from the server side.
180fn 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    // Tag is at [10..13], NUL at [13] per
189    // `ircDDBGateway/Common/ConnectData.cpp:374-393`.
190    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
209/// Decode a 100-byte voice packet from the client side.
210fn 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
224/// Decode a 100-byte voice packet from the server side.
225fn 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
239/// Shared 100-byte voice parser. Returns the embedded header, stream
240/// id, seq byte (with `0x40` stripped), the AMBE + slow data frame,
241/// and `is_end` flag.
242fn parse_voice(
243    bytes: &[u8],
244    sink: &mut dyn DiagnosticSink,
245) -> Result<(DStarHeader, StreamId, u8, VoiceFrame, bool), DcsError> {
246    // Magic check at [0..4].
247    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    // Stream id at [43..45] little-endian.
257    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    // Seq at [45]. Strip 0x40 bit to get the "real" seq and use it for
263    // is_end detection. We also consult bytes [55..58] for the EOT
264    // marker (the reference DCS uses the marker; xlxd additionally
265    // sets the 0x40 bit). Either signal flags end-of-stream.
266    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    // AMBE at [46..55].
271    let mut ambe = [0u8; 9];
272    if let Some(src) = bytes.get(46..55) {
273        ambe.copy_from_slice(src);
274    }
275    // Slow data at [55..58].
276    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    // Embedded header at [4..43] per HeaderData.cpp:520-528.
289    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
302/// Extract a `DStarHeader` from the embedded `[4..43]` region of a
303/// DCS voice packet.
304///
305/// DCS stores the header fields starting at offset 4 with a layout
306/// that differs from [`DStarHeader::encode`]'s default 41-byte
307/// encoding — the flag bytes are at offsets 4/5/6 and the suffix is
308/// at offsets 39..43 (no CRC). Build the struct manually from the
309/// field positions.
310fn 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    // ─── Client roundtrips ──────────────────────────────────
382    #[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    // ─── Server roundtrips ─────────────────────────────────
508    #[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    // ─── Error cases ────────────────────────────────────────
591    #[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        // 19-byte UNLINK is client-only; server never sends these.
603        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        // 14-byte ACK/NAK is server-only.
613        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        // Start with a valid LINK then stomp the client module byte.
623        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'; // lowercase — invalid
633        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'; // digit — invalid
658        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'; // not space
676        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        // stream id at [43..45] left as zero
691        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"); // wrong magic
702        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        // Tag is at [10..13], NUL at [13] per the reference.
718        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        // A packet that carries the 0x55 marker but NOT the 0x40 seq
730        // bit should still parse as EOT (the reference DCS writes the
731        // marker, xlxd adds the bit).
732        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        // Manually set the EOT marker without touching the seq byte.
739        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}