Skip to main content

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;