bacnet_types/error/decode.rs
1//! Errors without an allocated message (#1646): [`DecodeError`], which a
2//! decoder returns as [`Error::Decoding`](super::Error::Decoding), and
3//! [`RangeError`], which a checked constructor returns as
4//! [`Error::Range`](super::Error::Range).
5//!
6//! Each holds `&'static str` labels and up to two numbers, and builds its
7//! text only when it is displayed. A firmware image that never displays one
8//! links no formatting code for it, and every build, the hosted ones
9//! included, reads the same text: the label with each placeholder in it
10//! replaced by the next number:
11//!
12//! - `{}`: the number in decimal;
13//! - `{:#x}`: the number in hexadecimal, lower case, without padding;
14//! - `{:#04X}`: an octet in hexadecimal, upper case and zero-padded to two
15//! digits, such as `0x0B` (#1803); a wider number prints all its digits;
16//! - `{t}`: the name of the application datatype the number is the tag of
17//! (Clause 20.2.1.4), such as `Unsigned` for 2. The names live in the
18//! display code, so one label serves every type.
19
20use core::fmt;
21
22use crate::enums::RejectReason;
23
24/// Which fault a decoder found in the data it refused, so a responder can
25/// name it (#1446). [`DecodingKind::reject_reason`] gives the Clause 18.9
26/// Reject reason each kind draws when the data was a confirmed request.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum DecodingKind {
29 /// Contents whose encoding isn't valid for their datatype: a wrong
30 /// length, an Unsigned of no octets or with padding a canonical field
31 /// refuses, an unknown character set, a BIT STRING's unused-bit count
32 /// past 7, and the like. The kind [`Error::decoding`](super::Error::decoding)
33 /// makes.
34 InvalidEncoding,
35 /// A value too large for its field's width, or outside the range the
36 /// production or service gives it.
37 OutOfRange,
38 /// More than the decoder takes: a count of items past its limit, or a tag
39 /// length past its sanity bound.
40 Overflow,
41 /// A tag that doesn't fit where it stands: another number, class or form
42 /// than the member there needs, a closing tag that closes no open frame,
43 /// a tag header that is itself malformed, or an opening tag nested
44 /// deeper than the decoder walks.
45 InvalidTag,
46 /// The data ends, or the enclosing frame closes, where a required member
47 /// or a frame's closing tag is due: a member is missing.
48 Missing,
49 /// Octets are left after the last member of a value that must fill its
50 /// input: arguments beyond those the production has.
51 Trailing,
52 /// A well-formed encoding the decoder doesn't handle: a CharacterString
53 /// in a character set it can't convert (IBM/Microsoft DBCS, JIS X 0208
54 /// or UCS-4), or a segmented APDU, or one of a PDU type, that the
55 /// allocation-free APDU header doesn't read.
56 Unsupported,
57}
58
59impl DecodingKind {
60 /// The Clause 18.9 Reject reason that names this fault in a confirmed
61 /// request:
62 ///
63 /// | Kind | Reject reason |
64 /// |---|---|
65 /// | [`InvalidEncoding`](Self::InvalidEncoding) | INVALID_DATA_ENCODING |
66 /// | [`OutOfRange`](Self::OutOfRange) | PARAMETER_OUT_OF_RANGE |
67 /// | [`Overflow`](Self::Overflow) | BUFFER_OVERFLOW |
68 /// | [`InvalidTag`](Self::InvalidTag) | INVALID_TAG |
69 /// | [`Missing`](Self::Missing) | MISSING_REQUIRED_PARAMETER |
70 /// | [`Trailing`](Self::Trailing) | TOO_MANY_ARGUMENTS |
71 /// | [`Unsupported`](Self::Unsupported) | OTHER |
72 pub fn reject_reason(self) -> RejectReason {
73 match self {
74 Self::InvalidEncoding => RejectReason::INVALID_DATA_ENCODING,
75 Self::OutOfRange => RejectReason::PARAMETER_OUT_OF_RANGE,
76 Self::Overflow => RejectReason::BUFFER_OVERFLOW,
77 Self::InvalidTag => RejectReason::INVALID_TAG,
78 Self::Missing => RejectReason::MISSING_REQUIRED_PARAMETER,
79 Self::Trailing => RejectReason::TOO_MANY_ARGUMENTS,
80 Self::Unsupported => RejectReason::OTHER,
81 }
82 }
83}
84
85/// A decoder's refusal, without an allocated message (#1646).
86///
87/// It displays as `decoding error at offset {offset}: {context}: {label}`,
88/// leaving out `{context}: ` when the context is empty, and with each `{}`
89/// in the label replaced by the next of [`values`](Self::values) in decimal,
90/// each `{:#x}` in hexadecimal, or each `{:#04X}` as an upper-case,
91/// zero-padded octet such as `0x0B`. An [`index`](Self::index) follows the
92/// context as `[{index}]`.
93///
94/// ```
95/// use bacnet_types::error::{DecodeError, DecodingKind, Error};
96///
97/// let refused = DecodeError::new(DecodingKind::Trailing, 9, "{} trailing byte(s)")
98/// .within("IAm")
99/// .with_value(2);
100/// assert_eq!(refused.to_string(), "decoding error at offset 9: IAm: 2 trailing byte(s)");
101/// assert_eq!(Error::from(refused).reject_reason(), Some(DecodingKind::Trailing.reject_reason()));
102/// ```
103///
104/// It is `#[non_exhaustive]`: build one with [`new`](Self::new) and the
105/// builder methods.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107#[non_exhaustive]
108pub struct DecodeError {
109 /// Byte offset where the decoder found the fault.
110 pub offset: usize,
111 /// Which kind of fault it is.
112 pub kind: DecodingKind,
113 /// The production or member being decoded, such as `"WhoIs low limit"`;
114 /// empty when the decoder names none.
115 pub context: &'static str,
116 /// What is wrong, such as `"expected context tag [{}]"`.
117 pub label: &'static str,
118 /// The numbers the label's placeholders stand for, in order; a slot with
119 /// no placeholder is 0.
120 pub values: [u64; 2],
121 /// The zero-based position of the list item the fault is in, such as a
122 /// file record, when the decoder names one (#1776). It sits in the
123 /// struct's padding, so the error is no larger for it.
124 pub index: Option<u16>,
125}
126
127impl DecodeError {
128 /// A fault of `kind` at `offset`, described by `label`.
129 pub const fn new(kind: DecodingKind, offset: usize, label: &'static str) -> Self {
130 Self {
131 offset,
132 kind,
133 context: "",
134 label,
135 values: [0; 2],
136 index: None,
137 }
138 }
139
140 /// The same fault, met decoding `context`.
141 #[must_use]
142 pub const fn within(mut self, context: &'static str) -> Self {
143 self.context = context;
144 self
145 }
146
147 /// The same fault, in the list item at `index`.
148 #[must_use]
149 pub const fn at_item(mut self, index: u16) -> Self {
150 self.index = Some(index);
151 self
152 }
153
154 /// The same fault, with the number the label's one placeholder stands
155 /// for.
156 #[must_use]
157 pub const fn with_value(mut self, value: u64) -> Self {
158 self.values = [value, 0];
159 self
160 }
161
162 /// The same fault, with the numbers the label's two placeholders stand
163 /// for.
164 #[must_use]
165 pub const fn with_values(mut self, first: u64, second: u64) -> Self {
166 self.values = [first, second];
167 self
168 }
169
170 /// The context and label with their numbers, without the
171 /// `decoding error at offset {offset}: ` prefix.
172 pub fn message(&self) -> impl fmt::Display + '_ {
173 DecodeText(self)
174 }
175}
176
177impl fmt::Display for DecodeError {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 write!(f, "decoding error at offset {}: ", self.offset)?;
180 fmt::Display::fmt(&self.message(), f)
181 }
182}
183
184impl core::error::Error for DecodeError {}
185
186/// [`DecodeError::message`].
187struct DecodeText<'a>(&'a DecodeError);
188
189impl fmt::Display for DecodeText<'_> {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 let error = self.0;
192 if !error.context.is_empty() || error.index.is_some() {
193 f.write_str(error.context)?;
194 if let Some(index) = error.index {
195 write!(f, "[{index}]")?;
196 }
197 f.write_str(": ")?;
198 }
199 write_label(f, error.label, error.values)
200 }
201}
202
203/// A value a checked constructor refused for lying outside the range its
204/// type takes, such as an object instance past 4,194,303 (#1646).
205///
206/// Like [`DecodeError`] it carries a static label and up to two numbers,
207/// formatted when displayed. It displays as `value out of range: {label}`.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
209pub struct RangeError {
210 /// What is out of range, such as `"instance number {} exceeds max 4194303"`.
211 pub label: &'static str,
212 /// The numbers the label's placeholders stand for, in order; a slot with
213 /// no placeholder is 0.
214 pub values: [u64; 2],
215}
216
217impl RangeError {
218 /// A refusal described by `label`.
219 pub const fn new(label: &'static str) -> Self {
220 Self {
221 label,
222 values: [0; 2],
223 }
224 }
225
226 /// The same refusal, with the number the label's one placeholder stands
227 /// for.
228 #[must_use]
229 pub const fn with_value(mut self, value: u64) -> Self {
230 self.values = [value, 0];
231 self
232 }
233
234 /// The same refusal, with the numbers the label's two placeholders stand
235 /// for.
236 #[must_use]
237 pub const fn with_values(mut self, first: u64, second: u64) -> Self {
238 self.values = [first, second];
239 self
240 }
241
242 /// The label with its numbers, without the `value out of range: ` prefix.
243 pub fn message(&self) -> impl fmt::Display + '_ {
244 Message(self)
245 }
246}
247
248impl fmt::Display for RangeError {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.write_str("value out of range: ")?;
251 write_label(f, self.label, self.values)
252 }
253}
254
255impl core::error::Error for RangeError {}
256
257/// [`RangeError::message`].
258struct Message<'a>(&'a RangeError);
259
260impl fmt::Display for Message<'_> {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 write_label(f, self.0.label, self.0.values)
263 }
264}
265
266/// The name of the application datatype tag `number` stands for (Clause
267/// 20.2.1.4), as the decoders' labels call it.
268fn datatype_name(number: u64) -> &'static str {
269 const NAMES: [&str; 13] = [
270 "NULL",
271 "BOOLEAN",
272 "Unsigned",
273 "INTEGER",
274 "REAL",
275 "Double",
276 "OCTET STRING",
277 "CharacterString",
278 "BIT STRING",
279 "ENUMERATED",
280 "Date",
281 "Time",
282 "BACnetObjectIdentifier",
283 ];
284 usize::try_from(number)
285 .ok()
286 .and_then(|number| NAMES.get(number))
287 .copied()
288 .unwrap_or("value of a reserved type")
289}
290
291/// Write `label` with its placeholders filled from `values` in order (see
292/// the module docs). A placeholder past the second is written as it stands.
293pub(super) fn write_label(
294 f: &mut fmt::Formatter<'_>,
295 label: &str,
296 values: [u64; 2],
297) -> fmt::Result {
298 let mut rest = label;
299 let mut next = values.iter();
300 while let Some(at) = rest.find('{') {
301 let (before, from) = rest.split_at(at);
302 f.write_str(before)?;
303 let placeholder = ["{}", "{:#x}", "{:#04X}", "{t}"]
304 .into_iter()
305 .find(|placeholder| from.starts_with(placeholder));
306 match placeholder.zip(next.clone().next()) {
307 Some((placeholder, &value)) => {
308 next.next();
309 match placeholder {
310 "{:#x}" => write!(f, "{value:#x}")?,
311 "{:#04X}" => write!(f, "{value:#04X}")?,
312 "{t}" => f.write_str(datatype_name(value))?,
313 _ => write!(f, "{value}")?,
314 }
315 rest = &from[placeholder.len()..];
316 }
317 None => {
318 let placeholder = placeholder.unwrap_or("{");
319 f.write_str(placeholder)?;
320 rest = &from[placeholder.len()..];
321 }
322 }
323 }
324 f.write_str(rest)
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn labels_fill_their_placeholders_in_order() {
333 let display = |label, values: [u64; 2]| {
334 let mut refused = DecodeError::new(DecodingKind::InvalidTag, 3, label);
335 refused.values = values;
336 refused.to_string()
337 };
338 assert_eq!(
339 display("closing tag {} does not match opening tag {}", [4, 2]),
340 "decoding error at offset 3: closing tag 4 does not match opening tag 2"
341 );
342 assert_eq!(
343 display("unknown PDU type nibble: {:#x}", [0xF, 0]),
344 "decoding error at offset 3: unknown PDU type nibble: 0xf"
345 );
346 // An octet pads to two upper-case digits; a wider value keeps them all.
347 assert_eq!(
348 display("type {:#04X}, function {:#04X}", [0x0B, 0x1FF]),
349 "decoding error at offset 3: type 0x0B, function 0x1FF"
350 );
351 assert_eq!(
352 display("no placeholder", [7, 8]),
353 "decoding error at offset 3: no placeholder"
354 );
355 assert_eq!(
356 display("expected application-tagged {t}, not {t}", [2, 12]),
357 "decoding error at offset 3: expected application-tagged Unsigned, \
358 not BACnetObjectIdentifier"
359 );
360 assert_eq!(
361 display("{t} and {t}", [0, 13]),
362 "decoding error at offset 3: NULL and value of a reserved type"
363 );
364 // Braces that aren't a placeholder, and a third placeholder, as written.
365 assert_eq!(
366 display("{a} {} {} {}", [1, 2]),
367 "decoding error at offset 3: {a} 1 2 {}"
368 );
369 assert_eq!(
370 DecodeError::new(DecodingKind::OutOfRange, 0, "[{}] value {} exceeds u32")
371 .within("WhoIs low limit")
372 .with_values(0, 1 << 32)
373 .to_string(),
374 "decoding error at offset 0: WhoIs low limit: [0] value 4294967296 exceeds u32"
375 );
376 assert_eq!(
377 DecodeError::new(DecodingKind::InvalidTag, 7, "expected {t}")
378 .within("record data")
379 .at_item(1)
380 .with_value(6)
381 .to_string(),
382 "decoding error at offset 7: record data[1]: expected OCTET STRING"
383 );
384 }
385
386 #[test]
387 fn range_errors_display_their_label() {
388 let refused =
389 RangeError::new("low limit {} is above its high limit {}").with_values(20, 10);
390 assert_eq!(
391 refused.to_string(),
392 "value out of range: low limit 20 is above its high limit 10"
393 );
394 assert_eq!(
395 refused.message().to_string(),
396 "low limit 20 is above its high limit 10"
397 );
398 }
399
400 /// The errors stay small enough to return by value on a firmware stack.
401 #[test]
402 fn errors_stay_small() {
403 use core::mem::size_of;
404 // 64 octets on a 64-bit host, the error type's tag in DecodeError's
405 // niche; less on a 32-bit target, whose pointers are half the size.
406 assert!(size_of::<DecodeError>() <= 64);
407 assert!(size_of::<super::super::Error>() <= 64);
408 assert!(size_of::<RangeError>() <= 32);
409 assert!(size_of::<super::super::EncodeError>() <= 56);
410 assert!(size_of::<super::super::SegmentationError>() <= 24);
411 }
412}
413
414// On a 32-bit target the error is 48 octets: the other variants fit beside
415// DecodeError's niche, so the enum needs no tag of its own. A variant that
416// outgrows that space adds one, which the firmware decoders pay for
417// (#1802), so check-no-std's builds refuse it here.
418#[cfg(target_pointer_width = "32")]
419const _: () = assert!(core::mem::size_of::<super::Error>() == 48);