#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use bacnet_encoding::constructed::tagged::misplaced_kind;
use bacnet_encoding::constructed::{
decode_bacnet_property_value_in_list_detailed, PropertyValueDecodeError,
PropertyValueDecodeFailure, PropertyValueDecodeStage,
};
use bacnet_encoding::tags;
use bacnet_types::constructed::BACnetObjectPropertyReference;
use bacnet_types::enums::RejectReason;
use bacnet_types::error::{DecodeError, Error};
use bacnet_types::primitives::ObjectIdentifier;
use crate::common::MAX_DECODED_ITEMS;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WritePropertyMultipleDecodeStage {
ObjectIdentifier,
PropertyList,
PropertyIdentifier,
ArrayIndex,
Value,
Priority,
PropertyListEnd,
ItemLimit,
}
impl WritePropertyMultipleDecodeStage {
pub fn name(self) -> &'static str {
match self {
Self::ObjectIdentifier => "WPM ObjectIdentifier",
Self::PropertyList => "WPM PropertyList",
Self::PropertyIdentifier => "WPM PropertyIdentifier",
Self::ArrayIndex => "WPM ArrayIndex",
Self::Value => "WPM Value",
Self::Priority => "WPM Priority",
Self::PropertyListEnd => "WPM PropertyListEnd",
Self::ItemLimit => "WPM ItemLimit",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WritePropertyMultipleFailureKind {
Syntax(RejectReason),
PriorityOutOfRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritePropertyMultipleCursorError {
pub offset: usize,
pub stage: WritePropertyMultipleDecodeStage,
pub kind: WritePropertyMultipleFailureKind,
pub first_failed_write_attempt: Option<BACnetObjectPropertyReference>,
pub error: DecodeError,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritePropertyAttempt {
pub reference: BACnetObjectPropertyReference,
pub value: Vec<u8>,
pub priority: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WritePropertyMultipleEvent {
ObjectStart(ObjectIdentifier),
WriteAttempt(WritePropertyAttempt),
ObjectEnd,
}
#[derive(Debug, Clone, Copy)]
enum State {
Object,
Properties(ObjectIdentifier),
Done,
Failed,
}
pub struct WritePropertyMultipleCursor<'a> {
data: &'a [u8],
offset: usize,
state: State,
object_count: usize,
property_count: usize,
total_attempts: usize,
}
impl<'a> WritePropertyMultipleCursor<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self {
data,
offset: 0,
state: State::Object,
object_count: 0,
property_count: 0,
total_attempts: 0,
}
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn next_event(
&mut self,
) -> Result<Option<WritePropertyMultipleEvent>, WritePropertyMultipleCursorError> {
match self.state {
State::Done => Ok(None),
State::Failed => Ok(None),
State::Object => {
if self.offset == self.data.len() {
self.state = State::Done;
return Ok(None);
}
if self.object_count >= MAX_DECODED_ITEMS {
return self.fail(self.limit_error("WPM object count exceeds limit"));
}
let oid = match self.decode_object_identifier() {
Ok(oid) => oid,
Err(error) => return self.fail(error),
};
if let Err(error) = self.decode_property_list_opening() {
return self.fail(error);
}
self.object_count += 1;
self.property_count = 0;
self.state = State::Properties(oid);
Ok(Some(WritePropertyMultipleEvent::ObjectStart(oid)))
}
State::Properties(oid) => {
if self.offset >= self.data.len() {
return self.fail(self.syntax_error(
self.offset,
WritePropertyMultipleDecodeStage::PropertyListEnd,
RejectReason::MISSING_REQUIRED_PARAMETER,
None,
"WPM property list is missing closing tag 1",
));
}
let (tag, tag_end) = match tags::decode_tag(self.data, self.offset) {
Ok(decoded) => decoded,
Err(error) => {
return self.fail(self.syntax_error(
self.offset,
WritePropertyMultipleDecodeStage::PropertyIdentifier,
tag_reason(&error),
None,
described(&error),
));
}
};
if tag.is_closing_tag(1) {
self.offset = tag_end;
self.state = State::Object;
return Ok(Some(WritePropertyMultipleEvent::ObjectEnd));
}
if tag.is_closing {
return self.fail(self.syntax_error(
self.offset,
WritePropertyMultipleDecodeStage::PropertyListEnd,
RejectReason::INVALID_TAG,
None,
"WPM property list has an unmatched closing tag",
));
}
if self.property_count >= MAX_DECODED_ITEMS
|| self.total_attempts >= MAX_DECODED_ITEMS
{
return self.fail(self.limit_error("WPM property count exceeds limit"));
}
let (property, end) = match decode_bacnet_property_value_in_list_detailed(
self.data,
self.offset,
1,
) {
Ok(decoded) => decoded,
Err(error) => return self.fail(self.property_error(oid, error)),
};
self.offset = end;
self.property_count += 1;
self.total_attempts += 1;
Ok(Some(WritePropertyMultipleEvent::WriteAttempt(
WritePropertyAttempt {
reference: BACnetObjectPropertyReference {
object_identifier: oid,
property_identifier: property.property_identifier.to_raw(),
property_array_index: property.property_array_index,
},
value: property.value,
priority: property.priority,
},
)))
}
}
}
fn decode_object_identifier(
&mut self,
) -> Result<ObjectIdentifier, WritePropertyMultipleCursorError> {
let start = self.offset;
let (tag, content_start) = tags::decode_tag(self.data, start).map_err(|error| {
self.syntax_error(
start,
WritePropertyMultipleDecodeStage::ObjectIdentifier,
tag_reason(&error),
None,
described(&error),
)
})?;
if !tag.is_context(0) {
return Err(self.syntax_error(
start,
WritePropertyMultipleDecodeStage::ObjectIdentifier,
misplaced_kind(self.data, start, &tag, Some(0)).reject_reason(),
None,
"WPM object identifier must use primitive context tag 0",
));
}
let end = content_start
.checked_add(tag.length as usize)
.filter(|end| *end <= self.data.len())
.ok_or_else(|| {
self.syntax_error(
content_start,
WritePropertyMultipleDecodeStage::ObjectIdentifier,
RejectReason::MISSING_REQUIRED_PARAMETER,
None,
"WPM object identifier payload is truncated",
)
})?;
if tag.length != 4 {
return Err(self.syntax_error(
content_start,
WritePropertyMultipleDecodeStage::ObjectIdentifier,
RejectReason::INVALID_DATA_ENCODING,
None,
"WPM object identifier must contain exactly four octets",
));
}
let oid = ObjectIdentifier::decode(&self.data[content_start..end]).map_err(|error| {
self.syntax_error(
content_start,
WritePropertyMultipleDecodeStage::ObjectIdentifier,
RejectReason::INVALID_DATA_ENCODING,
None,
described(&error),
)
})?;
self.offset = end;
Ok(oid)
}
fn decode_property_list_opening(&mut self) -> Result<(), WritePropertyMultipleCursorError> {
if self.offset >= self.data.len() {
return Err(self.syntax_error(
self.offset,
WritePropertyMultipleDecodeStage::PropertyList,
RejectReason::MISSING_REQUIRED_PARAMETER,
None,
"WPM object is missing its property list",
));
}
let start = self.offset;
let (tag, end) = tags::decode_tag(self.data, start).map_err(|error| {
self.syntax_error(
start,
WritePropertyMultipleDecodeStage::PropertyList,
tag_reason(&error),
None,
described(&error),
)
})?;
if !tag.is_opening_tag(1) {
return Err(self.syntax_error(
start,
WritePropertyMultipleDecodeStage::PropertyList,
misplaced_kind(self.data, start, &tag, Some(1)).reject_reason(),
None,
"WPM expected opening tag 1 for the property list",
));
}
self.offset = end;
Ok(())
}
fn property_error(
&self,
oid: ObjectIdentifier,
error: PropertyValueDecodeError,
) -> WritePropertyMultipleCursorError {
let stage = match error.stage {
PropertyValueDecodeStage::PropertyIdentifier => {
WritePropertyMultipleDecodeStage::PropertyIdentifier
}
PropertyValueDecodeStage::ArrayIndex => WritePropertyMultipleDecodeStage::ArrayIndex,
PropertyValueDecodeStage::Value => WritePropertyMultipleDecodeStage::Value,
PropertyValueDecodeStage::Priority => WritePropertyMultipleDecodeStage::Priority,
};
let reference = error
.reference_complete
.then(|| BACnetObjectPropertyReference {
object_identifier: oid,
property_identifier: error
.property_identifier
.expect("complete reference has a property")
.to_raw(),
property_array_index: error.property_array_index,
});
let kind = match error.kind {
PropertyValueDecodeFailure::Syntax(reason) => {
WritePropertyMultipleFailureKind::Syntax(reason)
}
PropertyValueDecodeFailure::PriorityOutOfRange => {
WritePropertyMultipleFailureKind::PriorityOutOfRange
}
};
cursor_error(
error.offset,
stage,
kind,
reference,
described(&error.error),
)
}
fn limit_error(&self, label: &'static str) -> WritePropertyMultipleCursorError {
self.syntax_error(
self.offset,
WritePropertyMultipleDecodeStage::ItemLimit,
RejectReason::BUFFER_OVERFLOW,
None,
label,
)
}
fn syntax_error(
&self,
offset: usize,
stage: WritePropertyMultipleDecodeStage,
reject_reason: RejectReason,
first_failed_write_attempt: Option<BACnetObjectPropertyReference>,
described: impl Into<Described>,
) -> WritePropertyMultipleCursorError {
cursor_error(
offset,
stage,
WritePropertyMultipleFailureKind::Syntax(reject_reason),
first_failed_write_attempt,
described.into(),
)
}
fn fail<T>(
&mut self,
error: WritePropertyMultipleCursorError,
) -> Result<T, WritePropertyMultipleCursorError> {
self.state = State::Failed;
Err(error)
}
}
struct Described(&'static str, [u64; 2]);
impl From<&'static str> for Described {
fn from(label: &'static str) -> Self {
Self(label, [0; 2])
}
}
fn described(error: &Error) -> Described {
let refused = error.to_decode_error(0);
Described(refused.label, refused.values)
}
fn cursor_error(
offset: usize,
stage: WritePropertyMultipleDecodeStage,
kind: WritePropertyMultipleFailureKind,
first_failed_write_attempt: Option<BACnetObjectPropertyReference>,
Described(label, values): Described,
) -> WritePropertyMultipleCursorError {
let mut error = DecodeError::new(super::failure_kind(kind), offset, label).within(stage.name());
error.values = values;
WritePropertyMultipleCursorError {
offset,
stage,
kind,
first_failed_write_attempt,
error,
}
}
fn tag_reason(error: &Error) -> RejectReason {
error.reject_reason().unwrap_or(RejectReason::INVALID_TAG)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::BACnetPropertyValue;
use crate::wpm::{WriteAccessSpecification, WritePropertyMultipleRequest};
use bacnet_encoding::constructed::encode_bacnet_property_value;
use bacnet_encoding::{primitives, tags};
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bytes::BytesMut;
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn property(property_identifier: PropertyIdentifier) -> BACnetPropertyValue {
let mut value = BytesMut::new();
primitives::encode_app_null(&mut value);
BACnetPropertyValue {
property_identifier,
property_array_index: None,
value: value.to_vec(),
priority: None,
}
}
fn encode(specs: Vec<WriteAccessSpecification>) -> BytesMut {
let mut data = BytesMut::new();
for spec in specs {
primitives::encode_ctx_object_id(&mut data, 0, &spec.object_identifier);
tags::encode_opening_tag(&mut data, 1);
for property in spec.list_of_properties {
encode_bacnet_property_value(&property, &mut data);
}
tags::encode_closing_tag(&mut data, 1);
}
data
}
fn attempts(data: &[u8]) -> Vec<WritePropertyAttempt> {
let mut cursor = WritePropertyMultipleCursor::new(data);
let mut attempts = Vec::new();
while let Some(event) = cursor.next_event().unwrap() {
if let WritePropertyMultipleEvent::WriteAttempt(attempt) = event {
attempts.push(attempt);
}
}
attempts
}
#[test]
fn yields_multi_object_and_property_attempts_in_wire_order() {
let first = oid(ObjectType::ANALOG_OUTPUT, 1);
let second = oid(ObjectType::BINARY_OUTPUT, 2);
let data = encode(vec![
WriteAccessSpecification {
object_identifier: first,
list_of_properties: vec![
property(PropertyIdentifier::DESCRIPTION),
property(PropertyIdentifier::PRESENT_VALUE),
],
},
WriteAccessSpecification {
object_identifier: second,
list_of_properties: vec![property(PropertyIdentifier::OUT_OF_SERVICE)],
},
]);
let coordinates: Vec<_> = attempts(&data)
.into_iter()
.map(|attempt| {
(
attempt.reference.object_identifier,
attempt.reference.property_identifier,
)
})
.collect();
assert_eq!(
coordinates,
vec![
(first, PropertyIdentifier::DESCRIPTION.to_raw()),
(first, PropertyIdentifier::PRESENT_VALUE.to_raw()),
(second, PropertyIdentifier::OUT_OF_SERVICE.to_raw()),
]
);
}
#[test]
fn empty_request_and_empty_property_lists_are_noops() {
assert!(attempts(&[]).is_empty());
let object = oid(ObjectType::BINARY_VALUE, 1);
let data = encode(vec![WriteAccessSpecification {
object_identifier: object,
list_of_properties: vec![],
}]);
let mut cursor = WritePropertyMultipleCursor::new(&data);
assert_eq!(
cursor.next_event().unwrap(),
Some(WritePropertyMultipleEvent::ObjectStart(object))
);
assert_eq!(
cursor.next_event().unwrap(),
Some(WritePropertyMultipleEvent::ObjectEnd)
);
assert_eq!(cursor.next_event().unwrap(), None);
assert_eq!(
WritePropertyMultipleRequest::decode(&data)
.unwrap()
.list_of_write_access_specs[0]
.list_of_properties,
vec![]
);
}
fn first_error(data: &[u8]) -> WritePropertyMultipleCursorError {
let mut cursor = WritePropertyMultipleCursor::new(data);
loop {
match cursor.next_event() {
Ok(Some(_)) => {}
Ok(None) => panic!("expected malformed request"),
Err(error) => return error,
}
}
}
#[test]
fn pre_write_failures_use_narrow_reject_classifications() {
let object = oid(ObjectType::BINARY_VALUE, 1);
let mut object_bytes = BytesMut::new();
primitives::encode_ctx_object_id(&mut object_bytes, 0, &object);
let mut wrong_context = object_bytes.clone();
wrong_context[0] = 0x1c;
assert_eq!(
first_error(&wrong_context).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::MISSING_REQUIRED_PARAMETER)
);
assert_eq!(
first_error(&object_bytes).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::MISSING_REQUIRED_PARAMETER)
);
let mut wrong_type = BytesMut::new();
primitives::encode_app_object_id(&mut wrong_type, &object);
assert_eq!(
first_error(&wrong_type).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::INVALID_TAG)
);
assert_eq!(
first_error(&[0x0c, 0, 0, 0]).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::MISSING_REQUIRED_PARAMETER)
);
let invalid_priority = encode(vec![WriteAccessSpecification {
object_identifier: object,
list_of_properties: vec![BACnetPropertyValue {
priority: Some(17),
..property(PropertyIdentifier::PRESENT_VALUE)
}],
}]);
let error = first_error(&invalid_priority);
assert_eq!(
error.kind,
WritePropertyMultipleFailureKind::PriorityOutOfRange
);
assert_eq!(error.stage, WritePropertyMultipleDecodeStage::Priority);
assert!(error.first_failed_write_attempt.is_some());
let mut unexpected = encode(vec![WriteAccessSpecification {
object_identifier: object,
list_of_properties: vec![property(PropertyIdentifier::DESCRIPTION)],
}]);
unexpected.truncate(unexpected.len() - 1);
unexpected.extend_from_slice(&[0x49, 0x00, 0x1f]);
let error = first_error(&unexpected);
assert_eq!(
error.kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::INVALID_TAG)
);
assert!(error.first_failed_write_attempt.is_some());
let mut malformed_value = object_bytes.clone();
tags::encode_opening_tag(&mut malformed_value, 1);
primitives::encode_ctx_unsigned(
&mut malformed_value,
0,
PropertyIdentifier::DESCRIPTION.to_raw() as u64,
);
tags::encode_opening_tag(&mut malformed_value, 2);
let mut cut_value = malformed_value.clone();
cut_value.extend_from_slice(&[0x75, 10, b'x', 0x2f, 0x1f]);
assert_eq!(
first_error(&cut_value).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::MISSING_REQUIRED_PARAMETER)
);
malformed_value.extend_from_slice(&[0x3E; 40]);
assert_eq!(
first_error(&malformed_value).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::INVALID_TAG)
);
}
#[test]
fn value_opening_failures_keep_the_completed_property_reference() {
let object = oid(ObjectType::BINARY_VALUE, 1);
let property = PropertyIdentifier::OBJECT_TYPE;
for (array_index, terminate_with_wrong_tag) in [(Some(4), false), (None, true)] {
let mut data = BytesMut::new();
primitives::encode_ctx_object_id(&mut data, 0, &object);
tags::encode_opening_tag(&mut data, 1);
primitives::encode_ctx_unsigned(&mut data, 0, property.to_raw() as u64);
if let Some(index) = array_index {
primitives::encode_ctx_unsigned(&mut data, 1, index as u64);
}
if terminate_with_wrong_tag {
tags::encode_closing_tag(&mut data, 1);
}
let error = first_error(&data);
assert_eq!(error.stage, WritePropertyMultipleDecodeStage::Value);
let reference = error
.first_failed_write_attempt
.expect("the property coordinate is complete before value framing");
assert_eq!(reference.object_identifier, object);
assert_eq!(reference.property_identifier, property.to_raw());
assert_eq!(reference.property_array_index, array_index);
}
}
#[test]
fn total_attempt_bound_prevents_unbounded_collection() {
let object = oid(ObjectType::BINARY_VALUE, 1);
let data = encode(vec![WriteAccessSpecification {
object_identifier: object,
list_of_properties: (0..=MAX_DECODED_ITEMS)
.map(|_| property(PropertyIdentifier::DESCRIPTION))
.collect(),
}]);
let error = first_error(&data);
assert_eq!(
error.kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::BUFFER_OVERFLOW)
);
assert_eq!(error.stage, WritePropertyMultipleDecodeStage::ItemLimit);
}
#[test]
fn unmatched_closing_tag_is_invalid_tag() {
let object = oid(ObjectType::BINARY_VALUE, 1);
let mut data = BytesMut::new();
primitives::encode_ctx_object_id(&mut data, 0, &object);
tags::encode_opening_tag(&mut data, 1);
tags::encode_closing_tag(&mut data, 2);
assert_eq!(
first_error(&data).kind,
WritePropertyMultipleFailureKind::Syntax(RejectReason::INVALID_TAG)
);
}
}