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#[cfg(not(feature = "std"))]
8use alloc::{boxed::Box, format, string::String, vec::Vec};
9#[cfg(feature = "std")]
10use std::time::Duration;
11
12use crate::constructed::BACnetObjectPropertyReference;
13use crate::data_link::DataLink;
14use crate::enums::{ErrorClass, ErrorCode, PropertyIdentifier, RejectReason};
15
16fn format_protocol_error(class: u32, code: u32) -> String {
17    let class_name = ErrorClass::ALL_NAMED
18        .iter()
19        .find(|(_, v)| v.to_raw() as u32 == class)
20        .map(|(n, _)| n.to_lowercase().replace('_', "-"));
21    let code_name = ErrorCode::ALL_NAMED
22        .iter()
23        .find(|(_, v)| v.to_raw() as u32 == code)
24        .map(|(n, _)| n.to_lowercase().replace('_', "-"));
25
26    match (class_name, code_name) {
27        (Some(cn), Some(co)) => format!("BACnet error: {cn} / {co}"),
28        (Some(cn), None) => format!("BACnet error: {cn} / code={code}"),
29        (None, Some(co)) => format!("BACnet error: class={class} / {co}"),
30        (None, None) => format!("BACnet error: class={class} / code={code}"),
31    }
32}
33
34/// Top-level error type for the BACnet library.
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37    /// BACnet protocol error response (Clause 20.1.7).
38    #[error("{}", format_protocol_error(*.class, *.code))]
39    Protocol {
40        /// Error class value.
41        class: u32,
42        /// Error code value.
43        code: u32,
44    },
45
46    /// BACnet error response whose Clause 21 production carries more than the
47    /// class and code: ChangeList-Error, CreateObject-Error,
48    /// WritePropertyMultiple-Error, SubscribeCOVPropertyMultiple-Error,
49    /// ConfirmedPrivateTransfer-Error or VTClose-Error. `detail` holds the
50    /// extra fields; a body whose optional fields are all absent is reported
51    /// as [`Error::Protocol`].
52    #[error("{}, {detail}", format_protocol_error(*.class, *.code))]
53    Structured {
54        /// Error class value.
55        class: u32,
56        /// Error code value.
57        code: u32,
58        /// The production's fields after the class and code. Boxed so the
59        /// rare structured error does not widen every `Result<_, Error>`.
60        detail: Box<ErrorDetail>,
61    },
62
63    /// BACnet reject PDU (Clause 20.1.5).
64    #[error("BACnet reject: reason={reason}")]
65    Reject {
66        /// Reject reason value.
67        reason: u8,
68    },
69
70    /// BACnet abort PDU (Clause 20.1.6).
71    #[error("BACnet abort: reason={reason}")]
72    Abort {
73        /// Abort reason value.
74        reason: u8,
75    },
76
77    /// A router reported that the active message was too long for a routed path.
78    #[error("message is too long for routed path to DNET {dnet}")]
79    RoutedPathTooLong {
80        /// Destination network rejected by the router.
81        dnet: u16,
82    },
83
84    /// The client cannot safely allocate state for another routed path.
85    #[error("routed path state capacity of {capacity} entries is exhausted")]
86    RoutedPathCapacityExceeded {
87        /// Maximum number of immediate-router/DNET path entries retained.
88        capacity: usize,
89    },
90
91    /// The endpoint's transport cannot carry the requested operation, such as
92    /// a BBMD management request through a data link other than BACnet/IP.
93    /// Nothing was sent.
94    #[error("operation requires {required}; this transport is {actual}")]
95    UnsupportedTransport {
96        /// Data link the operation needs.
97        required: DataLink,
98        /// Data link the endpoint's transport carries.
99        actual: DataLink,
100    },
101
102    /// Error encoding a PDU.
103    #[error("encoding error: {0}")]
104    Encoding(String),
105
106    /// Error decoding received data.
107    #[error("decoding error at offset {offset}: {message}")]
108    Decoding {
109        /// Byte offset where the error occurred.
110        offset: usize,
111        /// Which kind of fault the decoder found.
112        kind: DecodingKind,
113        /// Description of what went wrong.
114        message: String,
115    },
116
117    /// Transport-level I/O error.
118    #[cfg(feature = "std")]
119    #[error("transport error: {0}")]
120    Transport(#[from] std::io::Error),
121
122    /// Request timed out.
123    #[cfg(feature = "std")]
124    #[error("request timed out after {0:?}")]
125    Timeout(Duration),
126
127    /// Segmentation assembly error.
128    #[error("segmentation error: {0}")]
129    Segmentation(String),
130
131    /// Buffer too short for the expected data.
132    #[error("buffer too short: need {need} bytes, have {have}")]
133    BufferTooShort {
134        /// Minimum bytes needed.
135        need: usize,
136        /// Bytes actually available.
137        have: usize,
138    },
139
140    /// Value out of valid range.
141    #[error("value out of range: {0}")]
142    OutOfRange(String),
143}
144
145/// Which fault a decoder found in the data it refused, so a responder can
146/// name it (#1446). [`DecodingKind::reject_reason`] gives the Clause 18.9
147/// Reject reason each kind draws when the data was a confirmed request.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149pub enum DecodingKind {
150    /// Contents whose encoding isn't valid for their datatype: a wrong
151    /// length, an Unsigned of no octets or with padding a canonical field
152    /// refuses, an unknown character set, a BIT STRING's unused-bit count
153    /// past 7, and the like. The kind [`Error::decoding`] makes.
154    InvalidEncoding,
155    /// A value too large for its field's width, or outside the range the
156    /// production or service gives it.
157    OutOfRange,
158    /// More than the decoder takes: a count of items past its limit, or a tag
159    /// length past its sanity bound.
160    Overflow,
161    /// A tag that doesn't fit where it stands: another number, class or form
162    /// than the member there needs, a closing tag that closes no open frame,
163    /// a tag header that is itself malformed, or an opening tag nested
164    /// deeper than the decoder walks.
165    InvalidTag,
166    /// The data ends, or the enclosing frame closes, where a required member
167    /// or a frame's closing tag is due: a member is missing.
168    Missing,
169    /// Octets are left after the last member of a value that must fill its
170    /// input: arguments beyond those the production has.
171    Trailing,
172    /// A well-formed encoding the decoder doesn't handle: a CharacterString
173    /// in a character set it can't convert (IBM/Microsoft DBCS, JIS X 0208
174    /// or UCS-4).
175    Unsupported,
176}
177
178impl DecodingKind {
179    /// The Clause 18.9 Reject reason that names this fault in a confirmed
180    /// request:
181    ///
182    /// | Kind | Reject reason |
183    /// |---|---|
184    /// | [`InvalidEncoding`](Self::InvalidEncoding) | INVALID_DATA_ENCODING |
185    /// | [`OutOfRange`](Self::OutOfRange) | PARAMETER_OUT_OF_RANGE |
186    /// | [`Overflow`](Self::Overflow) | BUFFER_OVERFLOW |
187    /// | [`InvalidTag`](Self::InvalidTag) | INVALID_TAG |
188    /// | [`Missing`](Self::Missing) | MISSING_REQUIRED_PARAMETER |
189    /// | [`Trailing`](Self::Trailing) | TOO_MANY_ARGUMENTS |
190    /// | [`Unsupported`](Self::Unsupported) | OTHER |
191    pub fn reject_reason(self) -> RejectReason {
192        match self {
193            Self::InvalidEncoding => RejectReason::INVALID_DATA_ENCODING,
194            Self::OutOfRange => RejectReason::PARAMETER_OUT_OF_RANGE,
195            Self::Overflow => RejectReason::BUFFER_OVERFLOW,
196            Self::InvalidTag => RejectReason::INVALID_TAG,
197            Self::Missing => RejectReason::MISSING_REQUIRED_PARAMETER,
198            Self::Trailing => RejectReason::TOO_MANY_ARGUMENTS,
199            Self::Unsupported => RejectReason::OTHER,
200        }
201    }
202}
203
204/// The fields a Clause 21 error production adds to the error class and code,
205/// one variant per shape. The service the error answers names the production.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum ErrorDetail {
208    /// ChangeList-Error (AddListElement, RemoveListElement; Clauses 15.1.1.3
209    /// and 15.2.1.3) and CreateObject-Error (Clause 15.3.1.3): the 1-based
210    /// position of the refused element in the request's List of Elements or
211    /// List of Initial Values, or 0 when the failure is not about one element.
212    /// An object refusing one element of a list-valued write uses it too, for
213    /// that element's position in the list it was given; the server's list
214    /// handler turns that into the request's position.
215    FirstFailedElementNumber(u32),
216    /// WritePropertyMultiple-Error (Clause 15.10.1.3): the object, property
217    /// and array index of the first write that failed.
218    FirstFailedWriteAttempt(BACnetObjectPropertyReference),
219    /// SubscribeCOVPropertyMultiple-Error (Clause 13.16.1.3) in its
220    /// first-failed-subscription form: the monitored object and the property
221    /// reference of the COV reference that failed. The other form, a general
222    /// error, has no detail.
223    FirstFailedSubscription(BACnetObjectPropertyReference),
224    /// ConfirmedPrivateTransfer-Error (Clause 16.2.1.3): the vendor and
225    /// service the error answers, and any vendor-defined error parameters.
226    PrivateTransfer {
227        /// Vendor identifier of the private service.
228        vendor_id: u32,
229        /// Vendor-defined service number.
230        service_number: u32,
231        /// Error parameters as encoded inside their context tag 3 frame,
232        /// opaque to the stack; `None` when absent.
233        error_parameters: Option<Vec<u8>>,
234    },
235    /// VTClose-Error (Clause 17.3.1.3) with its list present: the
236    /// requester's local identifiers of the VT sessions that could not be
237    /// closed. Without the list the error has no detail.
238    VtSessionIdentifiers(Vec<u8>),
239}
240
241impl core::fmt::Display for ErrorDetail {
242    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
243        fn reference(
244            f: &mut core::fmt::Formatter<'_>,
245            what: &str,
246            reference: &BACnetObjectPropertyReference,
247        ) -> core::fmt::Result {
248            write!(
249                f,
250                "{what} {} {}",
251                reference.object_identifier,
252                PropertyIdentifier::from_raw(reference.property_identifier)
253            )?;
254            match reference.property_array_index {
255                Some(index) => write!(f, "[{index}]"),
256                None => Ok(()),
257            }
258        }
259        match self {
260            Self::FirstFailedElementNumber(number) => write!(f, "first failed element {number}"),
261            Self::FirstFailedWriteAttempt(attempt) => {
262                reference(f, "first failed write attempt", attempt)
263            }
264            Self::FirstFailedSubscription(subscription) => {
265                reference(f, "first failed subscription", subscription)
266            }
267            Self::PrivateTransfer {
268                vendor_id,
269                service_number,
270                error_parameters,
271            } => {
272                write!(f, "vendor {vendor_id} service {service_number}")?;
273                match error_parameters {
274                    Some(parameters) => {
275                        write!(f, ", {} octets of error parameters", parameters.len())
276                    }
277                    None => Ok(()),
278                }
279            }
280            Self::VtSessionIdentifiers(sessions) => {
281                f.write_str("VT sessions not closed:")?;
282                for session in sessions {
283                    write!(f, " {session}")?;
284                }
285                Ok(())
286            }
287        }
288    }
289}
290
291/// Convenience alias for `Result<T, Error>`.
292pub type Result<T> = core::result::Result<T, Error>;
293
294impl Error {
295    /// The error an Error PDU reports: [`Error::Structured`] when its body
296    /// carried a detail, [`Error::Protocol`] otherwise.
297    pub fn protocol(class: u32, code: u32, detail: Option<ErrorDetail>) -> Self {
298        match detail {
299            Some(detail) => Self::Structured {
300                class,
301                code,
302                detail: Box::new(detail),
303            },
304            None => Self::Protocol { class, code },
305        }
306    }
307
308    /// Create a [`DecodingKind::InvalidEncoding`] decoding error at the given
309    /// byte offset: contents whose encoding isn't valid for their datatype.
310    pub fn decoding(offset: usize, message: impl Into<String>) -> Self {
311        Self::decoding_kind(DecodingKind::InvalidEncoding, offset, message)
312    }
313
314    /// Create a decoding error of `kind` at the given byte offset.
315    pub fn decoding_kind(kind: DecodingKind, offset: usize, message: impl Into<String>) -> Self {
316        Self::Decoding {
317            offset,
318            kind,
319            message: message.into(),
320        }
321    }
322
323    /// Create a [`DecodingKind::InvalidTag`] decoding error: a tag at
324    /// `offset` that doesn't fit there.
325    pub fn invalid_tag(offset: usize, message: impl Into<String>) -> Self {
326        Self::decoding_kind(DecodingKind::InvalidTag, offset, message)
327    }
328
329    /// Create a [`DecodingKind::Missing`] decoding error: a required member
330    /// due at `offset` isn't there.
331    pub fn missing(offset: usize, message: impl Into<String>) -> Self {
332        Self::decoding_kind(DecodingKind::Missing, offset, message)
333    }
334
335    /// Create a [`DecodingKind::Trailing`] decoding error: octets from
336    /// `offset` on that the value leaves unread.
337    pub fn trailing(offset: usize, message: impl Into<String>) -> Self {
338        Self::decoding_kind(DecodingKind::Trailing, offset, message)
339    }
340
341    /// Create a [`DecodingKind::OutOfRange`] decoding error: a value at
342    /// `offset` too large for its field, or outside its range.
343    pub fn out_of_range(offset: usize, message: impl Into<String>) -> Self {
344        Self::decoding_kind(DecodingKind::OutOfRange, offset, message)
345    }
346
347    /// Create a [`DecodingKind::Overflow`] decoding error: more items, or a
348    /// longer tag, than the decoder takes.
349    pub fn overflow(offset: usize, message: impl Into<String>) -> Self {
350        Self::decoding_kind(DecodingKind::Overflow, offset, message)
351    }
352
353    /// The Reject reason a responder answers a confirmed request with when
354    /// this error refused it while decoding its parameters (Clauses 18.9 and
355    /// 20.1.8): the reason itself for [`Error::Reject`], the kind's reason
356    /// for [`Error::Decoding`] (the table on [`DecodingKind::reject_reason`]),
357    /// and MISSING_REQUIRED_PARAMETER for [`Error::BufferTooShort`], a member
358    /// cut short because the data ran out before it was complete. `None` for
359    /// any other error, which no decoder of the request's parameters
360    /// reports.
361    pub fn reject_reason(&self) -> Option<RejectReason> {
362        match self {
363            Self::Reject { reason } => Some(RejectReason::from_raw(*reason)),
364            Self::Decoding { kind, .. } => Some(kind.reject_reason()),
365            Self::BufferTooShort { .. } => Some(RejectReason::MISSING_REQUIRED_PARAMETER),
366            _ => None,
367        }
368    }
369
370    /// This error, met decoding a confirmed request's own parameters, as the
371    /// [`Error::Reject`] a responder answers with: the
372    /// [`reject_reason`](Self::reject_reason) when it is a syntax fault, the
373    /// error unchanged otherwise.
374    ///
375    /// A responder converts only the request's decode with this. A decoding
376    /// error met later, once the service has started to run, is no reason to
377    /// reject the request (Clause 20.1.8), so it stays as it is.
378    pub fn into_request_reject(self) -> Self {
379        match self.reject_reason() {
380            Some(reason) => Self::Reject {
381                reason: reason.to_raw(),
382            },
383            None => self,
384        }
385    }
386
387    /// Create a buffer-too-short error.
388    pub fn buffer_too_short(need: usize, have: usize) -> Self {
389        Self::BufferTooShort { need, have }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn protocol_error_display() {
399        let err = Error::Protocol { class: 2, code: 31 };
400        assert!(err.to_string().contains("property"));
401        assert!(err.to_string().contains("unknown-object"));
402
403        // Unknown class/code falls back to numeric
404        let err2 = Error::Protocol {
405            class: 999,
406            code: 999,
407        };
408        assert!(err2.to_string().contains("class=999"));
409        assert!(err2.to_string().contains("code=999"));
410    }
411
412    #[test]
413    fn structured_error_display_names_the_detail() {
414        use crate::enums::ObjectType;
415        use crate::primitives::ObjectIdentifier;
416
417        let reference = |index| BACnetObjectPropertyReference {
418            object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 7).unwrap(),
419            property_identifier: PropertyIdentifier::PRESENT_VALUE.to_raw(),
420            property_array_index: index,
421        };
422        let private_transfer = |error_parameters| ErrorDetail::PrivateTransfer {
423            vendor_id: 555,
424            service_number: 7,
425            error_parameters,
426        };
427        for (class, code, detail, expected) in [
428            (
429                5,
430                81,
431                ErrorDetail::FirstFailedElementNumber(2),
432                "BACnet error: services / list-element-not-found, first failed element 2",
433            ),
434            (
435                2,
436                40,
437                ErrorDetail::FirstFailedWriteAttempt(reference(Some(3))),
438                "BACnet error: property / write-access-denied, \
439                 first failed write attempt ANALOG_VALUE,7 PRESENT_VALUE[3]",
440            ),
441            (
442                2,
443                44,
444                ErrorDetail::FirstFailedSubscription(reference(None)),
445                "BACnet error: property / not-cov-property, \
446                 first failed subscription ANALOG_VALUE,7 PRESENT_VALUE",
447            ),
448            (
449                5,
450                0,
451                private_transfer(Some(vec![0x21, 1])),
452                "BACnet error: services / other, vendor 555 service 7, \
453                 2 octets of error parameters",
454            ),
455            (
456                5,
457                0,
458                private_transfer(None),
459                "BACnet error: services / other, vendor 555 service 7",
460            ),
461            (
462                5,
463                39,
464                ErrorDetail::VtSessionIdentifiers(vec![1, 4]),
465                "BACnet error: services / vt-session-termination-failure, \
466                 VT sessions not closed: 1 4",
467            ),
468        ] {
469            assert_eq!(
470                Error::protocol(class, code, Some(detail)).to_string(),
471                expected
472            );
473        }
474        assert!(matches!(
475            Error::protocol(2, 31, None),
476            Error::Protocol { class: 2, code: 31 }
477        ));
478    }
479
480    #[test]
481    fn decoding_error_display() {
482        let err = Error::decoding(42, "unexpected tag");
483        assert!(err.to_string().contains("offset 42"));
484        assert!(err.to_string().contains("unexpected tag"));
485    }
486
487    #[test]
488    fn syntax_faults_name_their_reject_reasons() {
489        for (error, reason) in [
490            (
491                Error::decoding(1, "bad"),
492                RejectReason::INVALID_DATA_ENCODING,
493            ),
494            (
495                Error::out_of_range(1, "wide"),
496                RejectReason::PARAMETER_OUT_OF_RANGE,
497            ),
498            (Error::overflow(1, "many"), RejectReason::BUFFER_OVERFLOW),
499            (
500                Error::decoding_kind(DecodingKind::Unsupported, 1, "DBCS"),
501                RejectReason::OTHER,
502            ),
503            (Error::invalid_tag(1, "tag"), RejectReason::INVALID_TAG),
504            (
505                Error::missing(1, "gone"),
506                RejectReason::MISSING_REQUIRED_PARAMETER,
507            ),
508            (Error::trailing(1, "more"), RejectReason::TOO_MANY_ARGUMENTS),
509            (
510                Error::buffer_too_short(9, 4),
511                RejectReason::MISSING_REQUIRED_PARAMETER,
512            ),
513            (
514                Error::Reject { reason: 8 },
515                RejectReason::UNDEFINED_ENUMERATION,
516            ),
517        ] {
518            assert_eq!(error.reject_reason(), Some(reason), "{error}");
519        }
520        for error in [
521            Error::Protocol { class: 5, code: 0 },
522            Error::Encoding("x".into()),
523            Error::OutOfRange("x".into()),
524            Error::Abort { reason: 1 },
525        ] {
526            assert_eq!(error.reject_reason(), None, "{error}");
527        }
528        // A request's decode error becomes its Reject; any other error stays.
529        assert!(matches!(
530            Error::trailing(3, "more").into_request_reject(),
531            Error::Reject { reason } if reason == RejectReason::TOO_MANY_ARGUMENTS.to_raw()
532        ));
533        assert!(matches!(
534            Error::OutOfRange("x".into()).into_request_reject(),
535            Error::OutOfRange(_)
536        ));
537        // The kind doesn't show in the message.
538        assert_eq!(
539            Error::trailing(3, "two trailing byte(s)").to_string(),
540            "decoding error at offset 3: two trailing byte(s)"
541        );
542    }
543
544    #[test]
545    fn buffer_too_short_display() {
546        let err = Error::buffer_too_short(10, 3);
547        assert!(err.to_string().contains("need 10"));
548        assert!(err.to_string().contains("have 3"));
549    }
550
551    #[cfg(feature = "std")]
552    #[test]
553    fn timeout_error_display() {
554        let err = Error::Timeout(Duration::from_secs(3));
555        assert!(err.to_string().contains("3s"));
556    }
557
558    #[test]
559    fn routed_path_too_long_display_preserves_dnet() {
560        let err = Error::RoutedPathTooLong { dnet: 1234 };
561        assert!(err.to_string().contains("1234"));
562    }
563
564    #[test]
565    fn routed_path_capacity_display_preserves_bound() {
566        let err = Error::RoutedPathCapacityExceeded { capacity: 256 };
567        assert!(err.to_string().contains("256"));
568    }
569
570    #[test]
571    fn unsupported_transport_display_names_both_data_links() {
572        let err = Error::UnsupportedTransport {
573            required: DataLink::Bip,
574            actual: DataLink::Mstp,
575        };
576        assert_eq!(
577            err.to_string(),
578            "operation requires BACnet/IP; this transport is MS/TP"
579        );
580    }
581}