bacnet_objects/notification_class/recipient_list.rs
1//! Recipient_List writes, the cap on how many destinations the list holds
2//! (#1098), and the same cap on the lists routing accepts (#1124).
3
4use bacnet_encoding::constructed::decode_destination;
5use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
6use bacnet_types::enums::{ErrorClass, ErrorCode};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::PropertyValue;
9
10use crate::common;
11
12/// The most destinations a Notification Class Recipient_List holds (#1098).
13///
14/// A site usually routes a class to a few front ends, operator workstations
15/// or gateways, and the AE-CRL-B building block (Annex K.2.25) asks for room
16/// for at least 8, so 32 leaves ample headroom. The bound also keeps the list
17/// small on the wire. A destination naming a device encodes in at most 27
18/// octets, and one naming an address with a MAC of up to 6 octets in at most
19/// 35, so a full list of either kind takes at most 1,120 octets and a
20/// ReadProperty of it fits B/IP's 1476-octet APDU unsegmented. An address
21/// MAC is at most [`BACnetAddress::MAX_MAC_LEN`] octets (#1124), so no
22/// destination takes more than 47 octets and no full list more than 1,504.
23///
24/// A write that would leave more destinations fails with RESOURCES /
25/// NO_SPACE_TO_WRITE_PROPERTY, naming the first destination past the cap;
26/// AddListElement reports that as NO_SPACE_TO_ADD_LIST_ELEMENT at the request
27/// element that brought it (Clause 15.1.1.3). `add_destination` refuses the
28/// same way, without the element.
29///
30/// Routing holds every Notification Class to the same bound, so each event
31/// transition fans out to at most this many notifications per class (#1124).
32/// Only a custom NOTIFICATION_CLASS object can serve a longer list, and the
33/// transition then goes to none of it: see
34/// [`RecipientLookupOutcome::RecipientListTooLong`](super::RecipientLookupOutcome::RecipientListTooLong).
35pub const MAX_RECIPIENT_LIST_DESTINATIONS: usize = 32;
36
37/// The refusal of a destination past [`MAX_RECIPIENT_LIST_DESTINATIONS`].
38pub(crate) fn no_space_error() -> Error {
39 common::protocol_error(ErrorClass::RESOURCES, ErrorCode::NO_SPACE_TO_WRITE_PROPERTY)
40}
41
42/// The refusal of a written list's destination at `index` (from 0), the first
43/// past the cap, named so ChangeList-Error can report it (#1048).
44fn past_the_cap(index: usize) -> Error {
45 common::at_list_element(no_space_error(), index)
46}
47
48/// Why a framed Recipient_List was not taken.
49pub(super) enum CappedListError {
50 /// A destination, or what follows the last one, does not decode.
51 Malformed,
52 /// The list holds more than [`MAX_RECIPIENT_LIST_DESTINATIONS`]; the
53 /// field is the index (from 0) of the first destination past the cap.
54 PastTheCap(usize),
55}
56
57/// Decode a framed Recipient_List (a BACnetLIST of BACnetDestination, Clause
58/// 12.21) of at most [`MAX_RECIPIENT_LIST_DESTINATIONS`] destinations, all or
59/// nothing. Destinations are checked in order, so the first one that fails to
60/// decode, or the first past the cap, decides the error. A destination past
61/// the cap is refused before it is decoded, as Calendar's Date_List does, so
62/// no list costs more than one destination past the cap to refuse.
63///
64/// The write path and routing share this decoder, so the object never stores
65/// a list routing would refuse.
66pub(super) fn decode_capped(bytes: &[u8]) -> Result<Vec<BACnetDestination>, CappedListError> {
67 let mut list = Vec::new();
68 let mut offset = 0;
69 while offset < bytes.len() {
70 if list.len() == MAX_RECIPIENT_LIST_DESTINATIONS {
71 return Err(CappedListError::PastTheCap(list.len()));
72 }
73 let (destination, next) =
74 decode_destination(bytes, offset).map_err(|_| CappedListError::Malformed)?;
75 list.push(destination);
76 offset = next;
77 }
78 Ok(list)
79}
80
81/// Decode a whole-list Recipient_List write. Only the framed wire form
82/// (`ApplicationData`) is a Recipient_List value (#1125); anything else, a
83/// malformed destination included, fails with PROPERTY / INVALID_DATA_TYPE.
84pub(crate) fn decode_write(value: PropertyValue) -> Result<Vec<BACnetDestination>, Error> {
85 let PropertyValue::ApplicationData(bytes) = value else {
86 return Err(common::invalid_data_type_error());
87 };
88 decode_capped(&bytes).map_err(|error| match error {
89 CappedListError::Malformed => common::invalid_data_type_error(),
90 CappedListError::PastTheCap(index) => past_the_cap(index),
91 })
92}
93
94/// Refuse a destination given through `add_destination` that a framed write
95/// could not carry: an address whose MAC is longer than
96/// [`BACnetAddress::MAX_MAC_LEN`] octets fails, as the write would, with
97/// PROPERTY / INVALID_DATA_TYPE (#1124). Stored, it would make the class's
98/// own list undecodable and routing would drop every destination with it.
99pub(crate) fn check_added(destination: &BACnetDestination) -> Result<(), Error> {
100 match &destination.recipient {
101 BACnetRecipient::Address(address)
102 if address.mac_address.len() > BACnetAddress::MAX_MAC_LEN =>
103 {
104 Err(common::invalid_data_type_error())
105 }
106 _ => Ok(()),
107 }
108}