Skip to main content

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