dstar_gateway_core/codec/dplus/
error.rs

1//! `DPlus` wire-format errors returned by the codec.
2//!
3//! This is the codec-layer error type. It composes into
4//! [`crate::error::ProtocolError`] via `From` impls.
5
6use crate::error::EncodeError;
7use crate::validator::CallsignField;
8
9/// Errors returned by the `DPlus` codec functions.
10#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum DPlusError {
13    /// `DPlus` packet length is not one of the known sizes.
14    ///
15    /// Valid non-DSVT lengths: 3 (poll), 5 (LINK1/UNLINK ACK), 8 (LINK2 reply),
16    /// 28 (LINK2 / echo). Valid DSVT lengths: 29 (voice data), 32 (voice EOT),
17    /// 58 (voice header).
18    #[error("DPlus packet length {got} not valid for any known type")]
19    UnknownPacketLength {
20        /// Observed length.
21        got: usize,
22    },
23
24    /// Packet expected to be DSVT-framed but the magic at `[2..6]` is wrong.
25    #[error("expected DSVT magic at offset 2..6, got {got:02X?}")]
26    DsvtMagicMissing {
27        /// The 4 bytes at offsets 2..6.
28        got: [u8; 4],
29    },
30
31    /// Stream id at offsets `[14..16]` is zero (reserved per D-STAR spec).
32    #[error("DPlus stream id is zero (reserved)")]
33    StreamIdZero,
34
35    /// The 5-byte non-DSVT packet has an unknown control byte at offset 4.
36    ///
37    /// Valid values: 0x00 (UNLINK), 0x01 (LINK1).
38    #[error("DPlus 5-byte packet has invalid control byte 0x{byte:02X}")]
39    InvalidShortControlByte {
40        /// The rejected byte.
41        byte: u8,
42    },
43
44    /// `DPlus` auth chunk truncated at offset {offset}: needed {need} bytes, have {have}.
45    #[error("DPlus auth chunk truncated at offset {offset}: needed {need} bytes, have {have}")]
46    AuthChunkTruncated {
47        /// Byte offset where truncation occurred.
48        offset: usize,
49        /// Bytes needed to complete the chunk.
50        need: usize,
51        /// Bytes actually present.
52        have: usize,
53    },
54
55    /// Auth chunk flag byte `[1]` failed validation — `(b1 & 0xC0) != 0xC0`.
56    #[error("DPlus auth chunk has invalid flag byte 0x{byte:02X} at offset {offset}")]
57    AuthChunkFlagsInvalid {
58        /// Byte offset of the chunk header.
59        offset: usize,
60        /// The rejected flag byte.
61        byte: u8,
62    },
63
64    /// Auth chunk type byte `[2]` is not `0x01`.
65    #[error("DPlus auth chunk has invalid type byte 0x{byte:02X} at offset {offset}")]
66    AuthChunkTypeInvalid {
67        /// Byte offset of the chunk header.
68        offset: usize,
69        /// The rejected type byte.
70        byte: u8,
71    },
72
73    /// Auth chunk length `{claimed}` is smaller than the 8-byte chunk header.
74    #[error("DPlus auth chunk length {claimed} smaller than 8-byte header at offset {offset}")]
75    AuthChunkUndersized {
76        /// Byte offset of the chunk header.
77        offset: usize,
78        /// The too-small length value.
79        claimed: usize,
80    },
81
82    /// Reserved for callsign parsing errors on received packets.
83    ///
84    /// Currently unused — the lenient RX path uses
85    /// `Callsign::from_wire_bytes` which stores bytes verbatim and
86    /// cannot fail. This variant exists so that if a future strict
87    /// mode rejects non-printable wire callsigns at the codec
88    /// level, the error type already carries the right shape
89    /// without a breaking API change (the enum is `#[non_exhaustive]`).
90    #[error("DPlus callsign field {field:?} has invalid bytes")]
91    CallsignFieldInvalid {
92        /// Which callsign field.
93        field: CallsignField,
94    },
95
96    /// An encoder was called with an undersized output buffer.
97    ///
98    /// This is a programming error inside
99    /// [`crate::session::client::SessionCore`] — it should never
100    /// fire in production because the core sizes its own scratch
101    /// buffers. Propagated as a variant rather than swallowed so
102    /// callers can still surface the fault.
103    #[error("DPlus encode buffer too small: need {need}, have {have}")]
104    EncodeBufferTooSmall {
105        /// How many bytes the encoder needed.
106        need: usize,
107        /// How many bytes the buffer actually held.
108        have: usize,
109    },
110}
111
112impl From<EncodeError> for DPlusError {
113    fn from(value: EncodeError) -> Self {
114        match value {
115            EncodeError::BufferTooSmall { need, have } => Self::EncodeBufferTooSmall { need, have },
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn unknown_packet_length_display() {
126        let err = DPlusError::UnknownPacketLength { got: 42 };
127        assert!(err.to_string().contains("42"));
128    }
129
130    #[test]
131    fn dsvt_magic_missing_display() {
132        let err = DPlusError::DsvtMagicMissing {
133            got: [0x01, 0x02, 0x03, 0x04],
134        };
135        assert!(err.to_string().contains("01"));
136    }
137
138    #[test]
139    fn stream_id_zero_display() {
140        let err = DPlusError::StreamIdZero;
141        assert_eq!(err.to_string(), "DPlus stream id is zero (reserved)");
142    }
143
144    #[test]
145    fn auth_chunk_truncated_display() {
146        let err = DPlusError::AuthChunkTruncated {
147            offset: 16,
148            need: 26,
149            have: 8,
150        };
151        let s = err.to_string();
152        assert!(s.contains("16"));
153        assert!(s.contains("26"));
154        assert!(s.contains('8'));
155    }
156}