#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::{boxed::Box, string::String, vec::Vec};
use core::fmt;
use core::time::Duration;
#[cfg(feature = "alloc")]
use crate::constructed::BACnetObjectPropertyReference;
use crate::data_link::DataLink;
#[cfg(feature = "alloc")]
use crate::enums::PropertyIdentifier;
use crate::enums::{AbortReason, ErrorClass, ErrorCode, RejectReason};
mod decode;
mod encode;
pub use decode::{DecodeError, DecodingKind, RangeError};
pub use encode::{EncodeError, SegmentationError};
struct ProtocolText {
class: ErrorClass,
code: ErrorCode,
}
impl fmt::Display for ProtocolText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn name(
f: &mut fmt::Formatter<'_>,
named: Option<&str>,
field: &str,
value: u16,
) -> fmt::Result {
let Some(named) = named else {
return write!(f, "{field}={value}");
};
for c in named.chars() {
let c = if c == '_' {
'-'
} else {
c.to_ascii_lowercase()
};
fmt::Write::write_char(f, c)?;
}
Ok(())
}
f.write_str("BACnet error: ")?;
name(f, self.class.name(), "class", self.class.to_raw())?;
f.write_str(" / ")?;
name(f, self.code.name(), "code", self.code.to_raw())
}
}
struct NotAdvancing {
requested: u64,
returned: Option<u64>,
}
impl fmt::Display for NotAdvancing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let requested = self.requested;
match self.returned {
Some(returned) => write!(
f,
"asked for sequence number {requested}, the device answered from {returned}"
),
None => write!(
f,
"asked for sequence number {requested}, which the log's counts say it holds, \
the device answered with no records"
),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[deny(clippy::exhaustive_enums)]
pub enum Error {
#[error("{}", ProtocolText { class: *.class, code: *.code })]
Protocol {
class: ErrorClass,
code: ErrorCode,
},
#[cfg(feature = "alloc")]
#[error("{}, {detail}", ProtocolText { class: *.class, code: *.code })]
Structured {
class: ErrorClass,
code: ErrorCode,
detail: Box<ErrorDetail>,
},
#[error("BACnet reject: reason={}", reason.to_raw())]
Reject {
reason: RejectReason,
},
#[error("BACnet abort: reason={}", reason.to_raw())]
Abort {
reason: AbortReason,
},
#[error("message is too long for routed path to DNET {dnet}")]
RoutedPathTooLong {
dnet: u16,
},
#[error("routed path state capacity of {capacity} entries is exhausted")]
RoutedPathCapacityExceeded {
capacity: usize,
},
#[error("operation requires {required}; this transport is {actual}")]
UnsupportedTransport {
required: DataLink,
actual: DataLink,
},
#[error("send refused before transmission: {length} octets exceed the {limit}-octet limit")]
SendTooLong {
length: usize,
limit: usize,
},
#[cfg(feature = "alloc")]
#[error("encoding error: {0}")]
Encoding(String),
#[error("{0}")]
Encode(EncodeError),
#[error("{0}")]
Decoding(DecodeError),
#[cfg(feature = "std")]
#[error("transport error: {0}")]
Transport(#[from] std::io::Error),
#[error("request timed out after {0:?}")]
Timeout(Duration),
#[error("segmentation error: {0}")]
Segmentation(SegmentationError),
#[error("buffer too short: need {need} bytes, have {have}")]
BufferTooShort {
need: usize,
have: usize,
},
#[cfg(feature = "alloc")]
#[error("value out of range: {0}")]
OutOfRange(String),
#[error("{0}")]
Range(RangeError),
#[error("ReadRange acknowledgement refused: {0}")]
ReadRangeViolation(ReadRangeViolation),
#[error(
"log read is not advancing: {}",
NotAdvancing { requested: *.requested, returned: *.returned }
)]
LogNotAdvancing {
requested: u64,
returned: Option<u64>,
},
}
impl From<DecodeError> for Error {
fn from(error: DecodeError) -> Self {
Self::Decoding(error)
}
}
impl From<EncodeError> for Error {
fn from(error: EncodeError) -> Self {
Self::Encode(error)
}
}
impl From<SegmentationError> for Error {
fn from(error: SegmentationError) -> Self {
Self::Segmentation(error)
}
}
impl From<RangeError> for Error {
fn from(error: RangeError) -> Self {
Self::Range(error)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReadRangeViolation {
ObjectMismatch,
PropertyMismatch,
ArrayIndexMismatch,
MissingFirstSequenceNumber,
ZeroFirstSequenceNumber,
UnexpectedFirstSequenceNumber,
MoreItemsPastEnd,
ItemCountExceedsRequest,
}
impl ReadRangeViolation {
pub fn name(self) -> &'static str {
match self {
Self::ObjectMismatch => "object_mismatch",
Self::PropertyMismatch => "property_mismatch",
Self::ArrayIndexMismatch => "array_index_mismatch",
Self::MissingFirstSequenceNumber => "missing_first_sequence_number",
Self::ZeroFirstSequenceNumber => "zero_first_sequence_number",
Self::UnexpectedFirstSequenceNumber => "unexpected_first_sequence_number",
Self::MoreItemsPastEnd => "more_items_past_end",
Self::ItemCountExceedsRequest => "item_count_exceeds_request",
}
}
}
impl core::fmt::Display for ReadRangeViolation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::ObjectMismatch => "object identifier does not match the request",
Self::PropertyMismatch => "property identifier does not match the request",
Self::ArrayIndexMismatch => "array index does not match the request",
Self::MissingFirstSequenceNumber => {
"nonempty By Sequence/Time answer has no first sequence number"
}
Self::ZeroFirstSequenceNumber => {
"nonempty By Sequence/Time answer has first sequence number 0"
}
Self::UnexpectedFirstSequenceNumber => {
"answer to a read by position or without a range has a first sequence number"
}
Self::MoreItemsPastEnd => {
"MORE_ITEMS is set with the flag for the end a ranged read moves toward, \
or with both FIRST_ITEM and LAST_ITEM on a read without a range"
}
Self::ItemCountExceedsRequest => "more items than the request's count",
})
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorDetail {
FirstFailedElementNumber(u32),
FirstFailedWriteAttempt(BACnetObjectPropertyReference),
FirstFailedSubscription(BACnetObjectPropertyReference),
PrivateTransfer {
vendor_id: u32,
service_number: u32,
error_parameters: Option<Vec<u8>>,
},
VtSessionIdentifiers(Vec<u8>),
}
#[cfg(feature = "alloc")]
impl core::fmt::Display for ErrorDetail {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fn reference(
f: &mut core::fmt::Formatter<'_>,
what: &str,
reference: &BACnetObjectPropertyReference,
) -> core::fmt::Result {
write!(
f,
"{what} {} {}",
reference.object_identifier,
PropertyIdentifier::from_raw(reference.property_identifier)
)?;
match reference.property_array_index {
Some(index) => write!(f, "[{index}]"),
None => Ok(()),
}
}
match self {
Self::FirstFailedElementNumber(number) => write!(f, "first failed element {number}"),
Self::FirstFailedWriteAttempt(attempt) => {
reference(f, "first failed write attempt", attempt)
}
Self::FirstFailedSubscription(subscription) => {
reference(f, "first failed subscription", subscription)
}
Self::PrivateTransfer {
vendor_id,
service_number,
error_parameters,
} => {
write!(f, "vendor {vendor_id} service {service_number}")?;
match error_parameters {
Some(parameters) => {
write!(f, ", {} octets of error parameters", parameters.len())
}
None => Ok(()),
}
}
Self::VtSessionIdentifiers(sessions) => {
f.write_str("VT sessions not closed:")?;
for session in sessions {
write!(f, " {session}")?;
}
Ok(())
}
}
}
}
pub type Result<T> = core::result::Result<T, Error>;
impl Error {
#[cfg(feature = "alloc")]
pub fn protocol(class: ErrorClass, code: ErrorCode, detail: Option<ErrorDetail>) -> Self {
match detail {
Some(detail) => Self::Structured {
class,
code,
detail: Box::new(detail),
},
None => Self::Protocol { class, code },
}
}
pub fn decoding(offset: usize, label: &'static str) -> Self {
Self::decoding_kind(DecodingKind::InvalidEncoding, offset, label)
}
pub fn decoding_kind(kind: DecodingKind, offset: usize, label: &'static str) -> Self {
Self::Decoding(DecodeError::new(kind, offset, label))
}
pub fn invalid_tag(offset: usize, label: &'static str) -> Self {
Self::decoding_kind(DecodingKind::InvalidTag, offset, label)
}
pub fn missing(offset: usize, label: &'static str) -> Self {
Self::decoding_kind(DecodingKind::Missing, offset, label)
}
pub fn trailing(offset: usize, label: &'static str) -> Self {
Self::decoding_kind(DecodingKind::Trailing, offset, label)
}
pub fn out_of_range(offset: usize, label: &'static str) -> Self {
Self::decoding_kind(DecodingKind::OutOfRange, offset, label)
}
pub fn overflow(offset: usize, label: &'static str) -> Self {
Self::decoding_kind(DecodingKind::Overflow, offset, label)
}
pub fn to_decode_error(&self, offset: usize) -> DecodeError {
let mut refused = match self {
Self::Decoding(refused) => *refused,
Self::Range(refused) => {
let mut range = DecodeError::new(DecodingKind::OutOfRange, offset, refused.label);
range.values = refused.values;
range
}
Self::BufferTooShort { need, have } => DecodeError::new(
DecodingKind::Missing,
offset,
"buffer too short: need {} bytes, have {}",
)
.with_values(*need as u64, *have as u64),
_ => DecodeError::new(DecodingKind::InvalidEncoding, offset, "malformed"),
};
refused.offset = offset;
refused
}
pub fn decode_fault(&self) -> Option<(usize, DecodingKind)> {
match self {
Self::Decoding(error) => Some((error.offset, error.kind)),
_ => None,
}
}
pub fn reject_reason(&self) -> Option<RejectReason> {
match self {
Self::Reject { reason } => Some(*reason),
Self::BufferTooShort { .. } => Some(RejectReason::MISSING_REQUIRED_PARAMETER),
_ => self.decode_fault().map(|(_, kind)| kind.reject_reason()),
}
}
pub fn into_request_reject(self) -> Self {
match self.reject_reason() {
Some(reason) => Self::Reject { reason },
None => self,
}
}
pub fn encode(label: &'static str) -> Self {
Self::Encode(EncodeError::new(label))
}
pub fn range(label: &'static str) -> Self {
Self::Range(RangeError::new(label))
}
pub fn into_encode_refusal(self) -> Self {
match self {
Self::Encode(_) => self,
Self::Decoding(refused) => Self::Encode(EncodeError::from_decode(&refused)),
other => Self::Encode(EncodeError::restating(&other.to_decode_error(0))),
}
}
pub fn buffer_too_short(need: usize, have: usize) -> Self {
Self::BufferTooShort { need, have }
}
}
#[cfg(test)]
mod tests;