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;
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;