osdp/error.rs
1//! Crate-wide error type. `core::error::Error`-compatible.
2
3use core::fmt;
4
5/// Convenience [`Result`] alias.
6pub type Result<T> = core::result::Result<T, Error>;
7
8/// Crate-wide error.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum Error {
12 /// Buffer too small to hold a syntactically valid packet header.
13 Truncated {
14 /// Bytes available.
15 have: usize,
16 /// Bytes required.
17 need: usize,
18 },
19 /// SOM mismatch — packet did not start with `0x53`.
20 BadSom(u8),
21 /// `LEN` field disagrees with the actual buffer length.
22 BadLength {
23 /// Length declared in the LEN field.
24 declared: usize,
25 /// Length actually delivered.
26 actual: usize,
27 },
28 /// Reserved bits in the [`crate::ControlByte`] are non-zero, or the byte is
29 /// otherwise malformed.
30 BadControlByte(u8),
31 /// CRC verification failed.
32 BadCrc {
33 /// CRC carried on the wire.
34 got: u16,
35 /// CRC we computed from the header + payload.
36 want: u16,
37 },
38 /// Checksum verification failed.
39 BadChecksum {
40 /// Trailer byte carried on the wire.
41 got: u8,
42 /// Checksum we computed.
43 want: u8,
44 },
45 /// Address field is reserved or invalid.
46 BadAddress(u8),
47 /// Sequence number is not in `0..=3`.
48 BadSqn(u8),
49 /// Security block type is unknown.
50 BadSecurityBlock(u8),
51 /// Length of the security block is wrong for its type.
52 BadSecurityBlockLength(u8),
53 /// MAC verification failed (constant-time compare).
54 BadMac,
55 /// MAC is too short (must be at least 4 bytes on the wire).
56 ShortMac,
57 /// Command code is not recognized.
58 UnknownCommand(u8),
59 /// Reply code is not recognized.
60 UnknownReply(u8),
61 /// Payload bytes do not match the expected layout for this command/reply.
62 MalformedPayload {
63 /// Command or reply code.
64 code: u8,
65 /// One-line explanation.
66 reason: &'static str,
67 },
68 /// Payload length differs from the fixed length the message defines.
69 PayloadLength {
70 /// Command or reply code.
71 code: u8,
72 /// Bytes the spec mandates.
73 expected: usize,
74 /// Bytes actually delivered.
75 got: usize,
76 },
77 /// Payload is shorter than the minimum needed to parse the header.
78 PayloadTooShort {
79 /// Command or reply code.
80 code: u8,
81 /// Minimum bytes required.
82 min: usize,
83 /// Bytes actually delivered.
84 got: usize,
85 },
86 /// Record-array payload is not a positive multiple of the record size.
87 PayloadNotMultiple {
88 /// Command or reply code.
89 code: u8,
90 /// Per-record block size in bytes.
91 block: usize,
92 /// Bytes actually delivered.
93 got: usize,
94 },
95 /// Multi-part receiver detected an invariant violation.
96 Multipart(MultipartError),
97 /// Secure-channel state machine rejected an event.
98 SecureSession(SecureSessionError),
99 /// Transport-level I/O.
100 Io(&'static str),
101 /// Reply did not arrive within the configured budget.
102 Timeout,
103 /// PD declared off-line.
104 Offline,
105 /// Address mismatch between expected PD and reply ADDR.
106 AddrMismatch {
107 /// Address we sent to.
108 sent: u8,
109 /// Address echoed back in the reply.
110 got: u8,
111 },
112 /// Reply carried a sequence number we did not request. Per spec §5.7 /
113 /// Table 2 the PD must echo the ACU's SQN; a mismatch typically means a
114 /// stale reply from a desynchronised PD.
115 SqnMismatch {
116 /// SQN the ACU sent in the prompting command.
117 expected: u8,
118 /// SQN observed in the reply.
119 got: u8,
120 },
121 /// PD answered with [`crate::reply::Nak`].
122 Nak {
123 /// Error code byte from the reply.
124 code: u8,
125 },
126 /// Output buffer was too small.
127 BufferOverflow {
128 /// Minimum required capacity.
129 need: usize,
130 /// Capacity available.
131 have: usize,
132 },
133}
134
135/// Subcategory of multi-part assembler errors.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137#[non_exhaustive]
138pub enum MultipartError {
139 /// First fragment did not arrive at offset 0.
140 UnexpectedFirstOffset(u16),
141 /// Out-of-order or overlapping fragment.
142 OutOfOrderOffset {
143 /// Expected next offset.
144 expected: u16,
145 /// Offset declared by the fragment we received.
146 got: u16,
147 },
148 /// `MpSizeTotal` differs from the figure in a previous fragment.
149 InconsistentTotal {
150 /// First fragment's stated total.
151 first: u16,
152 /// New fragment's stated total.
153 now: u16,
154 },
155 /// Final fragment overruns the declared total.
156 OverflowsTotal,
157 /// `MpFragmentSize` does not match the actual payload.
158 BadFragmentSize,
159 /// Counterparty aborted the transfer.
160 Aborted,
161}
162
163/// Subcategory of secure-channel state errors.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165#[non_exhaustive]
166pub enum SecureSessionError {
167 /// Cryptogram comparison failed.
168 BadCryptogram,
169 /// Caller attempted to wrap a frame before SCS was fully established.
170 NotSecure,
171 /// SCS event arrived in a state that does not accept it.
172 BadTransition,
173 /// Encrypted DATA must be padded to a 16-byte multiple.
174 BadPadding,
175}
176
177impl fmt::Display for Error {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self {
180 Error::Truncated { have, need } => {
181 write!(f, "truncated: have {have} bytes, need {need}")
182 }
183 Error::BadSom(b) => write!(f, "bad SOM byte: {b:#04x}"),
184 Error::BadLength { declared, actual } => {
185 write!(f, "length mismatch: declared {declared}, actual {actual}")
186 }
187 Error::BadControlByte(b) => write!(f, "bad control byte: {b:#04x}"),
188 Error::BadCrc { got, want } => write!(f, "bad CRC: got {got:#06x}, want {want:#06x}"),
189 Error::BadChecksum { got, want } => {
190 write!(f, "bad checksum: got {got:#04x}, want {want:#04x}")
191 }
192 Error::BadAddress(b) => write!(f, "bad address: {b:#04x}"),
193 Error::BadSqn(b) => write!(f, "bad sequence number: {b}"),
194 Error::BadSecurityBlock(b) => write!(f, "unknown SCB type: {b:#04x}"),
195 Error::BadSecurityBlockLength(b) => write!(f, "bad SCB length for type {b:#04x}"),
196 Error::BadMac => f.write_str("bad MAC"),
197 Error::ShortMac => f.write_str("MAC too short"),
198 Error::UnknownCommand(c) => write!(f, "unknown command code: {c:#04x}"),
199 Error::UnknownReply(c) => write!(f, "unknown reply code: {c:#04x}"),
200 Error::MalformedPayload { code, reason } => {
201 write!(f, "malformed payload for {code:#04x}: {reason}")
202 }
203 Error::PayloadLength {
204 code,
205 expected,
206 got,
207 } => {
208 write!(
209 f,
210 "payload length for {code:#04x}: expected {expected}, got {got}"
211 )
212 }
213 Error::PayloadTooShort { code, min, got } => {
214 write!(
215 f,
216 "payload for {code:#04x} too short: need at least {min}, got {got}"
217 )
218 }
219 Error::PayloadNotMultiple { code, block, got } => {
220 write!(
221 f,
222 "payload length for {code:#04x}: {got} is not a positive multiple of {block}"
223 )
224 }
225 Error::Multipart(e) => write!(f, "multipart: {e:?}"),
226 Error::SecureSession(e) => write!(f, "secure session: {e:?}"),
227 Error::Io(s) => write!(f, "io: {s}"),
228 Error::Timeout => f.write_str("reply timed out"),
229 Error::Offline => f.write_str("PD declared off-line"),
230 Error::AddrMismatch { sent, got } => {
231 write!(f, "address mismatch: sent {sent:#04x}, got {got:#04x}")
232 }
233 Error::SqnMismatch { expected, got } => {
234 write!(f, "SQN mismatch: expected {expected}, got {got}")
235 }
236 Error::Nak { code } => write!(f, "PD replied NAK ({code:#04x})"),
237 Error::BufferOverflow { need, have } => {
238 write!(f, "buffer overflow: need {need}, have {have}")
239 }
240 }
241 }
242}
243
244impl core::error::Error for Error {}
245
246impl From<MultipartError> for Error {
247 fn from(e: MultipartError) -> Self {
248 Error::Multipart(e)
249 }
250}
251
252impl From<SecureSessionError> for Error {
253 fn from(e: SecureSessionError) -> Self {
254 Error::SecureSession(e)
255 }
256}