use bacnet_types::error::{DecodeError, DecodingKind, Error};
use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
use super::{
charset, decode_bit_string_ref, decode_double, decode_real, decode_signed, decode_unsigned,
fixed_length_label, invalid_utf8, unknown_charset,
};
use crate::tags::{self, Tag, TagClass, app_tag};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CharacterStringRef<'a> {
contents: &'a [u8],
}
impl<'a> CharacterStringRef<'a> {
pub fn decode(contents: &'a [u8]) -> Result<Self, Error> {
check_character_string(contents)?;
Ok(Self { contents })
}
pub(super) fn unchecked(contents: &'a [u8]) -> Self {
Self { contents }
}
pub fn charset(&self) -> u8 {
self.contents.first().copied().unwrap_or(charset::UTF8)
}
pub fn bytes(&self) -> &'a [u8] {
self.contents.get(1..).unwrap_or_default()
}
pub fn contents(&self) -> &'a [u8] {
self.contents
}
pub fn as_str(&self) -> Option<&'a str> {
if self.charset() != charset::UTF8 {
return None;
}
core::str::from_utf8(self.bytes()).ok()
}
}
pub(super) fn check_character_string(contents: &[u8]) -> Result<(), Error> {
let Some((&charset_id, payload)) = contents.split_first() else {
return Err(Error::decoding(
0,
"CharacterString requires at least 1 byte for charset",
));
};
match charset_id {
charset::UTF8 => {
core::str::from_utf8(payload).map_err(invalid_utf8)?;
}
charset::IBM_MICROSOFT_DBCS if payload.len() < 2 => {
return Err(Error::decoding(
1,
"DBCS CharacterString requires a two-octet code page",
));
}
charset::UCS2 => {
let (characters, rest) = payload.as_chunks::<2>();
if !rest.is_empty() {
return Err(Error::decoding(1, "UCS-2 data must have even length"));
}
let not_scalar = |c: &[u8; 2]| char::from_u32(u16::from_be_bytes(*c).into()).is_none();
if let Some(index) = characters.iter().position(not_scalar) {
return Err(Error::decoding(1 + index * 2, "invalid UCS-2 code point"));
}
}
charset::UCS4 => {
let (characters, rest) = payload.as_chunks::<4>();
if !rest.is_empty() {
return Err(Error::decoding(
1,
"UCS-4 CharacterString length must be a multiple of four",
));
}
let not_scalar = |c: &[u8; 4]| char::from_u32(u32::from_be_bytes(*c)).is_none();
if let Some(index) = characters.iter().position(not_scalar) {
return Err(Error::decoding(1 + index * 4, "invalid UCS-4 code point"));
}
}
charset::IBM_MICROSOFT_DBCS | charset::JIS_X_0208 | charset::ISO_8859_1 => {}
other => return Err(unknown_charset(other)),
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ApplicationValueRef<'a> {
Null,
Boolean(bool),
Unsigned(u64),
Signed(i32),
Real(f32),
Double(f64),
OctetString(&'a [u8]),
CharacterString(CharacterStringRef<'a>),
BitString {
unused_bits: u8,
data: &'a [u8],
},
Enumerated(u32),
Date(Date),
Time(Time),
ObjectIdentifier(ObjectIdentifier),
}
pub fn decode_application_value_ref(
data: &[u8],
offset: usize,
) -> Result<(ApplicationValueRef<'_>, usize), Error> {
let (tag, content_start) = tags::decode_tag(data, offset)?;
if tag.class == TagClass::Context {
return Err(Error::invalid_tag(
offset,
"expected an application-tagged value, found a context tag",
));
}
let (value, end) = decode_application_primitive::<true>(data, offset, tag, content_start)?;
if let ApplicationValueRef::CharacterString(text) = value {
let checked =
CharacterStringRef::decode(text.contents()).map_err(rebased(content_start))?;
return Ok((ApplicationValueRef::CharacterString(checked), end));
}
Ok((value, end))
}
pub(super) fn rebased(base: usize) -> impl Fn(Error) -> Error {
move |error| match error {
Error::Decoding(mut refused) => {
refused.offset = refused.offset.saturating_add(base);
refused.into()
}
other => other,
}
}
pub(super) fn decode_application_primitive<const IN_DATA: bool>(
data: &[u8],
offset: usize,
tag: Tag,
content_start: usize,
) -> Result<(ApplicationValueRef<'_>, usize), Error> {
if tag.is_opening || tag.is_closing {
return Err(Error::invalid_tag(offset, "unexpected opening/closing tag"));
}
if tag.number == app_tag::NULL && tag.length != 0 {
return Err(Error::decoding(
offset,
"application NULL must have no contents",
));
}
if tag.number == app_tag::BOOLEAN && tag.length > 1 {
return Err(Error::decoding(
offset,
"application BOOLEAN L/V/T must be 0 or 1",
));
}
let content_end = content_start
.checked_add(tag.length as usize)
.ok_or_else(|| Error::decoding(content_start, "length overflow"))?;
if tag.number == app_tag::BOOLEAN {
return Ok((ApplicationValueRef::Boolean(tag.length != 0), content_start));
}
if let Some((label, octets)) = fixed_length_label(tag.number)
&& tag.length != octets
{
return Err(
DecodeError::new(DecodingKind::InvalidEncoding, offset, label)
.with_value(tag.length.into())
.into(),
);
}
if data.len() < content_end {
return Err(Error::buffer_too_short(content_end, data.len()));
}
let content = &data[content_start..content_end];
let at = |error| {
if IN_DATA {
rebased(content_start)(error)
} else {
error
}
};
let value = match tag.number {
app_tag::NULL => ApplicationValueRef::Null,
app_tag::UNSIGNED => ApplicationValueRef::Unsigned(decode_unsigned(content).map_err(at)?),
app_tag::SIGNED => ApplicationValueRef::Signed(decode_signed(content).map_err(at)?),
app_tag::REAL => ApplicationValueRef::Real(decode_real(content).map_err(at)?),
app_tag::DOUBLE => ApplicationValueRef::Double(decode_double(content).map_err(at)?),
app_tag::OCTET_STRING => ApplicationValueRef::OctetString(content),
app_tag::CHARACTER_STRING => {
ApplicationValueRef::CharacterString(CharacterStringRef::unchecked(content))
}
app_tag::BIT_STRING => {
let (unused_bits, data) = decode_bit_string_ref(content).map_err(at)?;
ApplicationValueRef::BitString { unused_bits, data }
}
app_tag::ENUMERATED => {
let value = u32::try_from(decode_unsigned(content).map_err(at)?)
.map_err(|_| Error::out_of_range(content_start, "ENUMERATED exceeds u32"))?;
ApplicationValueRef::Enumerated(value)
}
app_tag::DATE => ApplicationValueRef::Date(Date::decode(content).map_err(at)?),
app_tag::TIME => ApplicationValueRef::Time(Time::decode(content).map_err(at)?),
app_tag::OBJECT_IDENTIFIER => {
ApplicationValueRef::ObjectIdentifier(ObjectIdentifier::decode(content).map_err(at)?)
}
other => {
return Err(DecodeError::new(
DecodingKind::InvalidTag,
offset,
"unknown application tag number {}",
)
.with_value(other.into())
.into());
}
};
Ok((value, content_end))
}
#[cfg(test)]
mod tests;