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}