bacnet_types/error.rs
1//! BACnet error types.
2//!
3//! Provides the top-level [`Error`] type used throughout the library,
4//! covering protocol errors, encoding/decoding failures, transport issues,
5//! and timeouts.
6//!
7//! Without `alloc` it keeps only the variants that own nothing: a decoder's
8//! [`DecodeError`], an encoder's [`EncodeError`], a constructor's
9//! [`RangeError`] and a segmenter's [`SegmentationError`] carry static
10//! labels or numbers and build their text when displayed (#1646, #1802).
11//! The variants holding a `String` or a `Box` need `alloc`.
12
13#[cfg(all(feature = "alloc", not(feature = "std")))]
14use alloc::{boxed::Box, string::String, vec::Vec};
15use core::fmt;
16use core::time::Duration;
17
18#[cfg(feature = "alloc")]
19use crate::constructed::BACnetObjectPropertyReference;
20use crate::data_link::DataLink;
21#[cfg(feature = "alloc")]
22use crate::enums::PropertyIdentifier;
23use crate::enums::{AbortReason, ErrorClass, ErrorCode, RejectReason};
24
25mod decode;
26mod encode;
27pub use decode::{DecodeError, DecodingKind, RangeError};
28pub use encode::{EncodeError, SegmentationError};
29
30/// How a protocol error's class and code read: each name in lower case with
31/// hyphens, or the raw number for a value without one.
32struct ProtocolText {
33 class: ErrorClass,
34 code: ErrorCode,
35}
36
37impl fmt::Display for ProtocolText {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 fn name(
40 f: &mut fmt::Formatter<'_>,
41 named: Option<&str>,
42 field: &str,
43 value: u16,
44 ) -> fmt::Result {
45 let Some(named) = named else {
46 return write!(f, "{field}={value}");
47 };
48 for c in named.chars() {
49 let c = if c == '_' {
50 '-'
51 } else {
52 c.to_ascii_lowercase()
53 };
54 fmt::Write::write_char(f, c)?;
55 }
56 Ok(())
57 }
58 f.write_str("BACnet error: ")?;
59 name(f, self.class.name(), "class", self.class.to_raw())?;
60 f.write_str(" / ")?;
61 name(f, self.code.name(), "code", self.code.to_raw())
62 }
63}
64
65/// [`Error::LogNotAdvancing`]'s text.
66struct NotAdvancing {
67 requested: u64,
68 returned: Option<u64>,
69}
70
71impl fmt::Display for NotAdvancing {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 let requested = self.requested;
74 match self.returned {
75 Some(returned) => write!(
76 f,
77 "asked for sequence number {requested}, the device answered from {returned}"
78 ),
79 None => write!(
80 f,
81 "asked for sequence number {requested}, which the log's counts say it holds, \
82 the device answered with no records"
83 ),
84 }
85 }
86}
87
88/// Top-level error type for the BACnet library.
89///
90/// It is `#[non_exhaustive]`, so a `match` on it outside this crate needs a
91/// wildcard arm. Some variants exist only with a feature, and any crate in a
92/// build can turn that on: `Error::Transport` needs `std`, and the variants
93/// that hold a `String` or a `Box` need `alloc`. Without the attribute a
94/// match written against a smaller build would stop compiling (#1604).
95#[derive(Debug, thiserror::Error)]
96#[non_exhaustive]
97// Clippy fails if the attribute above goes.
98#[deny(clippy::exhaustive_enums)]
99pub enum Error {
100 /// BACnet protocol error response (Clause 20.1.7).
101 #[error("{}", ProtocolText { class: *.class, code: *.code })]
102 Protocol {
103 /// Error class (Clause 18).
104 class: ErrorClass,
105 /// Error code (Clause 18).
106 code: ErrorCode,
107 },
108
109 /// BACnet error response whose Clause 21 production carries more than the
110 /// class and code: ChangeList-Error, CreateObject-Error,
111 /// WritePropertyMultiple-Error, SubscribeCOVPropertyMultiple-Error,
112 /// ConfirmedPrivateTransfer-Error or VTClose-Error. `detail` holds the
113 /// extra fields; a body whose optional fields are all absent is reported
114 /// as [`Error::Protocol`]. Only with `alloc`.
115 #[cfg(feature = "alloc")]
116 #[error("{}, {detail}", ProtocolText { class: *.class, code: *.code })]
117 Structured {
118 /// Error class (Clause 18).
119 class: ErrorClass,
120 /// Error code (Clause 18).
121 code: ErrorCode,
122 /// The production's fields after the class and code. Boxed so the
123 /// rare structured error does not widen every `Result<_, Error>`.
124 detail: Box<ErrorDetail>,
125 },
126
127 /// BACnet reject PDU (Clause 20.1.5).
128 #[error("BACnet reject: reason={}", reason.to_raw())]
129 Reject {
130 /// Reject reason (Clause 18.9).
131 reason: RejectReason,
132 },
133
134 /// BACnet abort PDU (Clause 20.1.6).
135 #[error("BACnet abort: reason={}", reason.to_raw())]
136 Abort {
137 /// Abort reason (Clause 18.10).
138 reason: AbortReason,
139 },
140
141 /// A router reported that the active message was too long for a routed path.
142 #[error("message is too long for routed path to DNET {dnet}")]
143 RoutedPathTooLong {
144 /// Destination network rejected by the router.
145 dnet: u16,
146 },
147
148 /// The client cannot safely allocate state for another routed path.
149 #[error("routed path state capacity of {capacity} entries is exhausted")]
150 RoutedPathCapacityExceeded {
151 /// Maximum number of immediate-router/DNET path entries retained.
152 capacity: usize,
153 },
154
155 /// The endpoint's transport cannot carry the requested operation, such as
156 /// a BBMD management request through a data link other than BACnet/IP.
157 /// Nothing was sent.
158 #[error("operation requires {required}; this transport is {actual}")]
159 UnsupportedTransport {
160 /// Data link the operation needs.
161 required: DataLink,
162 /// Data link the endpoint's transport carries.
163 actual: DataLink,
164 },
165
166 /// A definite length refusal before any bytes of this send were written
167 /// or admitted to a transport's write queue. The limit may apply to the
168 /// NPDU or its full link envelope, including data attributes. An I/O
169 /// failure, including an uncertain partial write, must not use this.
170 #[error("send refused before transmission: {length} octets exceed the {limit}-octet limit")]
171 SendTooLong {
172 /// Length of the NPDU or encoded link frame that was refused.
173 length: usize,
174 /// The corresponding bound when this send was refused.
175 limit: usize,
176 },
177
178 /// Error encoding a PDU, with a formatted message. Only with `alloc`;
179 /// [`Error::Encode`] is the static form, which the codec crates return.
180 #[cfg(feature = "alloc")]
181 #[error("encoding error: {0}")]
182 Encoding(String),
183
184 /// A value an encoder or validator refused, with a static label
185 /// (#1802). The encoders and validators of `bacnet-encoding` and
186 /// `bacnet-services` report this form.
187 #[error("{0}")]
188 Encode(EncodeError),
189
190 /// Error decoding received data, with a static label and its numbers
191 /// (#1646, #1776). Every decoder reports this form.
192 #[error("{0}")]
193 Decoding(DecodeError),
194
195 /// Transport-level I/O error. Only with the `std` feature, which has
196 /// `std::io`.
197 #[cfg(feature = "std")]
198 #[error("transport error: {0}")]
199 Transport(#[from] std::io::Error),
200
201 /// Request timed out.
202 #[error("request timed out after {0:?}")]
203 Timeout(Duration),
204
205 /// A payload that can't be segmented, segments that can't be
206 /// reassembled, or a segmented request a peer can't take (#1802).
207 #[error("segmentation error: {0}")]
208 Segmentation(SegmentationError),
209
210 /// Buffer too short for the expected data.
211 #[error("buffer too short: need {need} bytes, have {have}")]
212 BufferTooShort {
213 /// Minimum bytes needed.
214 need: usize,
215 /// Bytes actually available.
216 have: usize,
217 },
218
219 /// Value out of valid range, with a formatted message. Only with
220 /// `alloc`; [`Error::Range`] is the static form.
221 #[cfg(feature = "alloc")]
222 #[error("value out of range: {0}")]
223 OutOfRange(String),
224
225 /// A value a checked constructor refused, with a static label (#1646),
226 /// such as an object type or instance
227 /// [`ObjectIdentifier::new`](crate::primitives::ObjectIdentifier::new)
228 /// can't encode.
229 #[error("{0}")]
230 Range(RangeError),
231
232 /// A well-formed ReadRange acknowledgement that breaks a rule the client
233 /// checks against the request it answers, so the client refused it. The
234 /// acknowledgement decoded; a malformed one is [`Error::Decoding`]
235 /// instead. Read again leniently to keep its items.
236 #[error("ReadRange acknowledgement refused: {0}")]
237 ReadRangeViolation(ReadRangeViolation),
238
239 /// A paged log read asked for the records from one sequence number and
240 /// the device answered with a page starting before it, so reading on
241 /// would repeat records instead of advancing; or answered with none
242 /// although its record counts say it holds that record. Some devices
243 /// clamp a reference past their sequence wrap back to the oldest record,
244 /// or number the record after the wrap 0; read such a log by position
245 /// instead.
246 #[error(
247 "log read is not advancing: {}",
248 NotAdvancing { requested: *.requested, returned: *.returned }
249 )]
250 LogNotAdvancing {
251 /// The sequence number the read asked for.
252 requested: u64,
253 /// What the device numbered the first record it sent;
254 /// `None` when it returned no records.
255 returned: Option<u64>,
256 },
257}
258
259impl From<DecodeError> for Error {
260 fn from(error: DecodeError) -> Self {
261 Self::Decoding(error)
262 }
263}
264
265impl From<EncodeError> for Error {
266 fn from(error: EncodeError) -> Self {
267 Self::Encode(error)
268 }
269}
270
271impl From<SegmentationError> for Error {
272 fn from(error: SegmentationError) -> Self {
273 Self::Segmentation(error)
274 }
275}
276
277impl From<RangeError> for Error {
278 fn from(error: RangeError) -> Self {
279 Self::Range(error)
280 }
281}
282
283/// A rule a ReadRange acknowledgement can break against the request it
284/// answers (Clause 15.8).
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
286pub enum ReadRangeViolation {
287 /// The object identifier isn't the one the request named.
288 ObjectMismatch,
289 /// The property identifier isn't the one the request named.
290 PropertyMismatch,
291 /// The array index isn't the one the request named, or is present when
292 /// the request had none, or the reverse.
293 ArrayIndexMismatch,
294 /// A nonempty answer to a By-Sequence or By-Time read without the
295 /// sequence number of its first item (Clause 15.8.1.2.7).
296 MissingFirstSequenceNumber,
297 /// A nonempty answer to a By-Sequence or By-Time read that numbers its
298 /// first item 0, a number logs never assign: they count from 1 and skip
299 /// 0 when they wrap. Some devices do number the record after the wrap 0.
300 ZeroFirstSequenceNumber,
301 /// A first sequence number in the answer to a read that was by position
302 /// or had no range, which carries none (Clause 15.8.1.2.7).
303 UnexpectedFirstSequenceNumber,
304 /// MORE_ITEMS together with the flag that says the page reached the end
305 /// a ranged read moves toward: LAST_ITEM for a forward read, FIRST_ITEM
306 /// for a backward one. Items left out of a full page lie beyond that
307 /// end, so the two contradict each other (Clause 15.8.2). A read with no
308 /// range may keep either end, so for it only all three flags together
309 /// break the rule.
310 MoreItemsPastEnd,
311 /// More items than the request's count asked for.
312 ItemCountExceedsRequest,
313}
314
315impl ReadRangeViolation {
316 /// The rule's name in snake case, as the Python bindings report it.
317 pub fn name(self) -> &'static str {
318 match self {
319 Self::ObjectMismatch => "object_mismatch",
320 Self::PropertyMismatch => "property_mismatch",
321 Self::ArrayIndexMismatch => "array_index_mismatch",
322 Self::MissingFirstSequenceNumber => "missing_first_sequence_number",
323 Self::ZeroFirstSequenceNumber => "zero_first_sequence_number",
324 Self::UnexpectedFirstSequenceNumber => "unexpected_first_sequence_number",
325 Self::MoreItemsPastEnd => "more_items_past_end",
326 Self::ItemCountExceedsRequest => "item_count_exceeds_request",
327 }
328 }
329}
330
331impl core::fmt::Display for ReadRangeViolation {
332 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
333 f.write_str(match self {
334 Self::ObjectMismatch => "object identifier does not match the request",
335 Self::PropertyMismatch => "property identifier does not match the request",
336 Self::ArrayIndexMismatch => "array index does not match the request",
337 Self::MissingFirstSequenceNumber => {
338 "nonempty By Sequence/Time answer has no first sequence number"
339 }
340 Self::ZeroFirstSequenceNumber => {
341 "nonempty By Sequence/Time answer has first sequence number 0"
342 }
343 Self::UnexpectedFirstSequenceNumber => {
344 "answer to a read by position or without a range has a first sequence number"
345 }
346 Self::MoreItemsPastEnd => {
347 "MORE_ITEMS is set with the flag for the end a ranged read moves toward, \
348 or with both FIRST_ITEM and LAST_ITEM on a read without a range"
349 }
350 Self::ItemCountExceedsRequest => "more items than the request's count",
351 })
352 }
353}
354
355/// The fields a Clause 21 error production adds to the error class and code,
356/// one variant per shape. The service the error answers names the production.
357/// Only with `alloc`.
358#[cfg(feature = "alloc")]
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub enum ErrorDetail {
361 /// ChangeList-Error (AddListElement, RemoveListElement; Clauses 15.1.1.3
362 /// and 15.2.1.3) and CreateObject-Error (Clause 15.3.1.3): the 1-based
363 /// position of the refused element in the request's List of Elements or
364 /// List of Initial Values, or 0 when the failure is not about one element.
365 /// An object refusing one element of a list-valued write uses it too, for
366 /// that element's position in the list it was given; the server's list
367 /// handler turns that into the request's position.
368 FirstFailedElementNumber(u32),
369 /// WritePropertyMultiple-Error (Clause 15.10.1.3): the object, property
370 /// and array index of the first write that failed.
371 FirstFailedWriteAttempt(BACnetObjectPropertyReference),
372 /// SubscribeCOVPropertyMultiple-Error (Clause 13.16.1.3) in its
373 /// first-failed-subscription form: the monitored object and the property
374 /// reference of the COV reference that failed. The other form, a general
375 /// error, has no detail.
376 FirstFailedSubscription(BACnetObjectPropertyReference),
377 /// ConfirmedPrivateTransfer-Error (Clause 16.2.1.3): the vendor and
378 /// service the error answers, and any vendor-defined error parameters.
379 PrivateTransfer {
380 /// Vendor identifier of the private service.
381 vendor_id: u32,
382 /// Vendor-defined service number.
383 service_number: u32,
384 /// Error parameters as encoded inside their context tag 3 frame,
385 /// opaque to the stack; `None` when absent.
386 error_parameters: Option<Vec<u8>>,
387 },
388 /// VTClose-Error (Clause 17.3.1.3) with its list present: the
389 /// requester's local identifiers of the VT sessions that could not be
390 /// closed. Without the list the error has no detail.
391 VtSessionIdentifiers(Vec<u8>),
392}
393
394#[cfg(feature = "alloc")]
395impl core::fmt::Display for ErrorDetail {
396 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
397 fn reference(
398 f: &mut core::fmt::Formatter<'_>,
399 what: &str,
400 reference: &BACnetObjectPropertyReference,
401 ) -> core::fmt::Result {
402 write!(
403 f,
404 "{what} {} {}",
405 reference.object_identifier,
406 PropertyIdentifier::from_raw(reference.property_identifier)
407 )?;
408 match reference.property_array_index {
409 Some(index) => write!(f, "[{index}]"),
410 None => Ok(()),
411 }
412 }
413 match self {
414 Self::FirstFailedElementNumber(number) => write!(f, "first failed element {number}"),
415 Self::FirstFailedWriteAttempt(attempt) => {
416 reference(f, "first failed write attempt", attempt)
417 }
418 Self::FirstFailedSubscription(subscription) => {
419 reference(f, "first failed subscription", subscription)
420 }
421 Self::PrivateTransfer {
422 vendor_id,
423 service_number,
424 error_parameters,
425 } => {
426 write!(f, "vendor {vendor_id} service {service_number}")?;
427 match error_parameters {
428 Some(parameters) => {
429 write!(f, ", {} octets of error parameters", parameters.len())
430 }
431 None => Ok(()),
432 }
433 }
434 Self::VtSessionIdentifiers(sessions) => {
435 f.write_str("VT sessions not closed:")?;
436 for session in sessions {
437 write!(f, " {session}")?;
438 }
439 Ok(())
440 }
441 }
442 }
443}
444
445/// Convenience alias for `Result<T, Error>`.
446pub type Result<T> = core::result::Result<T, Error>;
447
448impl Error {
449 /// The error an Error PDU reports: [`Error::Structured`] when its body
450 /// carried a detail, [`Error::Protocol`] otherwise. Only with `alloc`.
451 #[cfg(feature = "alloc")]
452 pub fn protocol(class: ErrorClass, code: ErrorCode, detail: Option<ErrorDetail>) -> Self {
453 match detail {
454 Some(detail) => Self::Structured {
455 class,
456 code,
457 detail: Box::new(detail),
458 },
459 None => Self::Protocol { class, code },
460 }
461 }
462
463 /// Create a [`DecodingKind::InvalidEncoding`] decoding error at the given
464 /// byte offset: contents whose encoding isn't valid for their datatype.
465 /// Build a [`DecodeError`] directly for a label that names a context or
466 /// a number.
467 pub fn decoding(offset: usize, label: &'static str) -> Self {
468 Self::decoding_kind(DecodingKind::InvalidEncoding, offset, label)
469 }
470
471 /// Create an [`Error::Decoding`] of `kind` at the given byte offset,
472 /// described by a static `label`.
473 pub fn decoding_kind(kind: DecodingKind, offset: usize, label: &'static str) -> Self {
474 Self::Decoding(DecodeError::new(kind, offset, label))
475 }
476
477 /// Create a [`DecodingKind::InvalidTag`] decoding error: a tag at
478 /// `offset` that doesn't fit there.
479 pub fn invalid_tag(offset: usize, label: &'static str) -> Self {
480 Self::decoding_kind(DecodingKind::InvalidTag, offset, label)
481 }
482
483 /// Create a [`DecodingKind::Missing`] decoding error: a required member
484 /// due at `offset` isn't there.
485 pub fn missing(offset: usize, label: &'static str) -> Self {
486 Self::decoding_kind(DecodingKind::Missing, offset, label)
487 }
488
489 /// Create a [`DecodingKind::Trailing`] decoding error: octets from
490 /// `offset` on that the value leaves unread.
491 pub fn trailing(offset: usize, label: &'static str) -> Self {
492 Self::decoding_kind(DecodingKind::Trailing, offset, label)
493 }
494
495 /// Create a [`DecodingKind::OutOfRange`] decoding error: a value at
496 /// `offset` too large for its field, or outside its range.
497 pub fn out_of_range(offset: usize, label: &'static str) -> Self {
498 Self::decoding_kind(DecodingKind::OutOfRange, offset, label)
499 }
500
501 /// Create a [`DecodingKind::Overflow`] decoding error: more items, or a
502 /// longer tag, than the decoder takes.
503 pub fn overflow(offset: usize, label: &'static str) -> Self {
504 Self::decoding_kind(DecodingKind::Overflow, offset, label)
505 }
506
507 /// This error, an inner reader's refusal, as a static [`DecodeError`] at
508 /// `offset`, for a decoder that reports it as its own fault there and
509 /// names its own context or item (#1776). A decoding error keeps its
510 /// kind, context, label, values and index; a range error is
511 /// [`DecodingKind::OutOfRange`] with its label and values; a short buffer
512 /// is [`DecodingKind::Missing`] with its sizes. Any other error, which a
513 /// decoder doesn't return, is an [`DecodingKind::InvalidEncoding`]
514 /// labelled `malformed`.
515 pub fn to_decode_error(&self, offset: usize) -> DecodeError {
516 let mut refused = match self {
517 Self::Decoding(refused) => *refused,
518 Self::Range(refused) => {
519 let mut range = DecodeError::new(DecodingKind::OutOfRange, offset, refused.label);
520 range.values = refused.values;
521 range
522 }
523 Self::BufferTooShort { need, have } => DecodeError::new(
524 DecodingKind::Missing,
525 offset,
526 "buffer too short: need {} bytes, have {}",
527 )
528 .with_values(*need as u64, *have as u64),
529 _ => DecodeError::new(DecodingKind::InvalidEncoding, offset, "malformed"),
530 };
531 refused.offset = offset;
532 refused
533 }
534
535 /// The offset and kind of an [`Error::Decoding`]; `None` for any other
536 /// error.
537 pub fn decode_fault(&self) -> Option<(usize, DecodingKind)> {
538 match self {
539 Self::Decoding(error) => Some((error.offset, error.kind)),
540 _ => None,
541 }
542 }
543
544 /// The Reject reason a responder answers a confirmed request with when
545 /// this error refused it while decoding its parameters (Clauses 18.9 and
546 /// 20.1.8): the reason itself for [`Error::Reject`], the kind's reason
547 /// for a decoding error (the table on
548 /// [`DecodingKind::reject_reason`]), and MISSING_REQUIRED_PARAMETER for
549 /// [`Error::BufferTooShort`], a member cut short because the data ran out
550 /// before it was complete. `None` for any other error, which no decoder
551 /// of the request's parameters reports.
552 pub fn reject_reason(&self) -> Option<RejectReason> {
553 match self {
554 Self::Reject { reason } => Some(*reason),
555 Self::BufferTooShort { .. } => Some(RejectReason::MISSING_REQUIRED_PARAMETER),
556 _ => self.decode_fault().map(|(_, kind)| kind.reject_reason()),
557 }
558 }
559
560 /// This error, met decoding a confirmed request's own parameters, as the
561 /// [`Error::Reject`] a responder answers with: the
562 /// [`reject_reason`](Self::reject_reason) when it is a syntax fault, the
563 /// error unchanged otherwise.
564 ///
565 /// A responder converts only the request's decode with this. A decoding
566 /// error met later, once the service has started to run, is no reason to
567 /// reject the request (Clause 20.1.8), so it stays as it is.
568 pub fn into_request_reject(self) -> Self {
569 match self.reject_reason() {
570 Some(reason) => Self::Reject { reason },
571 None => self,
572 }
573 }
574
575 /// Create an [`Error::Encode`] described by a static `label`. Build an
576 /// [`EncodeError`] directly for a label that names a context or a
577 /// number.
578 pub fn encode(label: &'static str) -> Self {
579 Self::Encode(EncodeError::new(label))
580 }
581
582 /// Create an [`Error::Range`] described by a static `label`. Build a
583 /// [`RangeError`] directly for a label with numbers.
584 pub fn range(label: &'static str) -> Self {
585 Self::Range(RangeError::new(label))
586 }
587
588 /// This error, met checking octets an encoder was handed to copy onto
589 /// the wire, such as a captured raw value, as the encoder's own
590 /// [`Error::Encode`] (#1802). A decoder's refusal keeps its offset (see
591 /// [`EncodeError::from_decode`]); a short buffer or another refusal keeps
592 /// the label and numbers [`to_decode_error`](Self::to_decode_error) gives
593 /// it, without an offset; an [`Error::Encode`] stays as it is.
594 pub fn into_encode_refusal(self) -> Self {
595 match self {
596 Self::Encode(_) => self,
597 Self::Decoding(refused) => Self::Encode(EncodeError::from_decode(&refused)),
598 other => Self::Encode(EncodeError::restating(&other.to_decode_error(0))),
599 }
600 }
601
602 /// Create a buffer-too-short error.
603 pub fn buffer_too_short(need: usize, have: usize) -> Self {
604 Self::BufferTooShort { need, have }
605 }
606}
607
608#[cfg(test)]
609mod tests;