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    /// Multi-part receiver detected an invariant violation.
69    Multipart(MultipartError),
70    /// Secure-channel state machine rejected an event.
71    SecureSession(SecureSessionError),
72    /// Transport-level I/O.
73    Io(&'static str),
74    /// Reply did not arrive within the configured budget.
75    Timeout,
76    /// PD declared off-line.
77    Offline,
78    /// Address mismatch between expected PD and reply ADDR.
79    AddrMismatch {
80        /// Address we sent to.
81        sent: u8,
82        /// Address echoed back in the reply.
83        got: u8,
84    },
85    /// PD answered with [`crate::reply::Nak`].
86    Nak {
87        /// Error code byte from the reply.
88        code: u8,
89    },
90    /// Output buffer was too small.
91    BufferOverflow {
92        /// Minimum required capacity.
93        need: usize,
94        /// Capacity available.
95        have: usize,
96    },
97}
98
99/// Subcategory of multi-part assembler errors.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum MultipartError {
103    /// First fragment did not arrive at offset 0.
104    UnexpectedFirstOffset(u16),
105    /// Out-of-order or overlapping fragment.
106    OutOfOrderOffset {
107        /// Expected next offset.
108        expected: u16,
109        /// Offset declared by the fragment we received.
110        got: u16,
111    },
112    /// `MpSizeTotal` differs from the figure in a previous fragment.
113    InconsistentTotal {
114        /// First fragment's stated total.
115        first: u16,
116        /// New fragment's stated total.
117        now: u16,
118    },
119    /// Final fragment overruns the declared total.
120    OverflowsTotal,
121    /// `MpFragmentSize` does not match the actual payload.
122    BadFragmentSize,
123    /// Counterparty aborted the transfer.
124    Aborted,
125}
126
127/// Subcategory of secure-channel state errors.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum SecureSessionError {
131    /// Cryptogram comparison failed.
132    BadCryptogram,
133    /// Caller attempted to wrap a frame before SCS was fully established.
134    NotSecure,
135    /// SCS event arrived in a state that does not accept it.
136    BadTransition,
137    /// Encrypted DATA must be padded to a 16-byte multiple.
138    BadPadding,
139}
140
141impl fmt::Display for Error {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            Error::Truncated { have, need } => {
145                write!(f, "truncated: have {have} bytes, need {need}")
146            }
147            Error::BadSom(b) => write!(f, "bad SOM byte: {b:#04x}"),
148            Error::BadLength { declared, actual } => {
149                write!(f, "length mismatch: declared {declared}, actual {actual}")
150            }
151            Error::BadControlByte(b) => write!(f, "bad control byte: {b:#04x}"),
152            Error::BadCrc { got, want } => write!(f, "bad CRC: got {got:#06x}, want {want:#06x}"),
153            Error::BadChecksum { got, want } => {
154                write!(f, "bad checksum: got {got:#04x}, want {want:#04x}")
155            }
156            Error::BadAddress(b) => write!(f, "bad address: {b:#04x}"),
157            Error::BadSqn(b) => write!(f, "bad sequence number: {b}"),
158            Error::BadSecurityBlock(b) => write!(f, "unknown SCB type: {b:#04x}"),
159            Error::BadSecurityBlockLength(b) => write!(f, "bad SCB length for type {b:#04x}"),
160            Error::BadMac => f.write_str("bad MAC"),
161            Error::ShortMac => f.write_str("MAC too short"),
162            Error::UnknownCommand(c) => write!(f, "unknown command code: {c:#04x}"),
163            Error::UnknownReply(c) => write!(f, "unknown reply code: {c:#04x}"),
164            Error::MalformedPayload { code, reason } => {
165                write!(f, "malformed payload for {code:#04x}: {reason}")
166            }
167            Error::Multipart(e) => write!(f, "multipart: {e:?}"),
168            Error::SecureSession(e) => write!(f, "secure session: {e:?}"),
169            Error::Io(s) => write!(f, "io: {s}"),
170            Error::Timeout => f.write_str("reply timed out"),
171            Error::Offline => f.write_str("PD declared off-line"),
172            Error::AddrMismatch { sent, got } => {
173                write!(f, "address mismatch: sent {sent:#04x}, got {got:#04x}")
174            }
175            Error::Nak { code } => write!(f, "PD replied NAK ({code:#04x})"),
176            Error::BufferOverflow { need, have } => {
177                write!(f, "buffer overflow: need {need}, have {have}")
178            }
179        }
180    }
181}
182
183impl core::error::Error for Error {}
184
185impl From<MultipartError> for Error {
186    fn from(e: MultipartError) -> Self {
187        Error::Multipart(e)
188    }
189}
190
191impl From<SecureSessionError> for Error {
192    fn from(e: SecureSessionError) -> Self {
193        Error::SecureSession(e)
194    }
195}