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//! ```text
4//! Recipient_List ::= BACnetLIST OF BACnetDestination    -- concatenation, no wrapper
5//!
6//! BACnetDestination ::= SEQUENCE {
7//!     valid-days                    BACnetDaysOfWeek,          -- untagged
8//!     from-time                     Time,                      -- untagged
9//!     to-time                       Time,                      -- untagged
10//!     recipient                     BACnetRecipient,           -- CHOICE
11//!     process-identifier            Unsigned32,                -- untagged
12//!     issue-confirmed-notifications BOOLEAN,                   -- untagged
13//!     transitions                   BACnetEventTransitionBits  -- untagged
14//! }
15//!
16//! BACnetRecipient ::= CHOICE { device [0] BACnetObjectIdentifier,
17//!                              address [1] BACnetAddress }
18//!
19//! BACnetAddress ::= SEQUENCE { network-number Unsigned16,      -- untagged
20//!                              mac-address OCTET STRING }      -- untagged
21//! ```
22//!
23//! Tag forms: the destination members are UNTAGGED, so they travel as seven
24//! application-tagged elements in order (Bit String, Time, Time, recipient,
25//! Unsigned, Boolean, Bit String). `device [0]` tags a primitive
26//! ObjectIdentifier — a context-specific PRIMITIVE tag 0, length 4.
27//! `BACnetAddress` is constructed, so `address [1]` is an opening tag 1 /
28//! application-tagged Unsigned16 + OCTET STRING / closing tag 1.
29
30use bacnet_types::constructed::{BACnetAddress, BACnetDestination, BACnetRecipient};
31use bacnet_types::error::Error;
32use bacnet_types::primitives::{ObjectIdentifier, Time};
33use bacnet_types::MacAddr;
34use bytes::BytesMut;
35
36use crate::primitives;
37use crate::tags::{self, TagClass};
38
39use super::MAX_FRAMED_ITEMS;
40
41/// The two bit-string members travel MSB-first per Clause 20.2.10:
42/// `BACnetDaysOfWeek` is 7 bits (monday(0) at 0x80), so the fill octet has 1
43/// unused bit; `BACnetEventTransitionBits` is 3 bits, so 5 unused.
44use bacnet_types::bitstring::{pack_octet, unpack_octet};
45
46/// Encode one [`BACnetDestination`].
47pub fn encode_destination(buf: &mut BytesMut, dest: &BACnetDestination) {
48    primitives::encode_app_bit_string(buf, 1, &[pack_octet(dest.valid_days)]);
49    primitives::encode_app_time(buf, &dest.from_time);
50    primitives::encode_app_time(buf, &dest.to_time);
51    encode_recipient(buf, &dest.recipient);
52    primitives::encode_app_unsigned(buf, dest.process_identifier as u64);
53    primitives::encode_app_boolean(buf, dest.issue_confirmed_notifications);
54    primitives::encode_app_bit_string(buf, 5, &[pack_octet(dest.transitions)]);
55}
56
57/// Encode a `BACnetLIST of BACnetDestination`: plain concatenation, no
58/// wrapper (Clause 12.21 `Recipient_List`).
59pub fn encode_destination_list(buf: &mut BytesMut, destinations: &[BACnetDestination]) {
60    for dest in destinations {
61        encode_destination(buf, dest);
62    }
63}
64
65/// Encode a [`BACnetRecipient`].
66pub fn encode_recipient(buf: &mut BytesMut, recipient: &BACnetRecipient) {
67    match recipient {
68        BACnetRecipient::Device(oid) => {
69            // device [0] tags the primitive ObjectIdentifier.
70            tags::encode_tag(buf, 0, TagClass::Context, 4);
71            buf.extend_from_slice(&oid.encode());
72        }
73        BACnetRecipient::Address(address) => {
74            // address [1] tags the constructed BACnetAddress.
75            tags::encode_opening_tag(buf, 1);
76            primitives::encode_app_unsigned(buf, address.network_number as u64);
77            primitives::encode_app_octet_string(buf, &address.mac_address);
78            tags::encode_closing_tag(buf, 1);
79        }
80    }
81}
82
83/// Decode one [`BACnetRecipient`] at `offset`.
84pub fn decode_recipient(data: &[u8], offset: usize) -> Result<(BACnetRecipient, usize), Error> {
85    let what = "BACnetRecipient";
86    let (tag, pos) = tags::decode_tag(data, offset)?;
87    if tag.is_context(0) {
88        // device [0] — primitive, exactly 4 contents octets.
89        if tag.length != 4 {
90            return Err(Error::decoding(
91                offset,
92                format!(
93                    "{what}: device [0] expects 4 contents octets, got {}",
94                    tag.length
95                ),
96            ));
97        }
98        let end = pos + 4;
99        if end > data.len() {
100            return Err(Error::buffer_too_short(end, data.len()));
101        }
102        return Ok((
103            BACnetRecipient::Device(ObjectIdentifier::decode(&data[pos..end])?),
104            end,
105        ));
106    }
107    if tag.is_opening_tag(1) {
108        // network-number Unsigned16
109        let (network_number, pos) = decode_app_unsigned(data, pos, what)?;
110        let network_number = u16::try_from(network_number).map_err(|_| {
111            Error::decoding(pos, format!("{what}: network-number exceeds Unsigned16"))
112        })?;
113        // mac-address OCTET STRING (a zero-length string is a broadcast)
114        let (mac_address, pos) = decode_app_octet_string(data, pos, what)?;
115        let (close, close_pos) = tags::decode_tag(data, pos)?;
116        if !close.is_closing_tag(1) {
117            return Err(Error::decoding(
118                pos,
119                format!("{what}: missing closing tag [1] for BACnetAddress"),
120            ));
121        }
122        return Ok((
123            BACnetRecipient::Address(BACnetAddress {
124                network_number,
125                mac_address,
126            }),
127            close_pos,
128        ));
129    }
130    Err(Error::decoding(
131        offset,
132        format!(
133            "{what}: expected [0] (device) or [1] (address), got {}",
134            if tag.is_opening {
135                format!("opening tag [{}]", tag.number)
136            } else if tag.is_closing {
137                format!("closing tag [{}]", tag.number)
138            } else {
139                match tag.class {
140                    TagClass::Context => format!("primitive context tag [{}]", tag.number),
141                    TagClass::Application => format!("application tag {}", tag.number),
142                }
143            }
144        ),
145    ))
146}
147
148/// Decode one [`BACnetDestination`] at `offset`; returns it and the offset
149/// past the last member.
150pub fn decode_destination(data: &[u8], offset: usize) -> Result<(BACnetDestination, usize), Error> {
151    let what = "BACnetDestination";
152    // valid-days: BACnetDaysOfWeek (bit 0 = Monday, MSB-first on the wire).
153    let ((days_unused, days_data), pos) = decode_app_bit_string(data, offset, what)?;
154    check_fixed_bit_string(
155        days_unused,
156        &days_data,
157        1,
158        "valid-days (BACnetDaysOfWeek: 7 bits)",
159    )?;
160    let valid_days = unpack_octet(&days_data, 7);
161    // from-time / to-time.
162    let (from_time, pos) = decode_app_time(data, pos, what)?;
163    let (to_time, pos) = decode_app_time(data, pos, what)?;
164    // recipient CHOICE.
165    let (recipient, pos) = decode_recipient(data, pos)?;
166    // process-identifier Unsigned32.
167    let (process_identifier, pos) = decode_app_unsigned(data, pos, what)?;
168    let process_identifier = u32::try_from(process_identifier).map_err(|_| {
169        Error::decoding(
170            pos,
171            format!("{what}: process-identifier exceeds Unsigned32"),
172        )
173    })?;
174    // issue-confirmed-notifications BOOLEAN (application-tagged: the value
175    // rides in the tag's L/V/T bits with no contents octets).
176    let (tag, pos) = tags::decode_tag(data, pos)?;
177    if tag.class != TagClass::Application || tag.number != tags::app_tag::BOOLEAN {
178        return Err(Error::decoding(
179            pos,
180            format!("{what}: expected application-tagged BOOLEAN"),
181        ));
182    }
183    let issue_confirmed_notifications = tag.length != 0;
184    // transitions: BACnetEventTransitionBits (3 bits).
185    let ((transitions_unused, transitions_data), pos) = decode_app_bit_string(data, pos, what)?;
186    check_fixed_bit_string(
187        transitions_unused,
188        &transitions_data,
189        5,
190        "transitions (BACnetEventTransitionBits: 3 bits)",
191    )?;
192    let transitions = unpack_octet(&transitions_data, 3);
193
194    Ok((
195        BACnetDestination {
196            valid_days,
197            from_time,
198            to_time,
199            recipient,
200            process_identifier,
201            issue_confirmed_notifications,
202            transitions,
203        },
204        pos,
205    ))
206}
207
208/// Decode a whole `Recipient_List` (`BACnetLIST of BACnetDestination` —
209/// concatenated entries). Every entry must parse; trailing garbage errors.
210pub fn decode_destination_list(data: &[u8]) -> Result<Vec<BACnetDestination>, Error> {
211    let mut destinations = Vec::new();
212    let mut pos = 0;
213    while pos < data.len() {
214        if destinations.len() >= MAX_FRAMED_ITEMS {
215            return Err(Error::decoding(
216                pos,
217                "Recipient_List: destination count exceeds limit",
218            ));
219        }
220        let (dest, new_pos) = decode_destination(data, pos)?;
221        destinations.push(dest);
222        pos = new_pos;
223    }
224    Ok(destinations)
225}
226
227// ---------------------------------------------------------------------------
228// Small application-tagged member helpers
229// ---------------------------------------------------------------------------
230
231/// Decode an application-tagged Unsigned member, returning raw value + offset.
232fn decode_app_unsigned(data: &[u8], offset: usize, what: &str) -> Result<(u64, usize), Error> {
233    let (tag, pos) = tags::decode_tag(data, offset)?;
234    if tag.class != TagClass::Application || tag.number != tags::app_tag::UNSIGNED {
235        return Err(Error::decoding(
236            offset,
237            format!("{what}: expected application-tagged Unsigned"),
238        ));
239    }
240    let end = pos
241        .checked_add(tag.length as usize)
242        .ok_or_else(|| Error::decoding(pos, format!("{what}: length overflow")))?;
243    if end > data.len() {
244        return Err(Error::buffer_too_short(end, data.len()));
245    }
246    Ok((primitives::decode_unsigned(&data[pos..end])?, end))
247}
248
249/// Decode an application-tagged OCTET STRING member.
250fn decode_app_octet_string(
251    data: &[u8],
252    offset: usize,
253    what: &str,
254) -> Result<(MacAddr, usize), Error> {
255    let (tag, pos) = tags::decode_tag(data, offset)?;
256    if tag.class != TagClass::Application || tag.number != tags::app_tag::OCTET_STRING {
257        return Err(Error::decoding(
258            offset,
259            format!("{what}: expected application-tagged OCTET STRING"),
260        ));
261    }
262    let end = pos
263        .checked_add(tag.length as usize)
264        .ok_or_else(|| Error::decoding(pos, format!("{what}: length overflow")))?;
265    if end > data.len() {
266        return Err(Error::buffer_too_short(end, data.len()));
267    }
268    Ok((MacAddr::from_slice(&data[pos..end]), end))
269}
270
271/// Decode an application-tagged Time member (exactly 4 contents octets).
272fn decode_app_time(data: &[u8], offset: usize, what: &str) -> Result<(Time, usize), Error> {
273    let (tag, pos) = tags::decode_tag(data, offset)?;
274    if tag.class != TagClass::Application || tag.number != tags::app_tag::TIME || tag.length != 4 {
275        return Err(Error::decoding(
276            offset,
277            format!("{what}: expected application-tagged Time (4 octets)"),
278        ));
279    }
280    let end = pos + 4;
281    if end > data.len() {
282        return Err(Error::buffer_too_short(end, data.len()));
283    }
284    Ok((Time::decode(&data[pos..end])?, end))
285}
286
287/// Fixed-width bit-string member validation. Per Clause 20.2.10 (initial
288/// octet = unused-bit count; defined bits MSB-first) and the Clause 21
289/// productions, the named-bit sets here fit in exactly one subsequent
290/// octet: `BACnetDaysOfWeek` has 7 defined bits (unused_bits 1) and
291/// `BACnetEventTransitionBits` has 3 (unused_bits 5). A zero-length,
292/// overlong, or mismatched-unused-bits encoding is not a destination a
293/// notifier may act on — reject it rather than unpacking an all-zero
294/// (dormant) one.
295fn check_fixed_bit_string(
296    unused_bits: u8,
297    data: &[u8],
298    expected_unused: u8,
299    name: &str,
300) -> Result<(), Error> {
301    if data.len() != 1 || unused_bits != expected_unused {
302        return Err(Error::decoding(
303            0,
304            format!(
305                "{name}: expected 1 content octet with unused_bits {expected_unused} \
306                 (got {} content octet(s), unused_bits {unused_bits})",
307                data.len()
308            ),
309        ));
310    }
311    Ok(())
312}
313
314/// Decode an application-tagged BIT STRING member, returning
315/// `(unused_bits, data)` and the offset past it.
316fn decode_app_bit_string(
317    data: &[u8],
318    offset: usize,
319    what: &str,
320) -> Result<((u8, Vec<u8>), usize), Error> {
321    let (tag, pos) = tags::decode_tag(data, offset)?;
322    if tag.class != TagClass::Application || tag.number != tags::app_tag::BIT_STRING {
323        return Err(Error::decoding(
324            offset,
325            format!("{what}: expected application-tagged BIT STRING"),
326        ));
327    }
328    let end = pos
329        .checked_add(tag.length as usize)
330        .ok_or_else(|| Error::decoding(pos, format!("{what}: length overflow")))?;
331    if end > data.len() {
332        return Err(Error::buffer_too_short(end, data.len()));
333    }
334    let (unused_bits, bits) = primitives::decode_bit_string(&data[pos..end])?;
335    Ok(((unused_bits, bits), end))
336}