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