#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use core::fmt;
use bacnet_types::error::{DecodingKind, Error};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use super::MAX_FRAMED_ITEMS;
use crate::primitives;
use crate::tags::{self, Tag, TagClass};
fn next_tag_is(data: &[u8], offset: usize, test: impl FnOnce(&Tag) -> bool) -> Result<bool, Error> {
if offset >= data.len() {
return Ok(false);
}
Ok(test(&tags::decode_tag(data, offset)?.0))
}
pub fn next_is_context(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
next_tag_is(data, offset, |t| t.is_context(tag))
}
pub fn next_is_opening(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
next_tag_is(data, offset, |t| t.is_opening_tag(tag))
}
pub fn next_is_application(data: &[u8], offset: usize, number: u8) -> Result<bool, Error> {
next_tag_is(data, offset, |t| {
t.class == TagClass::Application && t.number == number
})
}
pub(crate) fn next_is_closing(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
Ok(tags::decode_tag(data, offset)?.0.is_closing_tag(tag))
}
pub fn misplaced_kind(data: &[u8], at: usize, found: &Tag, expected: Option<u8>) -> DecodingKind {
let later = expected.is_some_and(|number| {
found.class == TagClass::Context && !found.is_closing && found.number > number
});
if later || (found.is_closing && closes_open_frame(data, at, found.number)) {
DecodingKind::Missing
} else {
DecodingKind::InvalidTag
}
}
fn closes_open_frame(data: &[u8], at: usize, number: u8) -> bool {
let mut open = Vec::new();
let mut pos = 0;
while pos < at {
let Ok((tag, next)) = tags::decode_tag(data, pos) else {
return false;
};
pos = if tag.is_opening {
open.push(tag.number);
next
} else if tag.is_closing {
if open.pop() != Some(tag.number) {
return false;
}
next
} else if tag.class == TagClass::Application && tag.number == tags::app_tag::BOOLEAN {
next
} else {
next.saturating_add(tag.length as usize)
};
}
pos == at && open.last() == Some(&number)
}
pub fn misplaced_tag(
data: &[u8],
found: &Tag,
expected: Option<u8>,
offset: usize,
message: impl Into<String>,
) -> Error {
Error::decoding_kind(
misplaced_kind(data, offset, found, expected),
offset,
message,
)
}
pub fn unclosed_kind(found: &Tag) -> DecodingKind {
if found.is_closing {
DecodingKind::InvalidTag
} else {
DecodingKind::Trailing
}
}
pub fn expect_opening(data: &[u8], offset: usize, tag: u8, what: &str) -> Result<usize, Error> {
let (t, pos) = tags::decode_tag(data, offset)?;
if !t.is_opening_tag(tag) {
return Err(misplaced_tag(
data,
&t,
Some(tag),
offset,
format!("{what}: expected opening tag [{tag}]"),
));
}
Ok(pos)
}
pub fn expect_closing(data: &[u8], offset: usize, tag: u8, what: &str) -> Result<usize, Error> {
let (t, pos) = tags::decode_tag(data, offset)?;
if !t.is_closing_tag(tag) {
return Err(Error::decoding_kind(
unclosed_kind(&t),
offset,
format!("{what}: expected closing tag [{tag}]"),
));
}
Ok(pos)
}
pub fn decode_ctx_constructed<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(&'a [u8], usize), Error> {
let content = expect_opening(data, offset, tag, what)?;
tags::extract_context_value(data, content, tag)
}
pub(crate) fn decode_framed_value(
data: &[u8],
content: usize,
tag: u8,
what: &str,
) -> Result<(PropertyValue, usize), Error> {
let (inner, end) = tags::extract_context_value(data, content, tag)?;
let body = &data[..content + inner.len()];
let mut values = Vec::new();
let mut offset = content;
while offset < body.len() {
if values.len() >= MAX_FRAMED_ITEMS {
return Err(Error::overflow(
offset,
format!("{what}: value exceeds item limit"),
));
}
let (value, next) = primitives::decode_application_value(body, offset)?;
values.push(value);
offset = next;
}
let value = match <[PropertyValue; 1]>::try_from(values) {
Ok([value]) => value,
Err(values) => PropertyValue::List(values),
};
Ok((value, end))
}
pub fn expect_end(data: &[u8], end: usize, at: usize, what: &str) -> Result<(), Error> {
if end == data.len() {
return Ok(());
}
let kind = match tags::decode_tag(data, end) {
Ok((tag, _)) if !tag.is_closing => DecodingKind::Trailing,
_ => DecodingKind::InvalidTag,
};
Err(Error::decoding_kind(
kind,
at,
format!(
"{what}: {} trailing byte(s)",
data.len().saturating_sub(end)
),
))
}
pub(crate) fn contents(data: &[u8], start: usize, length: u32) -> Result<(&[u8], usize), Error> {
let end = usize::try_from(length).map_or(usize::MAX, |length| start.saturating_add(length));
if end > data.len() {
return Err(Error::buffer_too_short(end, data.len()));
}
Ok((&data[start..end], end))
}
fn ctx_header(
data: &[u8],
offset: usize,
tag: u8,
kind: &str,
what: &str,
) -> Result<(Tag, usize), Error> {
let (t, start) = tags::decode_tag(data, offset)?;
if !t.is_context(tag) {
let space = if kind.is_empty() { "" } else { " " };
return Err(misplaced_tag(
data,
&t,
Some(tag),
offset,
format!("{what}: expected context tag [{tag}]{space}{kind}"),
));
}
Ok((t, start))
}
pub fn decode_ctx_primitive<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = ctx_header(data, offset, tag, "", what)?;
contents(data, start, t.length)
}
pub fn decode_ctx_fixed<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
octets: u32,
kind: &str,
what: &str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = ctx_header(data, offset, tag, kind, what)?;
if t.length != octets {
return Err(Error::decoding(
offset,
format!(
"{what}: [{tag}] {kind} has {} contents octets, expected {octets}",
t.length
),
));
}
contents(data, start, octets)
}
pub fn decode_ctx_real(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(f32, usize), Error> {
let (octets, end) = decode_ctx_fixed(data, offset, tag, 4, "REAL", what)?;
Ok((primitives::decode_real(octets)?, end))
}
pub fn decode_ctx_boolean(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(bool, usize), Error> {
let (octets, end) = decode_ctx_fixed(data, offset, tag, 1, "BOOLEAN", what)?;
match octets[0] {
0 => Ok((false, end)),
1 => Ok((true, end)),
_ => Err(Error::decoding(
end - 1,
format!("{what}: [{tag}] BOOLEAN contents must be 0 or 1"),
)),
}
}
pub fn decode_ctx_object_id(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(ObjectIdentifier, usize), Error> {
let (octets, end) = decode_ctx_fixed(data, offset, tag, 4, "object identifier", what)?;
Ok((ObjectIdentifier::decode(octets)?, end))
}
pub fn decode_ctx_bit_string(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<((u8, Vec<u8>), usize), Error> {
let (t, start) = ctx_header(data, offset, tag, "BIT STRING", what)?;
let (octets, end) = contents(data, start, t.length)?;
Ok((primitives::decode_bit_string(octets)?, end))
}
pub fn decode_ctx_octet_string(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(Vec<u8>, usize), Error> {
let (t, start) = ctx_header(data, offset, tag, "OCTET STRING", what)?;
let (octets, end) = contents(data, start, t.length)?;
Ok((octets.to_vec(), end))
}
pub fn decode_ctx_character_string(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(String, usize), Error> {
let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
Ok((primitives::decode_character_string(octets)?, end))
}
pub fn decode_optional_ctx<'a, T>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &str,
decode: impl FnOnce(&'a [u8], usize, u8, &str) -> Result<(T, usize), Error>,
) -> Result<(Option<T>, usize), Error> {
if !next_is_context(data, offset, tag)? {
return Ok((None, offset));
}
let (value, end) = decode(data, offset, tag, what)?;
Ok((Some(value), end))
}
pub trait UnsignedWidth: sealed::Sealed + TryFrom<u64> {
const NAME: &'static str;
}
mod sealed {
pub trait Sealed {}
impl Sealed for u8 {}
impl Sealed for u16 {}
impl Sealed for u32 {}
impl Sealed for u64 {}
}
impl UnsignedWidth for u8 {
const NAME: &'static str = "u8";
}
impl UnsignedWidth for u16 {
const NAME: &'static str = "u16";
}
impl UnsignedWidth for u32 {
const NAME: &'static str = "u32";
}
impl UnsignedWidth for u64 {
const NAME: &'static str = "u64";
}
fn narrow<T: UnsignedWidth>(value: u64, offset: usize, tag: u8, what: &str) -> Result<T, Error> {
T::try_from(value).map_err(|_| {
Error::out_of_range(
offset,
format!("{what}: [{tag}] value {value} exceeds {}", T::NAME),
)
})
}
pub fn decode_ctx_unsigned<T: UnsignedWidth>(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(T, usize), Error> {
let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
let value = primitives::decode_unsigned(octets)?;
Ok((narrow(value, offset, tag, what)?, end))
}
pub fn decode_ctx_canonical_unsigned<T: UnsignedWidth>(
data: &[u8],
offset: usize,
tag: u8,
what: &str,
) -> Result<(T, usize), Error> {
let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
let value = decode_canonical_unsigned(octets, offset, what)?;
Ok((narrow(value, offset, tag, what)?, end))
}
pub fn decode_canonical_unsigned(octets: &[u8], offset: usize, what: &str) -> Result<u64, Error> {
if octets.is_empty() || octets.len() > 8 {
return Err(Error::decoding(
offset,
format!(
"{what}: has {} contents octets, expected one to eight",
octets.len()
),
));
}
if octets.len() > 1 && octets[0] == 0 {
return Err(Error::decoding(
offset,
format!("{what}: must use the shortest Unsigned/Enumerated encoding"),
));
}
primitives::decode_unsigned(octets)
}
fn app_kind(number: u8) -> &'static str {
match number {
tags::app_tag::NULL => "NULL",
tags::app_tag::BOOLEAN => "BOOLEAN",
tags::app_tag::UNSIGNED => "Unsigned",
tags::app_tag::SIGNED => "INTEGER",
tags::app_tag::REAL => "REAL",
tags::app_tag::DOUBLE => "Double",
tags::app_tag::OCTET_STRING => "OCTET STRING",
tags::app_tag::CHARACTER_STRING => "CharacterString",
tags::app_tag::BIT_STRING => "BIT STRING",
tags::app_tag::ENUMERATED => "ENUMERATED",
tags::app_tag::DATE => "Date",
tags::app_tag::TIME => "Time",
tags::app_tag::OBJECT_IDENTIFIER => "BACnetObjectIdentifier",
_ => "value of a reserved type",
}
}
pub fn decode_app_primitive(
data: &[u8],
offset: usize,
number: u8,
what: impl fmt::Display,
) -> Result<(&[u8], usize), Error> {
let (t, start) = app_header(data, offset, number, what)?;
contents(data, start, t.length)
}
fn app_header(
data: &[u8],
offset: usize,
number: u8,
what: impl fmt::Display,
) -> Result<(Tag, usize), Error> {
if number == tags::app_tag::BOOLEAN {
return Err(Error::decoding(
offset,
format!("{what}: an application-tagged BOOLEAN has no contents to read"),
));
}
let (t, start) = tags::decode_tag(data, offset)?;
if t.class != TagClass::Application || t.number != number {
return Err(misplaced_tag(
data,
&t,
None,
offset,
format!("{what}: expected application-tagged {}", app_kind(number)),
));
}
Ok((t, start))
}
pub fn decode_app_fixed<'a>(
data: &'a [u8],
offset: usize,
number: u8,
octets: u32,
what: &str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = app_header(data, offset, number, what)?;
if t.length != octets {
return Err(Error::decoding(
offset,
format!(
"{what}: {} has {} contents octets, expected {octets}",
app_kind(number),
t.length
),
));
}
contents(data, start, octets)
}
pub fn decode_app_object_id(
data: &[u8],
offset: usize,
what: &str,
) -> Result<(ObjectIdentifier, usize), Error> {
let (octets, end) = decode_app_fixed(data, offset, tags::app_tag::OBJECT_IDENTIFIER, 4, what)?;
Ok((ObjectIdentifier::decode(octets)?, end))
}
pub fn decode_app_unsigned<T: UnsignedWidth>(
data: &[u8],
offset: usize,
what: &str,
) -> Result<(T, usize), Error> {
let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::UNSIGNED, what)?;
let value = primitives::decode_unsigned(octets)?;
Ok((
narrow_app(value, end - octets.len(), "Unsigned", what)?,
end,
))
}
pub fn decode_app_bit_string(
data: &[u8],
offset: usize,
what: &str,
) -> Result<((u8, Vec<u8>), usize), Error> {
let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::BIT_STRING, what)?;
Ok((primitives::decode_bit_string(octets)?, end))
}
pub fn decode_app_enumerated<T: UnsignedWidth>(
data: &[u8],
offset: usize,
what: &str,
) -> Result<(T, usize), Error> {
let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::ENUMERATED, what)?;
let value = primitives::decode_unsigned(octets)?;
Ok((
narrow_app(value, end - octets.len(), "ENUMERATED", what)?,
end,
))
}
pub(crate) fn decode_app_canonical_enumerated<T: UnsignedWidth>(
data: &[u8],
offset: usize,
what: &str,
) -> Result<(T, usize), Error> {
let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::ENUMERATED, what)?;
let value = decode_canonical_unsigned(octets, offset, what)?;
Ok((
narrow_app(value, end - octets.len(), "ENUMERATED", what)?,
end,
))
}
fn narrow_app<T: UnsignedWidth>(
value: u64,
contents: usize,
kind: &str,
what: &str,
) -> Result<T, Error> {
T::try_from(value)
.map_err(|_| Error::out_of_range(contents, format!("{what}: {kind} exceeds {}", T::NAME)))
}
pub fn decode_app_character_string(
data: &[u8],
offset: usize,
what: &str,
) -> Result<(String, usize), Error> {
let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::CHARACTER_STRING, what)?;
Ok((primitives::decode_character_string(octets)?, end))
}
#[cfg(test)]
#[path = "tagged_frame_tests.rs"]
mod frame_tests;