use super::super::*;
use super::{make_dest_device, make_time};
use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
use bacnet_types::MacAddr;
fn make_dest_address(network_number: u16, mac: &[u8]) -> BACnetDestination {
BACnetDestination {
valid_days: DaysOfWeek::all(),
from_time: make_time(0, 0),
to_time: make_time(23, 59),
recipient: BACnetRecipient::Address(BACnetAddress {
network_number,
mac_address: MacAddr::from_slice(mac),
}),
process_identifier: 1,
issue_confirmed_notifications: false,
transitions: EventTransitionBits::all(),
}
}
#[test]
fn recipient_address_preserves_network_number_all_forms() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let mac = MacAddr::from_slice(&[192u8, 168, 1, 100, 0xBA, 0xC0]);
nc.add_destination(make_dest_address(0xBAC0, &mac)).unwrap();
let val = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
let PropertyValue::ApplicationData(bytes) = &val else {
panic!("Expected ApplicationData");
};
let decoded = bacnet_encoding::constructed::decode_destination_list(bytes).unwrap();
match &decoded[0].recipient {
BACnetRecipient::Address(addr) => {
assert_eq!(addr.network_number, 0xBAC0);
assert_eq!(addr.mac_address, mac);
}
other => panic!("expected Address recipient, got {other:?}"),
}
nc.write_property(PropertyIdentifier::RECIPIENT_LIST, None, val, None)
.unwrap();
match &nc.recipient_list[0].recipient {
BACnetRecipient::Address(addr) => {
assert_eq!(addr.network_number, 0xBAC0, "network number must survive");
assert_eq!(addr.mac_address, mac);
}
other => panic!("expected Address recipient, got {other:?}"),
}
let mut nc2 = NotificationClass::new(2, "NC-2").unwrap();
nc2.add_destination(make_dest_address(0xFFFF, &[])).unwrap();
nc2.add_destination(make_dest_address(0, &[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]))
.unwrap();
nc2.add_destination(make_dest_address(
1000,
&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0],
))
.unwrap();
let val2 = nc2
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
nc2.write_property(PropertyIdentifier::RECIPIENT_LIST, None, val2, None)
.unwrap();
let stored: Vec<u16> = nc2
.recipient_list
.iter()
.map(|d| match &d.recipient {
BACnetRecipient::Address(a) => a.network_number,
_ => 0xFFFF,
})
.collect();
assert_eq!(
stored,
vec![0xFFFF, 0, 1000],
"all three network forms survive"
);
}
#[test]
fn recipient_list_indexed_write_rejected_list_unchanged() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.add_destination(make_dest_device(10)).unwrap();
nc.add_destination(make_dest_device(20)).unwrap();
nc.add_destination(make_dest_device(30)).unwrap();
let mut framed = bytes::BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(&mut framed, &[make_dest_device(99)])
.unwrap();
let result = nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
Some(2),
PropertyValue::ApplicationData(framed.to_vec()),
None,
);
match result.unwrap_err() {
Error::Protocol { class, code } => {
assert_eq!(
class,
bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32
);
assert_eq!(
code,
bacnet_types::enums::ErrorCode::PROPERTY_IS_NOT_AN_ARRAY.to_raw() as u32
);
}
other => panic!("expected PROPERTY/PROPERTY_IS_NOT_AN_ARRAY, got {other:?}"),
}
assert_eq!(nc.recipient_list.len(), 3);
let instances: Vec<u32> = nc
.recipient_list
.iter()
.map(|d| match &d.recipient {
BACnetRecipient::Device(oid) => oid.instance_number(),
other => panic!("expected Device recipient, got {other:?}"),
})
.collect();
assert_eq!(instances, vec![10, 20, 30]);
}
#[test]
fn recipient_list_malformed_tail_fails_whole_decode_no_prefix_delivery() {
let good = make_dest_device(10);
let mut bytes = bytes::BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(&mut bytes, &[good]).unwrap();
let mut framed = bytes.to_vec();
framed.push(0x5E);
let val = PropertyValue::ApplicationData(framed);
assert!(decode_destination_list_pv(&val).is_err());
let hits = filter_recipient_list(
&val,
EventTransition::ToOffnormal,
DaysOfWeek::MONDAY,
&make_time(12, 0),
);
assert!(
hits.is_empty(),
"malformed list must NOT deliver to the valid prefix"
);
}
#[test]
fn routing_skips_delivery_when_stored_recipient_list_is_malformed() {
use std::borrow::Cow;
struct MalformedListObject {
oid: ObjectIdentifier,
}
impl BACnetObject for MalformedListObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
"malformed-nc"
}
fn read_property(
&self,
property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if property == PropertyIdentifier::NOTIFICATION_CLASS {
Ok(PropertyValue::Unsigned(1))
} else if property == PropertyIdentifier::RECIPIENT_LIST {
let mut framed = bytes::BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(
&mut framed,
&[make_dest_device(10)],
)
.unwrap();
let mut bytes = framed.to_vec();
bytes.push(0x5E); Ok(PropertyValue::ApplicationData(bytes))
} else {
Err(Error::Protocol {
class: bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32,
code: bacnet_types::enums::ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
})
}
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(Error::Protocol {
class: bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32,
code: bacnet_types::enums::ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
})
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[PropertyIdentifier::NOTIFICATION_CLASS])
}
}
let oid = ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, 1).unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(MalformedListObject { oid })).unwrap();
let None = get_notification_recipients_strict(
&db,
1,
EventTransition::ToOffnormal,
DaysOfWeek::MONDAY,
&make_time(12, 0),
) else {
panic!("undecodable Recipient_List must fail closed with None");
};
let empty = get_notification_recipients_strict(
&db,
99,
EventTransition::ToOffnormal,
DaysOfWeek::MONDAY,
&make_time(12, 0),
);
assert_eq!(empty, Some(Vec::new()));
}
#[test]
fn recipient_list_framed_eight_entry_write_round_trip() {
let mut nc = NotificationClass::new(9, "NC-9").unwrap();
let entries: Vec<BACnetDestination> = (0..8u32)
.map(|i| {
let mut d = make_dest_device(100 + i);
d.process_identifier = i;
if i % 2 == 1 {
d.recipient = BACnetRecipient::Address(BACnetAddress {
network_number: (1000 + i) as u16,
mac_address: MacAddr::from_slice(&[10, 0, i as u8, 1, 0xBA, 0xC0]),
});
}
d
})
.collect();
let mut framed = bytes::BytesMut::new();
bacnet_encoding::constructed::encode_destination_list(&mut framed, &entries).unwrap();
nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
None,
PropertyValue::ApplicationData(framed.to_vec()),
None,
)
.unwrap();
assert_eq!(nc.recipient_list, entries);
let val = nc
.read_property(PropertyIdentifier::RECIPIENT_LIST, None)
.unwrap();
assert_eq!(val, PropertyValue::ApplicationData(framed.to_vec()));
let mut db = ObjectDatabase::new();
db.add(Box::new(nc)).unwrap();
let noon = make_time(12, 0);
let hits = get_notification_recipients(
&db,
9,
EventTransition::ToOffnormal,
DaysOfWeek::MONDAY,
&noon,
);
assert_eq!(hits.len(), 8);
}
#[test]
fn recipient_list_framed_bad_recipient_tag_rejected() {
let mut framed = bytes::BytesMut::new();
bacnet_encoding::primitives::encode_app_bit_string(&mut framed, 1, &[0xFE]);
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(0, 0));
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(23, 59));
bacnet_encoding::primitives::encode_ctx_unsigned(&mut framed, 2, 1);
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let result = nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
None,
PropertyValue::ApplicationData(framed.to_vec()),
None,
);
assert!(result.is_err(), "recipient tag [2] must be rejected");
}
#[test]
fn recipient_list_framed_opening_without_closing_rejected() {
let mut framed = bytes::BytesMut::new();
bacnet_encoding::primitives::encode_app_bit_string(&mut framed, 1, &[0xFE]);
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(0, 0));
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(23, 59));
bacnet_encoding::tags::encode_opening_tag(&mut framed, 1);
bacnet_encoding::primitives::encode_app_unsigned(&mut framed, 0xBAC0);
bacnet_encoding::primitives::encode_app_octet_string(&mut framed, &[1, 2, 3]);
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let result = nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
None,
PropertyValue::ApplicationData(framed.to_vec()),
None,
);
assert!(result.is_err(), "unbalanced address must be rejected");
}
#[test]
fn write_recipient_list_rejects_malformed_address() {
let mk_entry = |address: &[u8]| {
let mut framed = bytes::BytesMut::new();
bacnet_encoding::primitives::encode_app_bit_string(&mut framed, 1, &[0b0111_1110]);
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(0, 0));
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(23, 59));
bacnet_encoding::tags::encode_opening_tag(&mut framed, 1);
framed.extend_from_slice(address);
bacnet_encoding::tags::encode_closing_tag(&mut framed, 1);
bacnet_encoding::primitives::encode_app_unsigned(&mut framed, 1);
bacnet_encoding::primitives::encode_app_boolean(&mut framed, false);
bacnet_encoding::primitives::encode_app_bit_string(&mut framed, 5, &[0b1110_0000]);
PropertyValue::ApplicationData(framed.to_vec())
};
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let cases = [
mk_entry(&[0x21, 100]),
mk_entry(&[0x11, 0x61, 0x01]),
];
for bad in cases {
let result = nc.write_property(PropertyIdentifier::RECIPIENT_LIST, None, bad, None);
assert!(result.is_err(), "malformed address must be rejected");
}
assert!(nc.recipient_list.is_empty());
}
#[test]
fn recipient_list_flat_form_write_refused() {
let flat_entry = PropertyValue::List(vec![
PropertyValue::BitString {
unused_bits: 1,
data: vec![0b1111_1110],
},
PropertyValue::Time(make_time(0, 0)),
PropertyValue::Time(make_time(23, 59)),
PropertyValue::ObjectIdentifier(ObjectIdentifier::new(ObjectType::DEVICE, 9).unwrap()),
PropertyValue::Unsigned(1),
PropertyValue::Boolean(false),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0b1110_0000],
},
]);
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
nc.add_destination(make_dest_device(10)).unwrap();
for (what, flat) in [
(
"one flat entry",
PropertyValue::List(vec![flat_entry.clone()]),
),
("an empty flat list", PropertyValue::List(Vec::new())),
] {
match nc.write_property(PropertyIdentifier::RECIPIENT_LIST, None, flat, None) {
Err(Error::Protocol { class, code }) => assert_eq!(
(class, code),
(
bacnet_types::enums::ErrorClass::PROPERTY.to_raw() as u32,
bacnet_types::enums::ErrorCode::INVALID_DATA_TYPE.to_raw() as u32
),
"{what}"
),
other => panic!("{what}: expected PROPERTY/INVALID_DATA_TYPE, got {other:?}"),
}
assert_eq!(nc.recipient_list(), [make_dest_device(10)], "{what}");
}
let flat = PropertyValue::List(vec![flat_entry]);
assert!(decode_destination_list_pv(&flat).is_err());
assert!(filter_recipient_list(
&flat,
EventTransition::ToOffnormal,
DaysOfWeek::MONDAY,
&make_time(12, 0),
)
.is_empty());
}
#[test]
fn written_valid_days_decodes_msb_first() {
let mut nc = NotificationClass::new(1, "NC-1").unwrap();
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 10).unwrap();
let mut framed = bytes::BytesMut::new();
bacnet_encoding::primitives::encode_app_bit_string(&mut framed, 1, &[0b1000_0000]);
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(0, 0));
bacnet_encoding::primitives::encode_app_time(&mut framed, &make_time(23, 59));
bacnet_encoding::primitives::encode_ctx_object_id(&mut framed, 0, &dev_oid);
bacnet_encoding::primitives::encode_app_unsigned(&mut framed, 7);
bacnet_encoding::primitives::encode_app_boolean(&mut framed, false);
bacnet_encoding::primitives::encode_app_bit_string(&mut framed, 5, &[0b1000_0000]);
nc.write_property(
PropertyIdentifier::RECIPIENT_LIST,
None,
PropertyValue::ApplicationData(framed.to_vec()),
None,
)
.unwrap();
let mut db = ObjectDatabase::new();
db.add(Box::new(nc)).unwrap();
let noon = make_time(12, 0);
let monday = get_notification_recipients(
&db,
1,
EventTransition::ToOffnormal,
DaysOfWeek::MONDAY,
&noon,
);
assert_eq!(monday.len(), 1, "Monday-only entry must match Monday");
let sunday = get_notification_recipients(
&db,
1,
EventTransition::ToOffnormal,
DaysOfWeek::SUNDAY,
&noon,
);
assert!(sunday.is_empty(), "Monday-only entry must not match Sunday");
}