lora_packet/error.rs
1//! Crate-wide error type.
2//!
3//! Every fallible public function returns [`Result<T>`], which is an alias for
4//! `core::result::Result<T, Error>`. The [`Error`] enum carries enough context
5//! (lengths, key roles, field names) to diagnose failures without re-parsing
6//! strings. Match on the variant, not on the `Display` output.
7
8use alloc::string::String;
9
10/// Every error this crate can produce.
11///
12/// Variants split into three rough buckets:
13/// 1. **Parsing** ([`Error::TooShort`], [`Error::TooLong`],
14/// [`Error::InvalidMType`], [`Error::InvalidMajor`],
15/// [`Error::InvalidRejoinType`], [`Error::ConflictingMacCommands`],
16/// [`Error::FOptsTooLong`], [`Error::InvalidJoinAcceptLength`]).
17/// 2. **Construction** ([`Error::InvalidKeyLength`],
18/// [`Error::InvalidIdentifierLength`], [`Error::MissingField`],
19/// [`Error::PayloadTooLarge`]).
20/// 3. **Crypto / MIC** ([`Error::MicMismatch`], [`Error::MissingKey`],
21/// [`Error::UnsupportedForVersion`]).
22///
23/// The `Other`, `Hex`, and `Base64` variants exist for boundary conversions
24/// and should not need to be matched in normal code paths.
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27 /// Wire buffer ran out before the expected structure was complete.
28 ///
29 /// Both `expected` and `got` are in bytes. Always inspect both fields; the
30 /// `expected` value differs by message type (5 for the minimum
31 /// `PHYPayload`, 18 for a Join Request body, etc.).
32 #[error("invalid wire format: expected at least {expected} bytes, got {got}")]
33 TooShort {
34 /// Required minimum length.
35 expected: usize,
36 /// Actual length provided.
37 got: usize,
38 },
39
40 /// Wire buffer exceeded the `LoRaWAN` PHY maximum of 256 bytes.
41 ///
42 /// PHY payload size varies by region but never exceeds 256 bytes total in
43 /// any `LoRaWAN` regional plan. Beyond this limit, the 1-byte length field
44 /// in CMAC B0/B1 blocks would silently wrap and produce a deterministic
45 /// but wrong MIC. Reject the input early.
46 #[error("invalid wire format: {got} bytes exceeds maximum of 256")]
47 TooLong {
48 /// Actual length provided.
49 got: usize,
50 },
51
52 /// `MType` field in the MHDR did not match any known value.
53 ///
54 /// All 3-bit patterns are currently defined by the `LoRaWAN` spec, so this
55 /// variant is reserved for forward compatibility.
56 #[error("invalid MType: {0:#05b}")]
57 InvalidMType(u8),
58
59 /// Major version field in the MHDR was not zero (the only defined value).
60 #[error("invalid major version: {0:#04b}")]
61 InvalidMajor(u8),
62
63 /// Rejoin Request type was not 0, 1, or 2.
64 #[error("invalid rejoin type: {0}")]
65 InvalidRejoinType(u8),
66
67 /// `FRMPayload` present with `FPort = 0` alongside non-empty `FOpts`.
68 ///
69 /// The `LoRaWAN` MAC spec forbids carrying MAC commands in both places at
70 /// once.
71 #[error("FOpts and FPort=0 cannot both carry MAC commands")]
72 ConflictingMacCommands,
73
74 /// `FOpts` exceeds the 15-byte maximum encoded in `FCtrl.FOptsLen`.
75 ///
76 /// Returned by [`crate::LoraPacketBuilder::build_unsigned`] when the
77 /// builder's `f_opts` vector is too long; not raised by wire parsing
78 /// because `FCtrl` only carries 4 bits of length.
79 #[error("FOpts length {0} exceeds maximum of 15")]
80 FOptsTooLong(usize),
81
82 /// A key slice supplied to `from_slice` had the wrong length.
83 ///
84 /// All `LoRaWAN` keys are 16 bytes (AES-128).
85 #[error("expected key length {expected}, got {got}")]
86 InvalidKeyLength {
87 /// Required length.
88 expected: usize,
89 /// Actual slice length.
90 got: usize,
91 },
92
93 /// An identifier slice supplied to `from_slice` had the wrong length.
94 ///
95 /// Lengths per identifier: `DevAddr` = 4, `NetId` = 3, `DevEui` /
96 /// `AppEui` = 8, `DevNonce` = 2, `AppNonce` = 3.
97 #[error("expected identifier length {expected}, got {got}")]
98 InvalidIdentifierLength {
99 /// Required length.
100 expected: usize,
101 /// Actual slice length.
102 got: usize,
103 },
104
105 /// MIC verification failed.
106 ///
107 /// The compare is constant-time (via `subtle::ConstantTimeEq`). Treat this
108 /// as a security event, not a parse error.
109 #[error("MIC mismatch")]
110 MicMismatch,
111
112 /// A MIC or crypto operation required a key that was not supplied.
113 ///
114 /// The string argument names the missing role (e.g. `"nwk_s_key required
115 /// for Data MIC"`).
116 #[error("missing key for operation: {0}")]
117 MissingKey(&'static str),
118
119 /// An API call was made on a message type the chosen `LoRaWAN` version does
120 /// not support.
121 ///
122 /// Example: `calculate_mic_v1_0` called on a Rejoin Request or Proprietary
123 /// frame (both 1.1-only). The string names which API to call instead.
124 #[error("operation not supported for this message type in this LoRaWAN version: {0}")]
125 UnsupportedForVersion(&'static str),
126
127 /// Builder finalisation failed because a required field was not set.
128 ///
129 /// The string argument names the field (`"dev_addr"`, `"join_eui"`, etc.).
130 /// Match on the field name to suggest a builder method to set it.
131 #[error("required builder field not set: {0}")]
132 MissingField(&'static str),
133
134 /// `FRMPayload` exceeded the AES-CTR block-index limit (255 blocks =
135 /// 4080 bytes).
136 ///
137 /// Beyond this size, the 1-byte block counter in the keystream block would
138 /// overflow and silently produce ciphertext no other `LoRaWAN` stack can
139 /// decrypt.
140 #[error("payload too large: {0} bytes")]
141 PayloadTooLarge(usize),
142
143 /// Join Accept ciphertext had a length outside the valid range.
144 ///
145 /// A Join Accept is one AES block (17 bytes total: MHDR + 1 block + MIC)
146 /// or two blocks with a `CFList` (33 bytes total).
147 #[error("invalid Join Accept length: {0} bytes (expected 17 or 33)")]
148 InvalidJoinAcceptLength(usize),
149
150 /// Catch-all carrying a free-form message. Used sparingly for situations
151 /// no other variant fits; downstream code should not match on the string.
152 #[error("{0}")]
153 Other(String),
154
155 /// Hex decoding failed (only with the `hex_base64` feature).
156 #[cfg(feature = "hex_base64")]
157 #[error("hex decode error: {0}")]
158 Hex(hex::FromHexError),
159
160 /// Base64 decoding failed (only with the `hex_base64` feature).
161 #[cfg(feature = "hex_base64")]
162 #[error("base64 decode error: {0}")]
163 Base64(base64::DecodeError),
164}
165
166#[cfg(feature = "hex_base64")]
167impl From<hex::FromHexError> for Error {
168 fn from(e: hex::FromHexError) -> Self {
169 Self::Hex(e)
170 }
171}
172
173#[cfg(feature = "hex_base64")]
174impl From<base64::DecodeError> for Error {
175 fn from(e: base64::DecodeError) -> Self {
176 Self::Base64(e)
177 }
178}
179
180/// Convenience alias for `core::result::Result<T, Error>`.
181pub type Result<T> = core::result::Result<T, Error>;
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use alloc::string::ToString;
187
188 #[test]
189 fn error_display_includes_context() {
190 let e = Error::TooShort { expected: 12, got: 7 };
191 assert_eq!(e.to_string(), "invalid wire format: expected at least 12 bytes, got 7");
192 }
193
194 #[test]
195 fn invalid_mtype_format() {
196 let e = Error::InvalidMType(0b111);
197 assert_eq!(e.to_string(), "invalid MType: 0b111");
198 }
199
200 #[test]
201 fn result_alias_works() {
202 #[allow(clippy::unnecessary_wraps)]
203 fn ok() -> Result<u8> {
204 Ok(42)
205 }
206 fn err() -> Result<u8> {
207 Err(Error::MicMismatch)
208 }
209 assert_eq!(ok().unwrap(), 42);
210 assert!(err().is_err());
211 }
212}