Skip to main content

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}