dstar_gateway_core/codec/dplus/
decode.rs

1//! `DPlus` 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) return `Err`.
10
11use crate::header::{DStarHeader, ENCODED_LEN};
12use crate::types::{Callsign, ProtocolKind, StreamId};
13use crate::validator::{Diagnostic, DiagnosticSink};
14use crate::voice::VoiceFrame;
15
16use super::consts::{DSVT_MAGIC, LINK2_ACCEPT_TAG, LINK2_BUSY_TAG};
17use super::error::DPlusError;
18use super::packet::{ClientPacket, Link2Result, ServerPacket};
19
20/// Decode a UDP datagram sent from a `DPlus` reflector (server → client).
21///
22/// # Errors
23///
24/// - `DPlusError::UnknownPacketLength` for unrecognized lengths
25/// - `DPlusError::DsvtMagicMissing` for DSVT-length packets without the magic
26/// - `DPlusError::StreamIdZero` for voice packets with stream id 0
27/// - `DPlusError::InvalidShortControlByte` for 5-byte packets with unknown control
28///
29/// # See also
30///
31/// `ircDDBGateway/Common/DPlusProtocolHandler.cpp` — the reference
32/// parser this decoder mirrors (length-dispatch then DSVT-magic
33/// branch). `xlxd/src/cdplusprotocol.cpp` is a mirror reference.
34pub fn decode_server_to_client(
35    bytes: &[u8],
36    sink: &mut dyn DiagnosticSink,
37) -> Result<ServerPacket, DPlusError> {
38    let len = bytes.len();
39    let is_dsvt = len >= 6 && bytes.get(2..6) == Some(DSVT_MAGIC.as_slice());
40
41    if !is_dsvt {
42        return match len {
43            3 => Ok(ServerPacket::PollEcho),
44            5 => {
45                let ctrl = bytes.get(4).copied().unwrap_or(0);
46                match ctrl {
47                    0x01 => Ok(ServerPacket::Link1Ack),
48                    0x00 => Ok(ServerPacket::UnlinkAck),
49                    other => Err(DPlusError::InvalidShortControlByte { byte: other }),
50                }
51            }
52            8 => {
53                let tag_slice = bytes.get(4..8).unwrap_or(&[]);
54                let mut tag = [0u8; 4];
55                tag.copy_from_slice(tag_slice);
56                let result = if tag == LINK2_ACCEPT_TAG {
57                    Link2Result::Accept
58                } else if tag == LINK2_BUSY_TAG {
59                    Link2Result::Busy
60                } else {
61                    sink.record(Diagnostic::UnknownLink2Reply { reply: tag });
62                    Link2Result::Unknown { reply: tag }
63                };
64                Ok(ServerPacket::Link2Reply { result })
65            }
66            28 => {
67                let cs_bytes = bytes.get(4..12).unwrap_or(&[]);
68                let mut cs = [b' '; 8];
69                let copy_len = cs_bytes.len().min(8);
70                if let Some(dst) = cs.get_mut(..copy_len)
71                    && let Some(src) = cs_bytes.get(..copy_len)
72                {
73                    dst.copy_from_slice(src);
74                }
75                // Treat trailing zeros as padding — replace them with spaces so
76                // `Callsign::from_wire_bytes` stores a clean wire representation.
77                for b in &mut cs {
78                    if *b == 0 {
79                        *b = b' ';
80                    }
81                }
82                Ok(ServerPacket::Link2Echo {
83                    callsign: Callsign::from_wire_bytes(cs),
84                })
85            }
86            _ => Err(DPlusError::UnknownPacketLength { got: len }),
87        };
88    }
89
90    // DSVT-framed path.
91    decode_dsvt_server(bytes, sink)
92}
93
94/// Decode a UDP datagram sent from a `DPlus` client (client → server).
95///
96/// # Errors
97///
98/// Same as [`decode_server_to_client`], but produces [`ClientPacket`]
99/// variants. The 8-byte length is NOT accepted here — clients do not
100/// send 8-byte LINK2 replies.
101///
102/// # See also
103///
104/// `ircDDBGateway/Common/DPlusProtocolHandler.cpp` — mirror parser
105/// on the server side.
106pub fn decode_client_to_server(
107    bytes: &[u8],
108    sink: &mut dyn DiagnosticSink,
109) -> Result<ClientPacket, DPlusError> {
110    let len = bytes.len();
111    let is_dsvt = len >= 6 && bytes.get(2..6) == Some(DSVT_MAGIC.as_slice());
112
113    if !is_dsvt {
114        return match len {
115            3 => Ok(ClientPacket::Poll),
116            5 => {
117                let ctrl = bytes.get(4).copied().unwrap_or(0);
118                match ctrl {
119                    0x01 => Ok(ClientPacket::Link1),
120                    0x00 => Ok(ClientPacket::Unlink),
121                    other => Err(DPlusError::InvalidShortControlByte { byte: other }),
122                }
123            }
124            28 => {
125                let cs_bytes = bytes.get(4..12).unwrap_or(&[]);
126                let mut cs = [b' '; 8];
127                let copy_len = cs_bytes.len().min(8);
128                if let Some(dst) = cs.get_mut(..copy_len)
129                    && let Some(src) = cs_bytes.get(..copy_len)
130                {
131                    dst.copy_from_slice(src);
132                }
133                for b in &mut cs {
134                    if *b == 0 {
135                        *b = b' ';
136                    }
137                }
138                Ok(ClientPacket::Link2 {
139                    callsign: Callsign::from_wire_bytes(cs),
140                })
141            }
142            _ => Err(DPlusError::UnknownPacketLength { got: len }),
143        };
144    }
145
146    // DSVT-framed path.
147    decode_dsvt_client(bytes, sink)
148}
149
150fn decode_dsvt_server(
151    bytes: &[u8],
152    sink: &mut dyn DiagnosticSink,
153) -> Result<ServerPacket, DPlusError> {
154    let (stream_id, header, frame_bytes, is_header, is_eot, len) = parse_dsvt_common(bytes, sink)?;
155    if is_header {
156        let Some(hdr) = header else {
157            return Err(DPlusError::UnknownPacketLength { got: len });
158        };
159        Ok(ServerPacket::VoiceHeader {
160            stream_id,
161            header: hdr,
162        })
163    } else if is_eot {
164        let seq = bytes.get(16).copied().unwrap_or(0);
165        Ok(ServerPacket::VoiceEot { stream_id, seq })
166    } else {
167        let Some(frame) = frame_bytes else {
168            return Err(DPlusError::UnknownPacketLength { got: len });
169        };
170        let seq = bytes.get(16).copied().unwrap_or(0);
171        Ok(ServerPacket::VoiceData {
172            stream_id,
173            seq,
174            frame,
175        })
176    }
177}
178
179fn decode_dsvt_client(
180    bytes: &[u8],
181    sink: &mut dyn DiagnosticSink,
182) -> Result<ClientPacket, DPlusError> {
183    let (stream_id, header, frame_bytes, is_header, is_eot, len) = parse_dsvt_common(bytes, sink)?;
184    if is_header {
185        let Some(hdr) = header else {
186            return Err(DPlusError::UnknownPacketLength { got: len });
187        };
188        Ok(ClientPacket::VoiceHeader {
189            stream_id,
190            header: hdr,
191        })
192    } else if is_eot {
193        let seq = bytes.get(16).copied().unwrap_or(0);
194        Ok(ClientPacket::VoiceEot { stream_id, seq })
195    } else {
196        let Some(frame) = frame_bytes else {
197            return Err(DPlusError::UnknownPacketLength { got: len });
198        };
199        let seq = bytes.get(16).copied().unwrap_or(0);
200        Ok(ClientPacket::VoiceData {
201            stream_id,
202            seq,
203            frame,
204        })
205    }
206}
207
208type DsvtParse = (
209    StreamId,
210    Option<DStarHeader>,
211    Option<VoiceFrame>,
212    bool,
213    bool,
214    usize,
215);
216
217fn parse_dsvt_common(bytes: &[u8], sink: &mut dyn DiagnosticSink) -> Result<DsvtParse, DPlusError> {
218    let len = bytes.len();
219    match len {
220        58 | 29 | 32 => {}
221        _ => return Err(DPlusError::UnknownPacketLength { got: len }),
222    }
223
224    // Stream id at [14..16] little-endian.
225    let lo = bytes.get(14).copied().unwrap_or(0);
226    let hi = bytes.get(15).copied().unwrap_or(0);
227    let raw_sid = u16::from_le_bytes([lo, hi]);
228    let stream_id = StreamId::new(raw_sid).ok_or(DPlusError::StreamIdZero)?;
229
230    let is_header = len == 58 && bytes.get(6).copied() == Some(0x10);
231    let is_eot = len == 32 && bytes.get(6).copied() == Some(0x20);
232    let is_voice = len == 29 && bytes.get(6).copied() == Some(0x20);
233
234    if !is_header && !is_eot && !is_voice {
235        return Err(DPlusError::UnknownPacketLength { got: len });
236    }
237
238    let header = if is_header {
239        let hdr_slice = bytes
240            .get(17..58)
241            .ok_or(DPlusError::UnknownPacketLength { got: len })?;
242        let mut arr = [0u8; ENCODED_LEN];
243        arr.copy_from_slice(hdr_slice);
244        let decoded = DStarHeader::decode(&arr);
245        // Lenient: diagnose non-zero flag bytes but still return the header.
246        if decoded.flag1 != 0 || decoded.flag2 != 0 || decoded.flag3 != 0 {
247            sink.record(Diagnostic::HeaderFlagsNonZero {
248                protocol: ProtocolKind::DPlus,
249                flag1: decoded.flag1,
250                flag2: decoded.flag2,
251                flag3: decoded.flag3,
252            });
253        }
254        Some(decoded)
255    } else {
256        None
257    };
258
259    let frame = if is_voice {
260        let mut ambe = [0u8; 9];
261        let mut slow = [0u8; 3];
262        if let Some(src) = bytes.get(17..26) {
263            ambe.copy_from_slice(src);
264        }
265        if let Some(src) = bytes.get(26..29) {
266            slow.copy_from_slice(src);
267        }
268        Some(VoiceFrame {
269            ambe,
270            slow_data: slow,
271        })
272    } else {
273        None
274    };
275
276    Ok((stream_id, header, frame, is_header, is_eot, len))
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::codec::dplus::encode::{
283        encode_link1, encode_link2, encode_link2_reply, encode_poll, encode_unlink,
284        encode_voice_data, encode_voice_eot, encode_voice_header,
285    };
286    use crate::types::Suffix;
287    use crate::validator::NullSink;
288
289    type TestResult = Result<(), Box<dyn std::error::Error>>;
290
291    const fn cs(bytes: [u8; 8]) -> Callsign {
292        Callsign::from_wire_bytes(bytes)
293    }
294
295    #[expect(clippy::unwrap_used, reason = "compile-time validated: n != 0")]
296    const fn sid(n: u16) -> StreamId {
297        StreamId::new(n).unwrap()
298    }
299
300    fn test_header() -> DStarHeader {
301        DStarHeader {
302            flag1: 0,
303            flag2: 0,
304            flag3: 0,
305            rpt2: cs(*b"REF030 G"),
306            rpt1: cs(*b"REF030 C"),
307            ur_call: cs(*b"CQCQCQ  "),
308            my_call: cs(*b"W1AW    "),
309            my_suffix: Suffix::EMPTY,
310        }
311    }
312
313    // ─── Client-side (what client sends) roundtrips ───────────
314    #[test]
315    fn link1_client_roundtrip() -> TestResult {
316        let mut buf = [0u8; 16];
317        let n = encode_link1(&mut buf)?;
318        let mut sink = NullSink;
319        let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
320        assert!(matches!(pkt, ClientPacket::Link1));
321        Ok(())
322    }
323
324    #[test]
325    fn unlink_client_roundtrip() -> TestResult {
326        let mut buf = [0u8; 16];
327        let n = encode_unlink(&mut buf)?;
328        let mut sink = NullSink;
329        let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
330        assert!(matches!(pkt, ClientPacket::Unlink));
331        Ok(())
332    }
333
334    #[test]
335    fn poll_client_roundtrip() -> TestResult {
336        let mut buf = [0u8; 16];
337        let n = encode_poll(&mut buf)?;
338        let mut sink = NullSink;
339        let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
340        assert!(matches!(pkt, ClientPacket::Poll));
341        Ok(())
342    }
343
344    #[test]
345    fn link2_client_roundtrip() -> TestResult {
346        let cs_in = cs(*b"W1AW    ");
347        let mut buf = [0u8; 32];
348        let n = encode_link2(&mut buf, &cs_in)?;
349        let mut sink = NullSink;
350        let pkt = decode_client_to_server(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
351        match pkt {
352            ClientPacket::Link2 { callsign } => assert_eq!(callsign, cs_in),
353            other => return Err(format!("expected Link2, got {other:?}").into()),
354        }
355        Ok(())
356    }
357
358    // ─── Server-side (what server sends) roundtrips ───────────
359    #[test]
360    fn link1_ack_server_roundtrip() -> TestResult {
361        // LINK1 ACK uses the same bytes as LINK1 (the server echoes it).
362        let mut buf = [0u8; 16];
363        let n = encode_link1(&mut buf)?;
364        let mut sink = NullSink;
365        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
366        assert!(matches!(pkt, ServerPacket::Link1Ack));
367        Ok(())
368    }
369
370    #[test]
371    fn unlink_ack_server_roundtrip() -> TestResult {
372        let mut buf = [0u8; 16];
373        let n = encode_unlink(&mut buf)?;
374        let mut sink = NullSink;
375        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
376        assert!(matches!(pkt, ServerPacket::UnlinkAck));
377        Ok(())
378    }
379
380    #[test]
381    fn poll_echo_server_roundtrip() -> TestResult {
382        let mut buf = [0u8; 16];
383        let n = encode_poll(&mut buf)?;
384        let mut sink = NullSink;
385        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
386        assert!(matches!(pkt, ServerPacket::PollEcho));
387        Ok(())
388    }
389
390    #[test]
391    fn link2_reply_accept_roundtrip() -> TestResult {
392        let mut buf = [0u8; 16];
393        let n = encode_link2_reply(&mut buf, Link2Result::Accept)?;
394        let mut sink = NullSink;
395        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
396        assert!(matches!(
397            pkt,
398            ServerPacket::Link2Reply {
399                result: Link2Result::Accept
400            }
401        ));
402        Ok(())
403    }
404
405    #[test]
406    fn link2_reply_busy_roundtrip() -> TestResult {
407        let mut buf = [0u8; 16];
408        let n = encode_link2_reply(&mut buf, Link2Result::Busy)?;
409        let mut sink = NullSink;
410        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
411        assert!(matches!(
412            pkt,
413            ServerPacket::Link2Reply {
414                result: Link2Result::Busy
415            }
416        ));
417        Ok(())
418    }
419
420    #[test]
421    fn link2_reply_unknown_records_diagnostic() -> TestResult {
422        // 8-byte reply with a tag that's neither OKRW nor BUSY — should still
423        // parse as Link2Reply { Unknown } and fire Diagnostic::UnknownLink2Reply.
424        let bytes = [0x08, 0xC0, 0x04, 0x00, b'F', b'A', b'I', b'L'];
425        let mut sink = crate::validator::VecSink::default();
426        let pkt = decode_server_to_client(&bytes, &mut sink)?;
427        assert!(matches!(
428            pkt,
429            ServerPacket::Link2Reply {
430                result: Link2Result::Unknown { .. }
431            }
432        ));
433        assert_eq!(sink.len(), 1);
434        Ok(())
435    }
436
437    // ─── Voice frames ─────────────────────────────────────────
438    #[test]
439    fn voice_header_server_roundtrip() -> TestResult {
440        let mut buf = [0u8; 64];
441        let n = encode_voice_header(&mut buf, sid(0xCAFE), &test_header())?;
442        let mut sink = NullSink;
443        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
444        match pkt {
445            ServerPacket::VoiceHeader { stream_id, header } => {
446                assert_eq!(stream_id, sid(0xCAFE));
447                assert_eq!(header.my_call, test_header().my_call);
448            }
449            other => return Err(format!("expected VoiceHeader, got {other:?}").into()),
450        }
451        Ok(())
452    }
453
454    #[test]
455    fn voice_data_server_roundtrip() -> TestResult {
456        let frame = VoiceFrame {
457            ambe: [0x11; 9],
458            slow_data: [0x22; 3],
459        };
460        let mut buf = [0u8; 64];
461        let n = encode_voice_data(&mut buf, sid(0x1234), 5, &frame)?;
462        let mut sink = NullSink;
463        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
464        match pkt {
465            ServerPacket::VoiceData {
466                stream_id,
467                seq,
468                frame: f,
469            } => {
470                assert_eq!(stream_id, sid(0x1234));
471                assert_eq!(seq, 5);
472                assert_eq!(f.ambe, [0x11; 9]);
473                assert_eq!(f.slow_data, [0x22; 3]);
474            }
475            other => return Err(format!("expected VoiceData, got {other:?}").into()),
476        }
477        Ok(())
478    }
479
480    #[test]
481    fn voice_eot_server_roundtrip() -> TestResult {
482        let mut buf = [0u8; 64];
483        let n = encode_voice_eot(&mut buf, sid(0x1234), 7)?;
484        let mut sink = NullSink;
485        let pkt = decode_server_to_client(buf.get(..n).ok_or("n within buf")?, &mut sink)?;
486        match pkt {
487            ServerPacket::VoiceEot { stream_id, seq } => {
488                assert_eq!(stream_id, sid(0x1234));
489                assert_eq!(seq & 0x40, 0x40, "EOT bit set");
490                assert_eq!(seq & 0x3F, 7, "low bits preserve seq");
491            }
492            other => return Err(format!("expected VoiceEot, got {other:?}").into()),
493        }
494        Ok(())
495    }
496
497    // ─── Error cases ──────────────────────────────────────────
498    #[test]
499    fn unknown_length_returns_error() -> TestResult {
500        let mut sink = NullSink;
501        let Err(err) = decode_client_to_server(&[0u8; 11], &mut sink) else {
502            return Err("expected error for bad length".into());
503        };
504        assert!(matches!(err, DPlusError::UnknownPacketLength { got: 11 }));
505        Ok(())
506    }
507
508    #[test]
509    fn short_5_byte_with_bad_control_byte() -> TestResult {
510        let mut sink = NullSink;
511        let Err(err) = decode_client_to_server(&[0x05, 0x00, 0x18, 0x00, 0x77], &mut sink) else {
512            return Err("expected error for bad control byte".into());
513        };
514        assert!(matches!(
515            err,
516            DPlusError::InvalidShortControlByte { byte: 0x77 }
517        ));
518        Ok(())
519    }
520
521    #[test]
522    fn client_rejects_8_byte_server_reply() -> TestResult {
523        // 8-byte LINK2 reply is server-only.
524        let bytes = [0x08, 0xC0, 0x04, 0x00, b'O', b'K', b'R', b'W'];
525        let mut sink = NullSink;
526        let Err(err) = decode_client_to_server(&bytes, &mut sink) else {
527            return Err("expected error for client rejecting 8-byte server reply".into());
528        };
529        assert!(matches!(err, DPlusError::UnknownPacketLength { got: 8 }));
530        Ok(())
531    }
532}