Skip to main content

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}