#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::{string::String, vec::Vec};
use bacnet_types::error::{DecodeError, DecodingKind, Error};
use bacnet_types::primitives::ObjectIdentifier;
#[cfg(feature = "alloc")]
use bacnet_types::primitives::PropertyValue;
#[cfg(feature = "alloc")]
use super::MAX_FRAMED_ITEMS;
use crate::primitives;
use crate::tags::{self, MAX_CONTEXT_NESTING_DEPTH, 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
})
}
#[cfg(feature = "alloc")]
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 = [0u8; MAX_CONTEXT_NESTING_DEPTH];
let mut depth = 0;
let mut pos = 0;
while pos < at {
let Ok((tag, next)) = tags::decode_tag(data, pos) else {
return false;
};
pos = if tag.is_opening {
let Some(slot) = open.get_mut(depth) else {
return false;
};
*slot = tag.number;
depth += 1;
next
} else if tag.is_closing {
if depth == 0 || open[depth - 1] != tag.number {
return false;
}
depth -= 1;
next
} else if tag.class == TagClass::Application && tag.number == tags::app_tag::BOOLEAN {
next
} else {
next.saturating_add(tag.length as usize)
};
}
pos == at && depth > 0 && open[depth - 1] == number
}
pub fn misplaced_tag(
data: &[u8],
found: &Tag,
expected: Option<u8>,
offset: usize,
label: &'static str,
) -> Error {
misplaced(data, found, expected, offset, label).into()
}
pub fn misplaced(
data: &[u8],
found: &Tag,
expected: Option<u8>,
offset: usize,
label: &'static str,
) -> DecodeError {
DecodeError::new(misplaced_kind(data, offset, found, expected), offset, label)
}
#[cold]
fn misplaced_member(
data: &[u8],
found: &Tag,
expected: Option<u8>,
offset: usize,
what: &'static str,
(label, named): (&'static str, u8),
) -> Error {
DecodeError::new(misplaced_kind(data, offset, found, expected), offset, label)
.within(what)
.with_value(named.into())
.into()
}
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: &'static str,
) -> Result<usize, Error> {
let (t, pos) = tags::decode_tag(data, offset)?;
if !t.is_opening_tag(tag) {
return Err(misplaced_member(
data,
&t,
Some(tag),
offset,
what,
("expected opening tag [{}]", tag),
));
}
Ok(pos)
}
pub fn expect_closing(
data: &[u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<usize, Error> {
let (t, pos) = tags::decode_tag(data, offset)?;
if !t.is_closing_tag(tag) {
return Err(
DecodeError::new(unclosed_kind(&t), offset, "expected closing tag [{}]")
.within(what)
.with_value(tag.into())
.into(),
);
}
Ok(pos)
}
pub fn decode_ctx_constructed<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(&'a [u8], usize), Error> {
let content = expect_opening(data, offset, tag, what)?;
tags::extract_context_value(data, content, tag)
}
#[cfg(feature = "alloc")]
pub(crate) fn decode_framed_value(
data: &[u8],
content: usize,
tag: u8,
what: &'static 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(DecodeError::new(
DecodingKind::Overflow,
offset,
"value exceeds item limit",
)
.within(what)
.into());
}
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: &'static 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(DecodeError::new(kind, at, "{} trailing byte(s)")
.within(what)
.with_value(data.len().saturating_sub(end) as u64)
.into())
}
#[inline]
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))
}
#[inline]
fn ctx_header(
data: &[u8],
offset: usize,
tag: u8,
misplaced: &'static str,
what: &'static str,
) -> Result<(Tag, usize), Error> {
let (t, start) = tags::decode_tag(data, offset)?;
if !t.is_context(tag) {
return Err(misplaced_member(
data,
&t,
Some(tag),
offset,
what,
(misplaced, tag),
));
}
Ok((t, start))
}
#[inline]
pub fn decode_ctx_primitive<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = ctx_header(data, offset, tag, "expected context tag [{}]", what)?;
contents(data, start, t.length)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Fixed {
pub octets: u32,
pub misplaced: &'static str,
pub wrong_length: &'static str,
}
macro_rules! fixed {
($name:literal, $octets:literal) => {
Fixed {
octets: $octets,
misplaced: concat!("expected context tag [{}] ", $name),
wrong_length: concat!(
"[{}] ",
$name,
" has {} contents octets, expected ",
$octets
),
}
};
}
impl Fixed {
pub const REAL: Self = fixed!("REAL", 4);
pub const DOUBLE: Self = fixed!("Double", 8);
pub const BOOLEAN: Self = fixed!("BOOLEAN", 1);
pub const DATE: Self = fixed!("Date", 4);
pub const TIME: Self = fixed!("Time", 4);
pub const OBJECT_IDENTIFIER: Self = fixed!("object identifier", 4);
}
#[inline]
pub fn decode_ctx_fixed<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
fixed: Fixed,
what: &'static str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = ctx_header(data, offset, tag, fixed.misplaced, what)?;
if t.length != fixed.octets {
return Err(ctx_length_error(offset, tag, fixed, what, t.length));
}
contents(data, start, fixed.octets)
}
#[cold]
fn ctx_length_error(
offset: usize,
tag: u8,
fixed: Fixed,
what: &'static str,
length: u32,
) -> Error {
DecodeError::new(DecodingKind::InvalidEncoding, offset, fixed.wrong_length)
.within(what)
.with_values(tag.into(), length.into())
.into()
}
pub fn decode_ctx_real(
data: &[u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(f32, usize), Error> {
let (octets, end) = decode_ctx_fixed(data, offset, tag, Fixed::REAL, what)?;
Ok((primitives::decode_real(octets)?, end))
}
pub fn decode_ctx_boolean(
data: &[u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(bool, usize), Error> {
let (octets, end) = decode_ctx_fixed(data, offset, tag, Fixed::BOOLEAN, what)?;
match octets[0] {
0 => Ok((false, end)),
1 => Ok((true, end)),
_ => Err(DecodeError::new(
DecodingKind::InvalidEncoding,
end - 1,
"[{}] BOOLEAN contents must be 0 or 1",
)
.within(what)
.with_value(tag.into())
.into()),
}
}
#[inline]
pub fn decode_ctx_object_id(
data: &[u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(ObjectIdentifier, usize), Error> {
let (octets, end) = decode_ctx_fixed(data, offset, tag, Fixed::OBJECT_IDENTIFIER, what)?;
Ok((ObjectIdentifier::decode(octets)?, end))
}
#[cfg(feature = "alloc")]
pub fn decode_ctx_bit_string(
data: &[u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<((u8, Vec<u8>), usize), Error> {
let ((unused, bits), end) = decode_ctx_bit_string_ref(data, offset, tag, what)?;
Ok(((unused, bits.to_vec()), end))
}
pub fn decode_ctx_bit_string_ref<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<((u8, &'a [u8]), usize), Error> {
let (t, start) = ctx_header(
data,
offset,
tag,
"expected context tag [{}] BIT STRING",
what,
)?;
let (octets, end) = contents(data, start, t.length)?;
Ok((primitives::decode_bit_string_ref(octets)?, end))
}
#[cfg(feature = "alloc")]
pub fn decode_ctx_octet_string(
data: &[u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(Vec<u8>, usize), Error> {
let (octets, end) = decode_ctx_octet_string_ref(data, offset, tag, what)?;
Ok((octets.to_vec(), end))
}
pub fn decode_ctx_octet_string_ref<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = ctx_header(
data,
offset,
tag,
"expected context tag [{}] OCTET STRING",
what,
)?;
contents(data, start, t.length)
}
#[cfg(feature = "alloc")]
pub fn decode_ctx_character_string(
data: &[u8],
offset: usize,
tag: u8,
what: &'static 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_ctx_character_string_ref<'a>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &'static str,
) -> Result<(primitives::CharacterStringRef<'a>, usize), Error> {
let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
Ok((primitives::CharacterStringRef::decode(octets)?, end))
}
pub fn decode_optional_ctx<'a, T>(
data: &'a [u8],
offset: usize,
tag: u8,
what: &'static str,
decode: impl FnOnce(&'a [u8], usize, u8, &'static 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;
const CONTEXT_EXCEEDS: &'static str;
const UNSIGNED_EXCEEDS: &'static str;
const ENUMERATED_EXCEEDS: &'static str;
}
macro_rules! width {
($ty:ident) => {
impl UnsignedWidth for $ty {
const NAME: &'static str = stringify!($ty);
const CONTEXT_EXCEEDS: &'static str =
concat!("[{}] value {} exceeds ", stringify!($ty));
const UNSIGNED_EXCEEDS: &'static str = concat!("Unsigned exceeds ", stringify!($ty));
const ENUMERATED_EXCEEDS: &'static str =
concat!("ENUMERATED exceeds ", stringify!($ty));
}
};
}
mod sealed {
pub trait Sealed {}
impl Sealed for u8 {}
impl Sealed for u16 {}
impl Sealed for u32 {}
impl Sealed for u64 {}
}
width!(u8);
width!(u16);
width!(u32);
width!(u64);
fn narrow<T: UnsignedWidth>(
value: u64,
offset: usize,
tag: u8,
what: &'static str,
) -> Result<T, Error> {
T::try_from(value).map_err(|_| {
DecodeError::new(DecodingKind::OutOfRange, offset, T::CONTEXT_EXCEEDS)
.within(what)
.with_values(tag.into(), value)
.into()
})
}
#[inline]
pub fn decode_ctx_unsigned<T: UnsignedWidth>(
data: &[u8],
offset: usize,
tag: u8,
what: &'static 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: &'static 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: &'static str,
) -> Result<u64, Error> {
if octets.is_empty() || octets.len() > 8 {
return Err(DecodeError::new(
DecodingKind::InvalidEncoding,
offset,
"has {} contents octets, expected one to eight",
)
.within(what)
.with_value(octets.len() as u64)
.into());
}
if octets.len() > 1 && octets[0] == 0 {
return Err(DecodeError::new(
DecodingKind::InvalidEncoding,
offset,
"must use the shortest Unsigned/Enumerated encoding",
)
.within(what)
.into());
}
primitives::decode_unsigned(octets)
}
pub fn decode_app_primitive<'a>(
data: &'a [u8],
offset: usize,
number: u8,
what: &'static str,
) -> Result<(&'a [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: &'static str,
) -> Result<(Tag, usize), Error> {
if number == tags::app_tag::BOOLEAN {
return Err(DecodeError::new(
DecodingKind::InvalidEncoding,
offset,
"an application-tagged BOOLEAN has no contents to read",
)
.within(what)
.into());
}
let (t, start) = tags::decode_tag(data, offset)?;
if t.class != TagClass::Application || t.number != number {
return Err(misplaced_member(
data,
&t,
None,
offset,
what,
("expected application-tagged {t}", number),
));
}
Ok((t, start))
}
pub fn decode_app_fixed<'a>(
data: &'a [u8],
offset: usize,
number: u8,
octets: u32,
what: &'static str,
) -> Result<(&'a [u8], usize), Error> {
let (t, start) = app_header(data, offset, number, what)?;
if t.length != octets {
let refused = match primitives::fixed_length_label(number) {
Some((label, _)) => DecodeError::new(DecodingKind::InvalidEncoding, offset, label)
.with_value(t.length.into()),
None => DecodeError::new(
DecodingKind::InvalidEncoding,
offset,
"application value has {} contents octets, expected {}",
)
.with_values(t.length.into(), octets.into()),
};
return Err(refused.within(what).into());
}
contents(data, start, octets)
}
pub fn decode_app_object_id(
data: &[u8],
offset: usize,
what: &'static 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: &'static 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(), T::UNSIGNED_EXCEEDS, what)?,
end,
))
}
#[cfg(feature = "alloc")]
pub fn decode_app_bit_string(
data: &[u8],
offset: usize,
what: &'static 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: &'static 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(), T::ENUMERATED_EXCEEDS, what)?,
end,
))
}
#[cfg(feature = "alloc")]
pub(crate) fn decode_app_canonical_enumerated<T: UnsignedWidth>(
data: &[u8],
offset: usize,
what: &'static 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(), T::ENUMERATED_EXCEEDS, what)?,
end,
))
}
fn narrow_app<T: UnsignedWidth>(
value: u64,
contents: usize,
label: &'static str,
what: &'static str,
) -> Result<T, Error> {
T::try_from(value).map_err(|_| {
DecodeError::new(DecodingKind::OutOfRange, contents, label)
.within(what)
.into()
})
}
#[cfg(feature = "alloc")]
pub fn decode_app_character_string(
data: &[u8],
offset: usize,
what: &'static 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))
}
pub fn decode_app_character_string_ref<'a>(
data: &'a [u8],
offset: usize,
what: &'static str,
) -> Result<(primitives::CharacterStringRef<'a>, usize), Error> {
let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::CHARACTER_STRING, what)?;
Ok((primitives::CharacterStringRef::decode(octets)?, end))
}
#[cfg(all(test, feature = "alloc"))]
#[path = "tagged_frame_tests.rs"]
mod frame_tests;