use super::*;
use bacnet_objects::loop_obj::LoopObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::EngineeringUnits;
use bacnet_types::error::RemoteError;
fn encode_value(value: PropertyValue) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_property_value(&mut buf, &value).unwrap();
buf.to_vec()
}
fn write_raw(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
property: PropertyIdentifier,
array_index: Option<u32>,
property_value: Vec<u8>,
) -> Result<(), Error> {
let request = WritePropertyRequest {
object_identifier: oid,
property_identifier: property,
property_array_index: array_index,
property_value,
priority: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_write_property(db, &buf).map(|_| ())
}
fn read_prop(
db: &ObjectDatabase,
oid: ObjectIdentifier,
property: PropertyIdentifier,
) -> PropertyValue {
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: property,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(db, &buf, &mut ack_buf).unwrap();
let raw = ReadPropertyACK::decode(&ack_buf).unwrap().property_value;
let mut values = Vec::new();
let mut offset = 0;
while offset < raw.len() {
let (value, new_offset) =
bacnet_encoding::primitives::decode_application_value(&raw, offset).unwrap();
values.push(value);
offset = new_offset;
}
match values.len() {
1 => values.pop().unwrap(),
_ => PropertyValue::List(values),
}
}
fn assert_refused(
db: &mut ObjectDatabase,
oid: ObjectIdentifier,
property: PropertyIdentifier,
raw_value: Vec<u8>,
expected_code: ErrorCode,
expected: PropertyValue,
context: &str,
) {
match write_raw(db, oid, property, None, raw_value).expect_err(context) {
Error::Remote(RemoteError::Error { class, code }) => {
assert_eq!(class, ErrorClass::PROPERTY, "{context}: wrong error class");
assert_eq!(code, expected_code, "{context}: wrong error code");
}
other => panic!("{context}: expected PROPERTY/{expected_code:?}, got {other:?}"),
}
assert_eq!(
read_prop(db, oid, property),
expected,
"{context}: refused write must leave the property unchanged"
);
}
#[test]
fn scalar_write_with_undecodable_trailing_element_is_refused_and_preserves() {
let mut db = ObjectDatabase::new();
let lo = LoopObject::new(1, "LOOP-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
let oid = lo.object_identifier();
db.add(Box::new(lo)).unwrap();
write_raw(
&mut db,
oid,
PropertyIdentifier::SETPOINT,
None,
encode_value(PropertyValue::Real(72.0)),
)
.unwrap();
let mut bytes = encode_value(PropertyValue::Real(50.0));
bytes.extend_from_slice(&[0xD1, 0x00]);
assert_refused(
&mut db,
oid,
PropertyIdentifier::SETPOINT,
bytes,
ErrorCode::INVALID_DATA_ENCODING,
PropertyValue::Real(72.0),
"scalar + undecodable trailing element",
);
}
#[test]
fn list_write_with_undecodable_trailing_element_is_refused_and_preserves() {
let mut db = make_db_with_msi();
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 1).unwrap();
write_raw(
&mut db,
oid,
PropertyIdentifier::ALARM_VALUES,
None,
vec![0x21, 2, 0x21, 3],
)
.unwrap();
assert_eq!(
read_prop(&db, oid, PropertyIdentifier::ALARM_VALUES),
PropertyValue::List(vec![PropertyValue::Unsigned(2), PropertyValue::Unsigned(3)])
);
assert_refused(
&mut db,
oid,
PropertyIdentifier::ALARM_VALUES,
vec![0x21, 4, 0x21, 5, 0xD1, 0x00],
ErrorCode::INVALID_DATA_ENCODING,
PropertyValue::List(vec![PropertyValue::Unsigned(2), PropertyValue::Unsigned(3)]),
"list + undecodable trailing element",
);
}
#[test]
fn list_write_with_tlv_truncated_tail_is_refused_before_the_value_decode() {
let mut db = make_db_with_msi();
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 1).unwrap();
write_raw(
&mut db,
oid,
PropertyIdentifier::ALARM_VALUES,
None,
vec![0x21, 2, 0x21, 3],
)
.unwrap();
let err = write_raw(
&mut db,
oid,
PropertyIdentifier::ALARM_VALUES,
None,
vec![0x21, 4, 0x21, 5, 0x21],
)
.unwrap_err();
assert!(
!matches!(err, Error::Remote(RemoteError::Error { .. })),
"a framing-level truncation is not an object-level protocol error: {err:?}"
);
assert_eq!(
read_prop(&db, oid, PropertyIdentifier::ALARM_VALUES),
PropertyValue::List(vec![PropertyValue::Unsigned(2), PropertyValue::Unsigned(3)]),
"truncated write must leave the list unchanged"
);
}
#[test]
fn well_formed_trailing_element_reaches_scalar_arm_as_list_and_is_refused() {
let mut db = ObjectDatabase::new();
let lo = LoopObject::new(1, "LOOP-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
let oid = lo.object_identifier();
db.add(Box::new(lo)).unwrap();
write_raw(
&mut db,
oid,
PropertyIdentifier::SETPOINT,
None,
encode_value(PropertyValue::Real(72.0)),
)
.unwrap();
let mut bytes = encode_value(PropertyValue::Real(50.0));
bytes.extend_from_slice(&encode_value(PropertyValue::Real(99.0)));
assert_refused(
&mut db,
oid,
PropertyIdentifier::SETPOINT,
bytes,
ErrorCode::INVALID_DATA_TYPE,
PropertyValue::Real(72.0),
"scalar + well-formed second element",
);
}
use bacnet_objects::value_types::{DateTimePatternValueObject, DateTimeValueObject};
use bacnet_types::primitives::{Date, Time};
const TEST_DATE: Date = Date {
year: 0x7E, month: 8,
day: 12,
day_of_week: 3,
};
const TEST_TIME: Time = Time {
hour: 9,
minute: 41,
second: 30,
hundredths: 0,
};
fn datetime_pv() -> PropertyValue {
PropertyValue::List(vec![
PropertyValue::Date(TEST_DATE),
PropertyValue::Time(TEST_TIME),
])
}
#[test]
fn datetime_value_present_value_and_priority_array_over_the_wire() {
let mut db = ObjectDatabase::new();
let obj = DateTimeValueObject::new(1, "DTV-1").unwrap();
let oid = obj.object_identifier();
db.add(Box::new(obj)).unwrap();
write_raw(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
None,
encode_value(datetime_pv()),
)
.unwrap();
assert_eq!(
read_prop(&db, oid, PropertyIdentifier::PRESENT_VALUE),
datetime_pv()
);
let later = PropertyValue::List(vec![
PropertyValue::Date(TEST_DATE),
PropertyValue::Time(Time {
hour: 12,
..TEST_TIME
}),
]);
let error = write_raw(
&mut db,
oid,
PropertyIdentifier::PRIORITY_ARRAY,
Some(2),
encode_value(later.clone()),
)
.unwrap_err();
assert!(
matches!(error, Error::Remote(RemoteError::Error { class, code }) if class == ErrorClass::PROPERTY && code == ErrorCode::WRITE_ACCESS_DENIED)
);
assert_eq!(
read_prop(&db, oid, PropertyIdentifier::PRESENT_VALUE),
datetime_pv()
);
}
#[test]
fn datetime_relinquish_default_over_the_wire_recaptures_present_value() {
let mut db = ObjectDatabase::new();
let obj = DateTimeValueObject::new(1, "DTV-1").unwrap();
let oid = obj.object_identifier();
db.add(Box::new(obj)).unwrap();
write_raw(
&mut db,
oid,
PropertyIdentifier::RELINQUISH_DEFAULT,
None,
encode_value(datetime_pv()),
)
.unwrap();
assert_eq!(
read_prop(&db, oid, PropertyIdentifier::RELINQUISH_DEFAULT),
datetime_pv()
);
assert_eq!(
read_prop(&db, oid, PropertyIdentifier::PRESENT_VALUE),
datetime_pv(),
"with an empty priority array, PV resolves to the written default"
);
let pat = DateTimePatternValueObject::new(2, "DTPV-2").unwrap();
let pat_oid = pat.object_identifier();
db.add(Box::new(pat)).unwrap();
write_raw(
&mut db,
pat_oid,
PropertyIdentifier::RELINQUISH_DEFAULT,
None,
encode_value(datetime_pv()),
)
.unwrap();
assert_eq!(
read_prop(&db, pat_oid, PropertyIdentifier::RELINQUISH_DEFAULT),
datetime_pv()
);
}
#[test]
fn datetime_write_with_mispaired_members_is_refused_and_preserves() {
let mut db = ObjectDatabase::new();
let obj = DateTimeValueObject::new(1, "DTV-1").unwrap();
let oid = obj.object_identifier();
db.add(Box::new(obj)).unwrap();
let baseline = read_prop(&db, oid, PropertyIdentifier::PRESENT_VALUE);
let bytes = encode_value(PropertyValue::List(vec![
PropertyValue::Date(TEST_DATE),
PropertyValue::Date(TEST_DATE),
]));
assert_refused(
&mut db,
oid,
PropertyIdentifier::PRESENT_VALUE,
bytes,
ErrorCode::INVALID_DATA_TYPE,
baseline,
"date+date is not a BACnetDateTime",
);
}