use super::*;
use bacnet_encoding::constructed::{
decode_calendar_entry, decode_calendar_entry_list, decode_destination, decode_destination_list,
decode_device_object_property_reference, decode_event_notification_subscription,
encode_calendar_entry, encode_calendar_entry_list, encode_destination, encode_destination_list,
encode_device_object_property_reference, encode_event_notification_subscription_list,
encode_recipient,
};
use bacnet_encoding::primitives::decode_application_value;
use bacnet_services::list_manipulation::ListElementRequest;
use bacnet_types::constructed::{
BACnetCalendarEntry, BACnetDestination, BACnetDeviceObjectPropertyReference,
BACnetEventNotificationSubscription,
};
use bacnet_types::error::RemoteError;
use bytes::Bytes;
use std::collections::hash_map::{Entry, HashMap};
use std::mem::{Discriminant, discriminant};
fn protocol_error(class: ErrorClass, code: ErrorCode) -> Error {
Error::protocol(class, code)
}
fn element_error(class: ErrorClass, code: ErrorCode, position: u32) -> Error {
Error::with_detail(
class,
code,
Some(ErrorDetail::FirstFailedElementNumber(position)),
)
}
fn invalid_data_type() -> Error {
protocol_error(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE)
}
fn position(index: usize) -> u32 {
u32::try_from(index).map_or(u32::MAX, |index| index.saturating_add(1))
}
pub fn handle_add_list_element(db: &mut ObjectDatabase, service_data: &[u8]) -> Result<(), Error> {
handle_list_element_observed(db, service_data, false, |_, _, _| {}).map(|_| ())
}
pub fn handle_remove_list_element(
db: &mut ObjectDatabase,
service_data: &[u8],
) -> Result<(), Error> {
handle_list_element_observed(db, service_data, true, |_, _, _| {}).map(|_| ())
}
#[derive(Clone, Copy)]
enum ElementCodec {
Values,
Destinations,
CalendarEntries,
References,
Subscriptions,
}
impl ElementCodec {
fn for_datatype(object_type: ObjectType, property: PropertyIdentifier) -> Self {
match property {
PropertyIdentifier::RECIPIENT_LIST
if matches!(
object_type,
ObjectType::NOTIFICATION_CLASS | ObjectType::NOTIFICATION_FORWARDER
) =>
{
Self::Destinations
}
PropertyIdentifier::DATE_LIST if object_type == ObjectType::CALENDAR => {
Self::CalendarEntries
}
PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES
if object_type == ObjectType::SCHEDULE =>
{
Self::References
}
PropertyIdentifier::SUBSCRIBED_RECIPIENTS
if object_type == ObjectType::NOTIFICATION_FORWARDER =>
{
Self::Subscriptions
}
_ => Self::Values,
}
}
fn holds(self, stored: &PropertyValue) -> bool {
match self {
Self::Values | Self::CalendarEntries => matches!(stored, PropertyValue::List(_)),
Self::Destinations | Self::References | Self::Subscriptions => {
matches!(stored, PropertyValue::ApplicationData(_))
}
}
}
fn decode(self, elements: &[u8]) -> Result<Elements, u32> {
match self {
Self::Values => decode_each(elements, decode_application_value).map(Elements::Values),
Self::Destinations => {
decode_each(elements, decode_destination).map(Elements::Destinations)
}
Self::CalendarEntries => {
decode_each(elements, decode_calendar_entry).map(Elements::CalendarEntries)
}
Self::References => decode_each(elements, decode_device_object_property_reference)
.map(Elements::References),
Self::Subscriptions => decode_each(elements, decode_event_notification_subscription)
.map(Elements::Subscriptions),
}
}
fn stored(self, current: &PropertyValue) -> Result<Elements, Error> {
match (self, current) {
(Self::Values, PropertyValue::List(items)) => Ok(Elements::Values(items.clone())),
(Self::Destinations, PropertyValue::ApplicationData(bytes)) => {
decode_destination_list(bytes)
.map(Elements::Destinations)
.map_err(|_| invalid_data_type())
}
(Self::CalendarEntries, PropertyValue::List(items)) => {
let mut entries = Vec::with_capacity(items.len());
for item in items {
let PropertyValue::ApplicationData(bytes) = item else {
return Err(invalid_data_type());
};
entries.extend(
decode_calendar_entry_list(bytes).map_err(|_| invalid_data_type())?,
);
}
Ok(Elements::CalendarEntries(entries))
}
(Self::References, PropertyValue::ApplicationData(bytes)) => {
decode_each(bytes, decode_device_object_property_reference)
.map(Elements::References)
.map_err(|_| invalid_data_type())
}
(Self::Subscriptions, PropertyValue::ApplicationData(bytes)) => {
decode_each(bytes, decode_event_notification_subscription)
.map(Elements::Subscriptions)
.map_err(|_| invalid_data_type())
}
_ => unreachable!("list_target admits only the form the codec edits"),
}
}
}
fn decode_each<T>(
elements: &[u8],
decode: impl Fn(&[u8], usize) -> Result<(T, usize), Error>,
) -> Result<Vec<T>, u32> {
let mut decoded = Vec::new();
let mut offset = 0;
while offset < elements.len() {
let (element, next) = decode(elements, offset).map_err(|_| position(decoded.len()))?;
decoded.push(element);
offset = next;
}
Ok(decoded)
}
enum Elements {
Values(Vec<PropertyValue>),
Destinations(Vec<BACnetDestination>),
CalendarEntries(Vec<BACnetCalendarEntry>),
References(Vec<BACnetDeviceObjectPropertyReference>),
Subscriptions(Vec<BACnetEventNotificationSubscription>),
}
#[derive(Default)]
struct Added {
kept: usize,
positions: Vec<u32>,
renewed: Vec<(usize, u32)>,
}
impl Added {
fn request_position(&self, named: u32) -> Option<u32> {
let index = usize::try_from(named).ok()?.checked_sub(1)?;
if let Some(&(_, position)) = self.renewed.iter().rev().find(|(at, _)| *at == index) {
return Some(position);
}
index
.checked_sub(self.kept)
.and_then(|index| self.positions.get(index).copied())
}
fn first_position(&self) -> Option<u32> {
let renewed = self.renewed.iter().map(|&(_, position)| position).min();
match (self.positions.first().copied(), renewed) {
(Some(added), Some(renewed)) => Some(added.min(renewed)),
(added, renewed) => added.or(renewed),
}
}
}
struct Edited {
value: PropertyValue,
added: Added,
}
impl Elements {
fn apply(self, edits: Elements, remove: bool) -> Result<Edited, Error> {
match (self, edits) {
(Self::Values(stored), Self::Values(edits)) => {
let kinds: HashSet<Discriminant<PropertyValue>> =
stored.iter().map(discriminant).collect();
let (list, added) = edit(stored, edits, remove, encode_property_value, |value| {
kinds.is_empty() || kinds.contains(&discriminant(value))
})?;
Ok(Edited {
value: PropertyValue::List(list),
added,
})
}
(Self::Destinations(stored), Self::Destinations(edits)) => {
let (list, added) = edit(stored, edits, remove, encode_destination, |_| true)?;
let mut bytes = BytesMut::new();
encode_destination_list(&mut bytes, &list)?;
Ok(Edited {
value: PropertyValue::ApplicationData(bytes.to_vec()),
added,
})
}
(Self::CalendarEntries(stored), Self::CalendarEntries(edits)) => {
let (list, added) = edit(
stored,
edits,
remove,
|buf, entry| {
encode_calendar_entry(buf, entry);
Ok(())
},
|_| true,
)?;
let mut bytes = BytesMut::new();
encode_calendar_entry_list(&mut bytes, &list);
Ok(Edited {
value: PropertyValue::ApplicationData(bytes.to_vec()),
added,
})
}
(Self::References(stored), Self::References(edits)) => {
let (list, added) = edit(
stored,
edits,
remove,
|buf, member| {
encode_device_object_property_reference(buf, member);
Ok(())
},
|_| true,
)?;
let mut bytes = BytesMut::new();
for member in &list {
encode_device_object_property_reference(&mut bytes, member);
}
Ok(Edited {
value: PropertyValue::ApplicationData(bytes.to_vec()),
added,
})
}
(Self::Subscriptions(stored), Self::Subscriptions(edits)) => {
let (list, added) = edit_subscriptions(stored, edits, remove)?;
let mut bytes = BytesMut::new();
encode_event_notification_subscription_list(&mut bytes, &list)?;
Ok(Edited {
value: PropertyValue::ApplicationData(bytes.to_vec()),
added,
})
}
_ => unreachable!("one codec decodes both lists"),
}
}
}
fn edit<T>(
mut stored: Vec<T>,
edits: Vec<T>,
remove: bool,
encode: impl Fn(&mut BytesMut, &T) -> Result<(), Error>,
datatype_matches: impl Fn(&T) -> bool,
) -> Result<(Vec<T>, Added), Error> {
let key = |element: &T| {
let mut buf = BytesMut::new();
encode(&mut buf, element).map(|()| buf.freeze())
};
let keys = stored
.iter()
.map(key)
.collect::<Result<Vec<Bytes>, Error>>()
.map_err(|_| invalid_data_type())?;
let request_key = |index: usize, element: &T| {
key(element).map_err(|_| {
element_error(
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
position(index),
)
})
};
if remove {
let present: HashSet<&Bytes> = keys.iter().collect();
let mut removed = HashSet::with_capacity(edits.len());
for (index, element) in edits.iter().enumerate() {
if !datatype_matches(element) {
return Err(element_error(
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
position(index),
));
}
let element_key = request_key(index, element)?;
if !present.contains(&element_key) {
return Err(element_error(
ErrorClass::SERVICES,
ErrorCode::LIST_ELEMENT_NOT_FOUND,
position(index),
));
}
removed.insert(element_key);
}
let list = stored
.into_iter()
.zip(&keys)
.filter(|(_, key)| !removed.contains(*key))
.map(|(element, _)| element)
.collect();
return Ok((list, Added::default()));
}
let mut added = Added {
kept: stored.len(),
..Added::default()
};
let mut present: HashSet<Bytes> = keys.into_iter().collect();
for (index, element) in edits.into_iter().enumerate() {
if present.insert(request_key(index, &element)?) {
added.positions.push(position(index));
stored.push(element);
}
}
Ok((stored, added))
}
fn edit_subscriptions(
mut stored: Vec<BACnetEventNotificationSubscription>,
edits: Vec<BACnetEventNotificationSubscription>,
remove: bool,
) -> Result<(Vec<BACnetEventNotificationSubscription>, Added), Error> {
let key = |subscription: &BACnetEventNotificationSubscription| {
let mut buf = BytesMut::new();
encode_recipient(&mut buf, &subscription.recipient)
.expect("decoded recipients fit BACnetAddress::MAX_MAC_LEN");
buf.extend_from_slice(&subscription.process_identifier.to_be_bytes());
buf.freeze()
};
let mut named: HashMap<Bytes, usize> = stored
.iter()
.enumerate()
.map(|(index, subscription)| (key(subscription), index))
.collect();
if remove {
let mut removed = HashSet::with_capacity(edits.len());
for (index, element) in edits.iter().enumerate() {
let element_key = key(element);
if !named.contains_key(&element_key) {
return Err(element_error(
ErrorClass::SERVICES,
ErrorCode::LIST_ELEMENT_NOT_FOUND,
position(index),
));
}
removed.insert(element_key);
}
stored.retain(|subscription| !removed.contains(&key(subscription)));
return Ok((stored, Added::default()));
}
let mut added = Added {
kept: stored.len(),
..Added::default()
};
for (index, element) in edits.into_iter().enumerate() {
match named.entry(key(&element)) {
Entry::Occupied(entry) => {
let at = *entry.get();
stored[at] = element;
added.renewed.push((at, position(index)));
}
Entry::Vacant(entry) => {
entry.insert(stored.len());
added.positions.push(position(index));
stored.push(element);
}
}
}
Ok((stored, added))
}
fn object_refusal(error: Error, remove: bool, added: &Added) -> Error {
let (class, code, named) = match error {
Error::Remote(RemoteError::Error { class, code }) => (class, code, None),
Error::Structured {
class,
code,
detail,
} => match *detail {
ErrorDetail::FirstFailedElementNumber(named) => (class, code, Some(named)),
_ => (class, code, None),
},
other => return other,
};
let code = if !remove
&& class == ErrorClass::RESOURCES
&& code == ErrorCode::NO_SPACE_TO_WRITE_PROPERTY
{
ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT
} else {
code
};
let about_elements = [
ErrorCode::INVALID_DATA_TYPE,
ErrorCode::DATATYPE_NOT_SUPPORTED,
ErrorCode::INVALID_DATA_ENCODING,
ErrorCode::VALUE_OUT_OF_RANGE,
ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT,
]
.contains(&code);
let element = match named {
_ if remove => None,
Some(named) => added.request_position(named),
None => added.first_position().filter(|_| about_elements),
};
Error::with_detail(
class,
code,
element.map(ErrorDetail::FirstFailedElementNumber),
)
}
struct Refusal {
error: Error,
current: Option<PropertyValue>,
}
impl Refusal {
fn unread(error: Error) -> Self {
Self {
error,
current: None,
}
}
fn read(class: ErrorClass, code: ErrorCode, current: PropertyValue) -> Self {
Self {
error: protocol_error(class, code),
current: Some(current),
}
}
}
fn list_target(
db: &ObjectDatabase,
request: &ListElementRequest,
codec: ElementCodec,
) -> Result<PropertyValue, Refusal> {
let object = db.get(&request.object_identifier).ok_or_else(|| {
Refusal::unread(protocol_error(
ErrorClass::OBJECT,
ErrorCode::UNKNOWN_OBJECT,
))
})?;
let property = request.property_identifier;
let index = request.property_array_index;
let read = |index| {
object
.read_property(property, index)
.map_err(Refusal::unread)
};
if index.is_some() && !object.is_array_property(property) {
return Err(Refusal::read(
ErrorClass::PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
read(None)?,
));
}
let current = read(index)?;
if index.is_some() || !object.is_list_property(property) {
return Err(Refusal::read(
ErrorClass::SERVICES,
ErrorCode::PROPERTY_IS_NOT_A_LIST,
current,
));
}
if !codec.holds(¤t) {
return Err(Refusal::read(
ErrorClass::PROPERTY,
ErrorCode::WRITE_ACCESS_DENIED,
current,
));
}
Ok(current)
}
fn localize_edits(db: &ObjectDatabase, edits: &mut Elements) {
if let Elements::References(members) = edits {
let local = db.local_device();
for member in members {
crate::local_references::localize_member(member, local);
}
}
}
pub(crate) fn edited_list_value(
db: &ObjectDatabase,
service_data: &[u8],
remove: bool,
) -> Option<PropertyValue> {
let request = ListElementRequest::decode(service_data).ok()?;
let codec = ElementCodec::for_datatype(
request.object_identifier.object_type(),
request.property_identifier,
);
let current = list_target(db, &request, codec).ok()?;
let mut edits = codec.decode(&request.list_of_elements).ok()?;
localize_edits(db, &mut edits);
let stored = codec.stored(¤t).ok()?;
stored.apply(edits, remove).ok().map(|edited| edited.value)
}
pub(crate) fn handle_list_element_observed(
db: &mut ObjectDatabase,
service_data: &[u8],
remove: bool,
mut before: impl FnMut(&ObjectDatabase, &ListElementRequest, Option<&PropertyValue>),
) -> Result<ObjectIdentifier, Error> {
let request = ListElementRequest::decode(service_data).map_err(Error::into_request_reject)?;
let codec = ElementCodec::for_datatype(
request.object_identifier.object_type(),
request.property_identifier,
);
let target = list_target(db, &request, codec);
let edits = codec.decode(&request.list_of_elements);
let current = match target {
Ok(current) => current,
Err(Refusal { error, current }) => {
if edits.is_ok() {
before(db, &request, current.as_ref());
}
return Err(error);
}
};
let mut edits = edits.map_err(|position| {
element_error(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_TYPE, position)
})?;
localize_edits(db, &mut edits);
let stored = codec.stored(¤t)?;
before(db, &request, Some(¤t));
let Edited { value, added } = stored.apply(edits, remove)?;
db.get_mut(&request.object_identifier)
.expect("readable object")
.write_property(
request.property_identifier,
request.property_array_index,
value,
None,
)
.map_err(|error| object_refusal(error, remove, &added))?;
Ok(request.object_identifier)
}