use super::*;
use bacnet_objects::object_profile::{ObjectProfile, TagsPersistence, TagsSnapshot};
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_types::constructed::BACnetNameValue;
use bacnet_types::enums::{ErrorClass, ErrorCode, RejectReason};
use bacnet_types::error::RemoteError;
use std::net::Ipv4Addr;
const TAGS: PropertyIdentifier = PropertyIdentifier::TAGS;
const LOCATION: PropertyIdentifier = PropertyIdentifier::PROFILE_LOCATION;
const NAME: PropertyIdentifier = PropertyIdentifier::PROFILE_NAME;
const EXHAUST: &[u8] = &[0x0d, 8, 0, b'e', b'x', b'h', b'a', b'u', b's', b't'];
const FLOOR: &[u8] = &[0x0d, 6, 0, b'f', b'l', b'o', b'o', b'r', 0x21, 3];
#[derive(Default)]
struct Store(AtomicUsize);
impl TagsPersistence for Store {
fn load(&self, _: ObjectIdentifier) -> Result<Option<TagsSnapshot>, Error> {
Ok(None)
}
fn save(&self, _: ObjectIdentifier, _: &TagsSnapshot) -> Result<(), Error> {
self.0.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
fn text(value: &str) -> Vec<u8> {
let mut bytes = vec![0x75, value.len() as u8 + 1, 0];
bytes.extend(value.as_bytes());
bytes
}
fn primitive(bytes: &[u8]) -> PropertyValue {
let (value, end) = bacnet_encoding::primitives::decode_application_value(bytes, 0).unwrap();
assert_eq!(end, bytes.len());
value
}
fn assert_reject<T: std::fmt::Debug>(result: Result<T, Error>) {
assert!(
matches!(result, Err(Error::Remote(RemoteError::Reject { reason })) if reason == RejectReason::UNRECOGNIZED_SERVICE),
"{result:?}"
);
}
fn assert_error<T: std::fmt::Debug>(result: Result<T, Error>, expected: ErrorCode) {
assert!(
matches!(result, Err(Error::Remote(RemoteError::Error { class, code })) if class == ErrorClass::PROPERTY && code == expected),
"{result:?}"
);
}
fn profiled_database(
identity: &crate::DeviceIdentity,
tags: bool,
store: Arc<Store>,
) -> ObjectDatabase {
let mut db = crate::identity::build_database_with_extra(identity, vec![]).unwrap();
let selected = oid(identity.instance());
let original = db.get(&selected).unwrap();
let name = original.object_name().to_owned();
let identity_rows = [
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::VENDOR_IDENTIFIER,
PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
PropertyIdentifier::SEGMENTATION_SUPPORTED,
PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED,
PropertyIdentifier::DEVICE_UUID,
PropertyIdentifier::OBJECT_LIST,
];
let before = identity_rows.map(|p| original.read_property(p, None).unwrap());
let mut object = DeviceObject::with_tags_persistence(
DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(
identity.instance(),
identity.vendor_id(),
)
.expect("valid Device identity")
.with_name(name)
.with_max_apdu(identity.max_apdu_length())
.expect("valid Device APDU")
.with_segmentation(identity.segmentation()),
..Default::default()
},
store,
)
.unwrap();
object.set_services_supported(identity.services());
object.set_device_uuid(identity.device_uuid());
object.set_object_list(vec![selected]);
object
.set_profile(ObjectProfile {
tags: tags.then(|| vec![BACnetNameValue::semantic("exhaust")]),
profile_location: Some("https://example.com/p.xdd".into()),
profile_name: Some("555-device".into()),
})
.unwrap();
assert_eq!(
identity_rows.map(|p| object.read_property(p, None).unwrap()),
before
);
db.add(Box::new(object)).unwrap();
assert_eq!(db.selected_device(), Some(selected));
db
}
#[tokio::test]
async fn device_profiles_read_but_tags_never_enter_endpoint_mutation_authority() {
for writes in [false, true] {
for provisioned in [false, true] {
let identity = crate::DeviceIdentity::new(123, 42).unwrap();
let store = Arc::new(Store::default());
let db = profiled_database(&identity, provisioned, store.clone());
let calls = Arc::new(AtomicUsize::new(0));
let mut endpoint =
crate::bip::BipEndpointBuilder::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST)
.role(SessionRole::ServerOnly)
.database(db)
.device(identity)
.build_session()
.unwrap();
if writes {
let calls = calls.clone();
endpoint = endpoint.with_device_writes(Arc::new(move |_| {
calls.fetch_add(1, Ordering::SeqCst);
true
}));
}
endpoint.start().await.unwrap();
let mut client = bacnet_client::client::BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.build()
.await
.unwrap();
let mac = bacnet_transport::bvll::encode_bip_mac(
[127, 0, 0, 1],
endpoint.bip_local_address().unwrap().port(),
);
let expected_services = if writes { vec![12, 15] } else { vec![12] };
for target in [oid(123), oid(ObjectIdentifier::MAX_INSTANCE)] {
for (p, bytes) in [
(LOCATION, text("https://example.com/p.xdd")),
(NAME, text("555-device")),
] {
assert_eq!(
client
.read_property(&mac, target, p, None)
.await
.unwrap()
.property_value,
bytes
);
}
let result = client.read_property(&mac, target, TAGS, None).await;
if provisioned {
assert_eq!(result.unwrap().property_value, EXHAUST);
assert_eq!(
client
.read_property(&mac, target, TAGS, Some(0))
.await
.unwrap()
.property_value,
[0x21, 1]
);
assert_eq!(
client
.read_property(&mac, target, TAGS, Some(1))
.await
.unwrap()
.property_value,
EXHAUST
);
} else {
assert_error(result, ErrorCode::UNKNOWN_PROPERTY);
}
let read = client
.read_property(
&mac,
target,
PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED,
None,
)
.await
.unwrap();
assert_eq!(services(primitive(&read.property_value)), expected_services);
let list = client
.read_property(&mac, target, PropertyIdentifier::PROPERTY_LIST, None)
.await
.unwrap()
.property_value;
let mut offset = 0;
let mut properties = Vec::new();
while offset < list.len() {
let (PropertyValue::Enumerated(p), next) =
bacnet_encoding::primitives::decode_application_value(&list, offset)
.unwrap()
else {
panic!("property identifier");
};
properties.push(p);
offset = next;
}
assert!(properties.windows(2).all(|p| p[0] < p[1]));
assert_eq!(properties.contains(&TAGS.to_raw()), provisioned);
assert!(
properties.contains(&LOCATION.to_raw()) && properties.contains(&NAME.to_raw())
);
assert!(!properties.contains(&PropertyIdentifier::PROPERTY_LIST.to_raw()));
let count = client
.read_property(&mac, target, PropertyIdentifier::PROPERTY_LIST, Some(0))
.await
.unwrap();
assert_eq!(
primitive(&count.property_value),
PropertyValue::Unsigned(properties.len() as u64)
);
for (index, p) in properties.iter().enumerate() {
let item = client
.read_property(
&mac,
target,
PropertyIdentifier::PROPERTY_LIST,
Some(index as u32 + 1),
)
.await
.unwrap();
assert_eq!(
primitive(&item.property_value),
PropertyValue::Enumerated(*p)
);
}
for p in [
PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS,
PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS,
] {
assert!(!properties.contains(&p.to_raw()));
assert_error(
client.read_property(&mac, target, p, None).await,
ErrorCode::UNKNOWN_PROPERTY,
);
}
assert!(
client
.read_property(
&mac,
target,
PropertyIdentifier::DEVICE_ADDRESS_BINDING,
None
)
.await
.unwrap()
.property_value
.is_empty()
);
}
let result = client
.write_property(&mac, oid(123), TAGS, None, FLOOR.to_vec(), None)
.await;
if writes {
assert_error(
result,
if provisioned {
ErrorCode::WRITE_ACCESS_DENIED
} else {
ErrorCode::UNKNOWN_PROPERTY
},
);
} else {
assert_reject(result);
}
assert_reject(
client
.write_property_multiple(
&mac,
vec![WriteAccessSpecification {
object_identifier: oid(123),
list_of_properties: vec![BACnetPropertyValue {
property_identifier: TAGS,
property_array_index: None,
value: FLOOR.to_vec(),
priority: None,
}],
}],
)
.await,
);
assert_eq!(calls.load(Ordering::SeqCst), 0);
assert_eq!(store.0.load(Ordering::SeqCst), 0);
if provisioned {
assert_eq!(
client
.read_property(&mac, oid(123), TAGS, None)
.await
.unwrap()
.property_value,
EXHAUST
);
}
if writes {
client
.write_property(
&mac,
oid(123),
PropertyIdentifier::DESCRIPTION,
None,
text("authorized"),
None,
)
.await
.unwrap();
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(
read(&endpoint, PropertyIdentifier::DESCRIPTION).await,
PropertyValue::CharacterString("authorized".into())
);
}
let served = client
.read_property(
&mac,
oid(123),
PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED,
None,
)
.await
.unwrap();
assert_eq!(
services(primitive(&served.property_value)),
expected_services
);
assert_eq!(store.0.load(Ordering::SeqCst), 0);
client.stop().await.unwrap();
endpoint.stop().await.unwrap();
}
}
}