Skip to main content

bacnet_encoding/constructed/
recipient.rs

1//! `Recipient_List` full ASN.1 framing (ASHRAE 135-2020 Clauses 12.21, 21).
2//!
3//! The property is a BACnetLIST of `BACnetDestination`, so on the wire it is
4//! the destinations back to back with no list wrapper. A destination has
5//! seven members and none carries a context tag, so they travel in this order
6//! as application-tagged elements (the recipient CHOICE brings its own tags):
7//!
8//! | Member | Wire form |
9//! |---|---|
10//! | `valid-days` | Bit String (`BACnetDaysOfWeek`) |
11//! | `from-time` | Time |
12//! | `to-time` | Time |
13//! | `recipient` | `BACnetRecipient` CHOICE, see below |
14//! | `process-identifier` | Unsigned, 32-bit range |
15//! | `issue-confirmed-notifications` | Boolean |
16//! | `transitions` | Bit String (`BACnetEventTransitionBits`) |
17//!
18//! `BACnetRecipient` picks either `device [0]`, an object identifier, or
19//! `address [1]`, a `BACnetAddress`. The device form tags a primitive
20//! ObjectIdentifier — a context-specific PRIMITIVE tag 0, length 4.
21//! `BACnetAddress` is constructed (a 16-bit network number, then the MAC
22//! address as an OCTET STRING, both application-tagged), so `address [1]` is
23//! an opening tag 1 / application-tagged Unsigned16 + OCTET STRING / closing
24//! tag 1.
25//!
26//! A `BACnetAddress` MAC is at most [`BACnetAddress::MAX_MAC_LEN`] octets in
27//! both directions: [`decode_recipient`] refuses a longer one before copying
28//! it (#1124 for configured recipients, #1156 everywhere else) and
29//! [`encode_recipient`] refuses to write one. Every other codec that carries a
30//! `BACnetAddress` (ValueSource, the AuditLogQuery filters) shares
31//! [`check_decoded_mac_len`] and [`check_encoded_mac_len`], so the bound has
32//! one definition. The source addresses the stack learns off the network fit
33//! it too: the NPDU codec bounds SADR the same way (#1141), and the network
34//! layer drops a frame from a longer link-layer source MAC (#1198).
35
36#[cfg(not(feature = "std"))]
37use alloc::{string::String, vec::Vec};
38
39use bacnet_types::bitstring::{DaysOfWeek, EventTransitionBits};
40use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
41use bacnet_types::error::Error;
42use bacnet_types::primitives::Time;
43use bacnet_types::MacAddr;
44use bytes::BytesMut;
45
46use crate::primitives;
47use crate::tags::{self, TagClass};
48
49use super::tagged::{
50    contents, decode_app_bit_string, decode_app_unsigned, decode_ctx_object_id, expect_closing,
51    misplaced_tag,
52};
53use super::MAX_FRAMED_ITEMS;
54
55/// The message for a `BACnetAddress` MAC of `length` octets, or `None` when it
56/// fits [`BACnetAddress::MAX_MAC_LEN`]. The one place the bound is compared.
57fn mac_len_excess(what: &str, length: usize) -> Option<String> {
58    (length > BACnetAddress::MAX_MAC_LEN).then(|| {
59        format!(
60            "{what}: mac-address of {length} octets exceeds the {}-octet limit",
61            BACnetAddress::MAX_MAC_LEN
62        )
63    })
64}
65
66/// Refuse, as a decode error at `offset`, a `BACnetAddress` mac-address whose
67/// OCTET STRING holds more than [`BACnetAddress::MAX_MAC_LEN`] octets (#1156),
68/// or any other OCTET STRING that names a MAC on a data link, such as the
69/// You-Are device MAC (#1200). Decoders call it on the tag's length, before
70/// copying any octet.
71pub fn check_decoded_mac_len(length: usize, offset: usize, what: &str) -> Result<(), Error> {
72    mac_len_excess(what, length).map_or(Ok(()), |message| Err(Error::overflow(offset, message)))
73}
74
75/// Refuse to encode a MAC longer than [`BACnetAddress::MAX_MAC_LEN`] octets,
76/// which [`check_decoded_mac_len`] would refuse on the way back in: a
77/// `BACnetAddress` mac-address (#1156) or the You-Are device MAC (#1200).
78pub fn check_encoded_mac_len(mac: &[u8], what: &str) -> Result<(), Error> {
79    mac_len_excess(what, mac.len()).map_or(Ok(()), |message| Err(Error::Encoding(message)))
80}
81
82/// [`check_encoded_mac_len`] for the address form of a recipient; the device
83/// form always encodes.
84pub(super) fn check_encoded_recipient(recipient: &BACnetRecipient) -> Result<(), Error> {
85    match recipient {
86        BACnetRecipient::Device(_) => Ok(()),
87        BACnetRecipient::Address(address) => {
88            check_encoded_mac_len(&address.mac_address, "BACnetRecipient")
89        }
90    }
91}
92
93/// Encode one [`BACnetDestination`].
94///
95/// The two bit-string members travel MSB-first per Clause 20.2.10:
96/// `BACnetDaysOfWeek` is 7 bits (monday(0) at 0x80), so the fill octet has 1
97/// unused bit; `BACnetEventTransitionBits` is 3 bits, so 5 unused.
98///
99/// A recipient MAC past [`BACnetAddress::MAX_MAC_LEN`] octets is an error,
100/// returned before `buf` changes.
101pub fn encode_destination(buf: &mut BytesMut, dest: &BACnetDestination) -> Result<(), Error> {
102    check_encoded_recipient(&dest.recipient)?;
103    write_destination(buf, dest);
104    Ok(())
105}
106
107/// Write a destination whose recipient has passed [`check_encoded_recipient`].
108fn write_destination(buf: &mut BytesMut, dest: &BACnetDestination) {
109    primitives::encode_app_bit_string(buf, 1, &[dest.valid_days.to_bacnet()]);
110    primitives::encode_app_time(buf, &dest.from_time);
111    primitives::encode_app_time(buf, &dest.to_time);
112    write_recipient(buf, &dest.recipient);
113    primitives::encode_app_unsigned(buf, dest.process_identifier as u64);
114    primitives::encode_app_boolean(buf, dest.issue_confirmed_notifications);
115    primitives::encode_app_bit_string(buf, 5, &[dest.transitions.to_bacnet()]);
116}
117
118/// Encode a `BACnetLIST of BACnetDestination`: plain concatenation, no
119/// wrapper (Clause 12.21 `Recipient_List`). Every destination is checked
120/// before `buf` changes, so a refused list writes nothing.
121pub fn encode_destination_list(
122    buf: &mut BytesMut,
123    destinations: &[BACnetDestination],
124) -> Result<(), Error> {
125    for dest in destinations {
126        check_encoded_recipient(&dest.recipient)?;
127    }
128    for dest in destinations {
129        write_destination(buf, dest);
130    }
131    Ok(())
132}
133
134/// Encode a [`BACnetRecipient`]. An address MAC past
135/// [`BACnetAddress::MAX_MAC_LEN`] octets is an error, returned before `buf`
136/// changes.
137pub fn encode_recipient(buf: &mut BytesMut, recipient: &BACnetRecipient) -> Result<(), Error> {
138    check_encoded_recipient(recipient)?;
139    write_recipient(buf, recipient);
140    Ok(())
141}
142
143/// Write a recipient that has passed [`check_encoded_recipient`].
144pub(super) fn write_recipient(buf: &mut BytesMut, recipient: &BACnetRecipient) {
145    match recipient {
146        BACnetRecipient::Device(oid) => {
147            // device [0] tags the primitive ObjectIdentifier.
148            tags::encode_tag(buf, 0, TagClass::Context, 4);
149            buf.extend_from_slice(&oid.encode());
150        }
151        BACnetRecipient::Address(address) => {
152            // address [1] tags the constructed BACnetAddress.
153            tags::encode_opening_tag(buf, 1);
154            primitives::encode_app_unsigned(buf, address.network_number as u64);
155            primitives::encode_app_octet_string(buf, &address.mac_address);
156            tags::encode_closing_tag(buf, 1);
157        }
158    }
159}
160
161/// Decode one [`BACnetRecipient`] at `offset`.
162///
163/// An address whose MAC is longer than [`BACnetAddress::MAX_MAC_LEN`] octets
164/// names no node this stack reaches, so it is a decode error, raised before
165/// the MAC is copied. That holds for a configured recipient (a Recipient_List
166/// destination or the Audit_Notification_Recipient, #1124) and for one a
167/// service carries or the stack reports (COV subscription lists, audit
168/// records, enrollment filters, #1156).
169pub fn decode_recipient(data: &[u8], offset: usize) -> Result<(BACnetRecipient, usize), Error> {
170    let what = "BACnetRecipient";
171    let (tag, pos) = tags::decode_tag(data, offset)?;
172    if tag.is_context(0) {
173        // device [0] tags the primitive ObjectIdentifier.
174        let (device, end) = decode_ctx_object_id(data, offset, 0, "BACnetRecipient device")?;
175        return Ok((BACnetRecipient::Device(device), end));
176    }
177    if tag.is_opening_tag(1) {
178        // network-number Unsigned16
179        let (network_number, pos) = decode_app_unsigned::<u64>(data, pos, what)?;
180        let network_number = u16::try_from(network_number).map_err(|_| {
181            Error::out_of_range(pos, format!("{what}: network-number exceeds Unsigned16"))
182        })?;
183        // mac-address OCTET STRING (a zero-length string is a broadcast)
184        let (mac_address, pos) = decode_app_mac_address(data, pos, what)?;
185        let end = expect_closing(data, pos, 1, "BACnetRecipient address")?;
186        return Ok((
187            BACnetRecipient::Address(BACnetAddress {
188                network_number,
189                mac_address,
190            }),
191            end,
192        ));
193    }
194    Err(misplaced_tag(
195        data,
196        &tag,
197        None,
198        offset,
199        format!(
200            "{what}: expected [0] (device) or [1] (address), got {}",
201            if tag.is_opening {
202                format!("opening tag [{}]", tag.number)
203            } else if tag.is_closing {
204                format!("closing tag [{}]", tag.number)
205            } else {
206                match tag.class {
207                    TagClass::Context => format!("primitive context tag [{}]", tag.number),
208                    TagClass::Application => format!("application tag {}", tag.number),
209                }
210            }
211        ),
212    ))
213}
214
215/// Decode one [`BACnetDestination`] at `offset`; returns it and the offset
216/// past the last member. Its recipient decodes through [`decode_recipient`],
217/// so an address MAC past [`BACnetAddress::MAX_MAC_LEN`] octets is refused
218/// (#1124).
219pub fn decode_destination(data: &[u8], offset: usize) -> Result<(BACnetDestination, usize), Error> {
220    let what = "BACnetDestination";
221    // valid-days: BACnetDaysOfWeek (bit 0 = Monday, MSB-first on the wire).
222    let ((days_unused, days_data), pos) = decode_app_bit_string(data, offset, what)?;
223    check_fixed_bit_string(
224        days_unused,
225        &days_data,
226        1,
227        "valid-days (BACnetDaysOfWeek: 7 bits)",
228    )?;
229    let valid_days = DaysOfWeek::from_bacnet(&days_data);
230    // from-time / to-time.
231    let (from_time, pos) = decode_app_time(data, pos, what)?;
232    let (to_time, pos) = decode_app_time(data, pos, what)?;
233    // recipient CHOICE, its address MAC bounded.
234    let (recipient, pos) = decode_recipient(data, pos)?;
235    // process-identifier Unsigned32.
236    let (process_identifier, pos) = decode_app_unsigned::<u64>(data, pos, what)?;
237    let process_identifier = u32::try_from(process_identifier).map_err(|_| {
238        Error::out_of_range(
239            pos,
240            format!("{what}: process-identifier exceeds Unsigned32"),
241        )
242    })?;
243    // issue-confirmed-notifications BOOLEAN (application-tagged: the value
244    // rides in the tag's L/V/T bits with no contents octets).
245    let (tag, pos) = tags::decode_tag(data, pos)?;
246    if tag.class != TagClass::Application || tag.number != tags::app_tag::BOOLEAN {
247        return Err(misplaced_tag(
248            data,
249            &tag,
250            None,
251            pos,
252            format!("{what}: expected application-tagged BOOLEAN"),
253        ));
254    }
255    let issue_confirmed_notifications = tag.length != 0;
256    // transitions: BACnetEventTransitionBits (3 bits).
257    let ((transitions_unused, transitions_data), pos) = decode_app_bit_string(data, pos, what)?;
258    check_fixed_bit_string(
259        transitions_unused,
260        &transitions_data,
261        5,
262        "transitions (BACnetEventTransitionBits: 3 bits)",
263    )?;
264    let transitions = EventTransitionBits::from_bacnet(&transitions_data);
265
266    Ok((
267        BACnetDestination {
268            valid_days,
269            from_time,
270            to_time,
271            recipient,
272            process_identifier,
273            issue_confirmed_notifications,
274            transitions,
275        },
276        pos,
277    ))
278}
279
280/// Decode a whole `Recipient_List` (`BACnetLIST of BACnetDestination` —
281/// concatenated entries). Every entry must parse; trailing garbage errors.
282pub fn decode_destination_list(data: &[u8]) -> Result<Vec<BACnetDestination>, Error> {
283    let mut destinations = Vec::new();
284    let mut pos = 0;
285    while pos < data.len() {
286        if destinations.len() >= MAX_FRAMED_ITEMS {
287            return Err(Error::overflow(
288                pos,
289                "Recipient_List: destination count exceeds limit",
290            ));
291        }
292        let (dest, new_pos) = decode_destination(data, pos)?;
293        destinations.push(dest);
294        pos = new_pos;
295    }
296    Ok(destinations)
297}
298
299// ---------------------------------------------------------------------------
300// Small application-tagged member helpers
301// ---------------------------------------------------------------------------
302
303/// Decode the application-tagged OCTET STRING of a `BACnetAddress` MAC,
304/// refusing one longer than [`BACnetAddress::MAX_MAC_LEN`] octets before
305/// copying it.
306pub(super) fn decode_app_mac_address(
307    data: &[u8],
308    offset: usize,
309    what: &str,
310) -> Result<(MacAddr, usize), Error> {
311    let (tag, pos) = tags::decode_tag(data, offset)?;
312    if tag.class != TagClass::Application || tag.number != tags::app_tag::OCTET_STRING {
313        return Err(misplaced_tag(
314            data,
315            &tag,
316            None,
317            offset,
318            format!("{what}: expected application-tagged OCTET STRING"),
319        ));
320    }
321    check_decoded_mac_len(tag.length as usize, offset, what)?;
322    let (mac, end) = contents(data, pos, tag.length)?;
323    Ok((MacAddr::from_slice(mac), end))
324}
325
326/// Decode an application-tagged Time member (exactly 4 contents octets).
327fn decode_app_time(data: &[u8], offset: usize, what: &str) -> Result<(Time, usize), Error> {
328    let (tag, pos) = tags::decode_tag(data, offset)?;
329    if tag.class != TagClass::Application || tag.number != tags::app_tag::TIME {
330        return Err(misplaced_tag(
331            data,
332            &tag,
333            None,
334            offset,
335            format!("{what}: expected application-tagged Time"),
336        ));
337    }
338    if tag.length != 4 {
339        return Err(Error::decoding(
340            offset,
341            format!(
342                "{what}: Time has {} contents octets, expected 4",
343                tag.length
344            ),
345        ));
346    }
347    let (octets, end) = contents(data, pos, 4)?;
348    Ok((Time::decode(octets)?, end))
349}
350
351/// Fixed-width bit-string member validation. Per Clause 20.2.10 (initial
352/// octet = unused-bit count; defined bits MSB-first) and the Clause 21
353/// productions, the named-bit sets here fit in exactly one subsequent
354/// octet: `BACnetDaysOfWeek` has 7 defined bits (unused_bits 1) and
355/// `BACnetEventTransitionBits` has 3 (unused_bits 5). A zero-length,
356/// overlong, or mismatched-unused-bits encoding is not a destination a
357/// notifier may act on — reject it rather than unpacking an all-zero
358/// (dormant) one.
359fn check_fixed_bit_string(
360    unused_bits: u8,
361    data: &[u8],
362    expected_unused: u8,
363    name: &str,
364) -> Result<(), Error> {
365    if data.len() != 1 || unused_bits != expected_unused {
366        return Err(Error::decoding(
367            0,
368            format!(
369                "{name}: expected 1 content octet with unused_bits {expected_unused} \
370                 (got {} content octet(s), unused_bits {unused_bits})",
371                data.len()
372            ),
373        ));
374    }
375    Ok(())
376}