dstar_gateway_core/codec/dcs/
error.rs

1//! `DCS` wire-format errors returned by the codec.
2
3use crate::error::EncodeError;
4
5/// Errors returned by the `DCS` codec functions.
6#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
7#[non_exhaustive]
8pub enum DcsError {
9    /// `DCS` packet length is not one of the known sizes.
10    #[error("DCS packet length {got} not valid for any known type")]
11    UnknownPacketLength {
12        /// Observed length.
13        got: usize,
14    },
15
16    /// Voice frame magic at `[0..4]` is not `b"0001"`.
17    #[error("DCS voice magic missing at offset 0..4, got {got:02X?}")]
18    VoiceMagicMissing {
19        /// Observed 4-byte magic.
20        got: [u8; 4],
21    },
22
23    /// Stream id at `[43..45]` is zero.
24    #[error("DCS stream id is zero (reserved)")]
25    StreamIdZero,
26
27    /// Connect reply tag at `[10..13]` is neither `ACK` nor `NAK`.
28    #[error("DCS connect reply has unknown tag {tag:02X?}")]
29    UnknownConnectTag {
30        /// The 3-byte tag observed.
31        tag: [u8; 3],
32    },
33
34    /// UNLINK byte `[9]` is not the expected space (`0x20`).
35    #[error("DCS UNLINK has invalid module byte 0x{byte:02X} at offset 9 (expected 0x20)")]
36    UnlinkModuleByteInvalid {
37        /// Rejected byte.
38        byte: u8,
39    },
40
41    /// LINK or UNLINK packet has a non-A-Z module byte at `[8]` or `[9]`.
42    #[error("DCS connect packet has invalid module byte 0x{byte:02X} at offset {offset}")]
43    InvalidModuleByte {
44        /// Byte offset within the 19- or 519-byte packet.
45        offset: usize,
46        /// Rejected byte.
47        byte: u8,
48    },
49
50    /// An encoder was called with an undersized output buffer.
51    ///
52    /// Programming error inside [`crate::session::client::SessionCore`];
53    /// surfaced as a variant rather than swallowed so callers can
54    /// still observe the fault.
55    #[error("DCS encode buffer too small: need {need}, have {have}")]
56    EncodeBufferTooSmall {
57        /// How many bytes the encoder needed.
58        need: usize,
59        /// How many bytes the buffer actually held.
60        have: usize,
61    },
62
63    /// `send_voice` / `send_eot` called before `send_header` cached the TX header.
64    ///
65    /// The DCS wire format embeds the D-STAR header in every 100-byte
66    /// voice frame, so [`crate::session::client::SessionCore`] must
67    /// have a cached header before it can encode voice data or EOT.
68    /// Call `send_header` first.
69    #[error("DCS send_voice called before send_header cached the TX header")]
70    NoTxHeader,
71}
72
73impl From<EncodeError> for DcsError {
74    fn from(value: EncodeError) -> Self {
75        match value {
76            EncodeError::BufferTooSmall { need, have } => Self::EncodeBufferTooSmall { need, have },
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn unknown_length_display() {
87        let err = DcsError::UnknownPacketLength { got: 42 };
88        assert!(err.to_string().contains("42"));
89    }
90
91    #[test]
92    fn voice_magic_missing_display() {
93        let err = DcsError::VoiceMagicMissing {
94            got: [b'X', b'X', b'X', b'X'],
95        };
96        let s = err.to_string();
97        assert!(s.contains("58"), "display should contain hex of 'X' (0x58)");
98    }
99
100    #[test]
101    fn stream_id_zero_display() {
102        let err = DcsError::StreamIdZero;
103        assert_eq!(err.to_string(), "DCS stream id is zero (reserved)");
104    }
105
106    #[test]
107    fn unknown_connect_tag_display() {
108        let err = DcsError::UnknownConnectTag {
109            tag: [b'F', b'O', b'O'],
110        };
111        assert!(
112            err.to_string().contains("46"),
113            "display should contain hex of 'F'"
114        );
115    }
116
117    #[test]
118    fn unlink_module_byte_invalid_display() {
119        let err = DcsError::UnlinkModuleByteInvalid { byte: 0x41 };
120        let s = err.to_string();
121        assert!(s.contains("41"));
122        assert!(s.contains('9'));
123    }
124
125    #[test]
126    fn invalid_module_byte_display() {
127        let err = DcsError::InvalidModuleByte {
128            offset: 8,
129            byte: 0x40,
130        };
131        let s = err.to_string();
132        assert!(s.contains('8'));
133        assert!(s.contains("40"));
134    }
135
136    #[test]
137    fn no_tx_header_display_mentions_send_header() {
138        let err = DcsError::NoTxHeader;
139        let s = err.to_string();
140        assert!(s.contains("send_header"));
141    }
142}