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