Skip to main content

bacnet_encoding/constructed/
event_parameter.rs

1//! `BACnetEventParameter` full ASN.1 framing.
2//!
3//! Production (ASHRAE 135-2020 Clause 21), alternatives modeled by
4//! [`BACnetEventParameter`]:
5//!
6//! ```text
7//! BACnetEventParameter ::= CHOICE {
8//!     change-of-bitstring [0]  SEQUENCE { time-delay [0] Unsigned,
9//!                                         bitmask [1] BIT STRING,
10//!                                         list-of-bitstring-values [2] SEQUENCE OF BIT STRING },
11//!     change-of-state     [1]  SEQUENCE { time-delay [0] Unsigned,
12//!                                         list-of-values [1] SEQUENCE OF BACnetPropertyStates },
13//!     change-of-value     [2]  SEQUENCE { time-delay [0] Unsigned,
14//!                                         cov-criteria [1] CHOICE { bitmask [0] BIT STRING,
15//!                                                     referenced-property-increment [1] REAL } },
16//!     floating-limit      [4]  SEQUENCE { time-delay [0] Unsigned,
17//!                                         setpoint-reference [1] BACnetDeviceObjectPropertyReference,
18//!                                         low-diff-limit [2] REAL, high-diff-limit [3] REAL,
19//!                                         deadband [4] REAL },
20//!     out-of-range        [5]  SEQUENCE { time-delay [0] Unsigned,
21//!                                         low-limit [1] REAL, high-limit [2] REAL,
22//!                                         deadband [3] REAL },
23//!     /* [6] omitted: proprietary parallel of the notification CHOICE [6] */
24//!     /* [7] deprecated */    /* [12] reserved */  /* [19] omitted: change-of-reliability */
25//!     extended            [9]  SEQUENCE { vendor-id [0] Unsigned16,
26//!                                         extended-event-type [1] Unsigned,
27//!                                         parameters [2] SEQUENCE OF CHOICE { ... } },
28//!     none                [20] NULL,
29//!     ... }
30//! ```
31//!
32//! Alternatives with no Rust model (`command-failure [3]`,
33//! `change-of-life-safety [8]`, `buffer-ready [10]`, `unsigned-range [11]`,
34//! `access-event [13]`, `*-out-of-range [14..=16]`, `change-of-characterstring
35//! [17]`, `change-of-status-flags [18]`, `none [20] NULL`,
36//! `change-of-discrete-value [21]`, `change-of-timer [22]`, vendor tags) are
37//! preserved byte-for-byte through [`BACnetEventParameter::Opaque`]. The
38//! omitted/deprecated/reserved slots 6, 7, 12 and 19 are rejected on decode.
39
40use bacnet_types::constructed::{BACnetEventParameter, ChangeOfValueCriteria};
41use bacnet_types::error::Error;
42use bytes::BytesMut;
43
44use crate::primitives;
45use crate::tags;
46
47use super::{
48    decode_app_bit_string, decode_ctx_bit_string, decode_ctx_real, decode_ctx_unsigned,
49    decode_dopr_body, decode_property_state, encode_dopr_body, encode_property_state,
50    expect_closing, expect_opening, validate_extended_parameters, validate_tlv_sequence,
51    MAX_FRAMED_ITEMS,
52};
53
54/// Context tags the Clause 21 production omits, deprecates, or reserves.
55/// Decoding one means the peer is speaking something other than the 135-2020
56/// production, so these are hard errors rather than Opaque shadows.
57const RESERVED_EVENT_TAGS: [u8; 4] = [6, 7, 12, 19];
58const MODELED_EVENT_TAGS: [u8; 6] = [0, 1, 2, 4, 5, 9];
59
60/// Encode a [`BACnetEventParameter`] as its full CHOICE framing.
61///
62/// Validation includes the complete framing depth. Errors leave `buf`
63/// unchanged.
64pub fn encode_event_parameter(
65    buf: &mut BytesMut,
66    value: &BACnetEventParameter,
67) -> Result<(), Error> {
68    let mut encoded = BytesMut::new();
69    encode_event_parameter_into(&mut encoded, value)?;
70    validate_tlv_sequence(&encoded, "BACnetEventParameter")
71        .map_err(|error| Error::Encoding(error.to_string()))?;
72    buf.extend_from_slice(&encoded);
73    Ok(())
74}
75
76fn encode_event_parameter_into(
77    buf: &mut BytesMut,
78    value: &BACnetEventParameter,
79) -> Result<(), Error> {
80    match value {
81        BACnetEventParameter::ChangeOfBitstring {
82            time_delay,
83            bitmask,
84            list_of_values,
85        } => {
86            tags::encode_opening_tag(buf, 0);
87            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
88            primitives::encode_ctx_bit_string(buf, 1, bitmask.0, &bitmask.1);
89            tags::encode_opening_tag(buf, 2);
90            for (unused_bits, data) in list_of_values {
91                primitives::encode_app_bit_string(buf, *unused_bits, data);
92            }
93            tags::encode_closing_tag(buf, 2);
94            tags::encode_closing_tag(buf, 0);
95        }
96        BACnetEventParameter::ChangeOfState {
97            time_delay,
98            list_of_values,
99        } => {
100            let mut encoded_states = BytesMut::new();
101            for state in list_of_values {
102                encode_property_state(&mut encoded_states, state)?;
103            }
104            tags::encode_opening_tag(buf, 1);
105            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
106            tags::encode_opening_tag(buf, 1);
107            buf.extend_from_slice(&encoded_states);
108            tags::encode_closing_tag(buf, 1);
109            tags::encode_closing_tag(buf, 1);
110        }
111        BACnetEventParameter::ChangeOfValue {
112            time_delay,
113            criteria,
114        } => {
115            tags::encode_opening_tag(buf, 2);
116            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
117            // cov-criteria [1] CHOICE — a tagged CHOICE is always explicitly
118            // tagged: opening/closing [1] around the alternative's own tag.
119            tags::encode_opening_tag(buf, 1);
120            match criteria {
121                ChangeOfValueCriteria::Bitmask { unused_bits, data } => {
122                    primitives::encode_ctx_bit_string(buf, 0, *unused_bits, data);
123                }
124                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment) => {
125                    primitives::encode_ctx_real(buf, 1, *increment);
126                }
127            }
128            tags::encode_closing_tag(buf, 1);
129            tags::encode_closing_tag(buf, 2);
130        }
131        BACnetEventParameter::FloatingLimit {
132            time_delay,
133            setpoint_reference,
134            low_diff_limit,
135            high_diff_limit,
136            deadband,
137        } => {
138            tags::encode_opening_tag(buf, 4);
139            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
140            // setpoint-reference [1] BACnetDeviceObjectPropertyReference (SEQUENCE)
141            tags::encode_opening_tag(buf, 1);
142            encode_dopr_body(buf, setpoint_reference);
143            tags::encode_closing_tag(buf, 1);
144            primitives::encode_ctx_real(buf, 2, *low_diff_limit);
145            primitives::encode_ctx_real(buf, 3, *high_diff_limit);
146            primitives::encode_ctx_real(buf, 4, *deadband);
147            tags::encode_closing_tag(buf, 4);
148        }
149        BACnetEventParameter::OutOfRange {
150            time_delay,
151            low_limit,
152            high_limit,
153            deadband,
154        } => {
155            tags::encode_opening_tag(buf, 5);
156            primitives::encode_ctx_unsigned(buf, 0, *time_delay as u64);
157            primitives::encode_ctx_real(buf, 1, *low_limit);
158            primitives::encode_ctx_real(buf, 2, *high_limit);
159            primitives::encode_ctx_real(buf, 3, *deadband);
160            tags::encode_closing_tag(buf, 5);
161        }
162        BACnetEventParameter::Extended {
163            vendor_id,
164            extended_event_type,
165            parameters,
166        } => {
167            validate_extended_parameters(parameters, "extended")
168                .map_err(|error| Error::Encoding(error.to_string()))?;
169            tags::encode_opening_tag(buf, 9);
170            primitives::encode_ctx_unsigned(buf, 0, *vendor_id as u64);
171            primitives::encode_ctx_unsigned(buf, 1, *extended_event_type as u64);
172            // parameters [2] SEQUENCE OF CHOICE — held as raw pre-encoded items.
173            tags::encode_opening_tag(buf, 2);
174            buf.extend_from_slice(parameters);
175            tags::encode_closing_tag(buf, 2);
176            tags::encode_closing_tag(buf, 9);
177        }
178        BACnetEventParameter::Opaque { tag, data } if *tag == u8::MAX => {
179            // Legacy pre-framing EventEnrollment values used an Octet String.
180            primitives::encode_app_octet_string(buf, data);
181        }
182        BACnetEventParameter::Opaque { tag: 20, data } if data.is_empty() => {
183            // Clause 21 defines none [20] as context NULL, not an empty SEQUENCE.
184            tags::encode_tag(buf, 20, tags::TagClass::Context, 0);
185        }
186        BACnetEventParameter::Opaque { tag, .. }
187            if *tag == 20
188                || MODELED_EVENT_TAGS.contains(tag)
189                || RESERVED_EVENT_TAGS.contains(tag) =>
190        {
191            return Err(Error::Encoding(format!(
192                "BACnetEventParameter: tag [{tag}] cannot use an opaque constructed body"
193            )));
194        }
195        BACnetEventParameter::Opaque { tag, data } => {
196            validate_tlv_sequence(data, "opaque event parameters")
197                .map_err(|error| Error::Encoding(error.to_string()))?;
198            // Preserved alternative: its captured bytes are the complete
199            // SEQUENCE body, re-emitted under its own opening/closing pair.
200            tags::encode_opening_tag(buf, *tag);
201            buf.extend_from_slice(data);
202            tags::encode_closing_tag(buf, *tag);
203        }
204    }
205    Ok(())
206}
207
208/// Decode one framed [`BACnetEventParameter`] CHOICE alternative at `offset`.
209///
210/// Returns the value and the offset past its encoding. Unmodeled (but valid)
211/// alternatives are preserved as [`BACnetEventParameter::Opaque`]; the
212/// omitted/deprecated/reserved tags 6, 7, 12, 19 are rejected.
213pub fn decode_event_parameter(
214    data: &[u8],
215    offset: usize,
216) -> Result<(BACnetEventParameter, usize), Error> {
217    use BACnetEventParameter as EP;
218
219    // Pre-framing EventEnrollment writes use [255] as an internal wrapper.
220    // Keep this shipped compatibility form local so the generic tag parser
221    // can reject ASHRAE-reserved tag number 255 everywhere else.
222    if data.get(offset..offset.saturating_add(2)) == Some(&[0xFE, 0xFF]) {
223        let body_start = offset + 2;
224        let body_end = data.len().checked_sub(2).ok_or_else(|| {
225            Error::decoding(offset, "BACnetEventParameter: truncated legacy wrapper")
226        })?;
227        if body_end < body_start || data.get(body_end..) != Some(&[0xFF, 0xFF]) {
228            return Err(Error::decoding(
229                offset,
230                "BACnetEventParameter: missing legacy closing tag",
231            ));
232        }
233        return Ok((
234            EP::Opaque {
235                tag: u8::MAX,
236                data: data[body_start..body_end].to_vec(),
237            },
238            data.len(),
239        ));
240    }
241
242    let (tag, pos) = tags::decode_tag(data, offset)?;
243    let what = "BACnetEventParameter";
244
245    if tag.class == tags::TagClass::Application && tag.number == tags::app_tag::OCTET_STRING {
246        let end = pos
247            .checked_add(tag.length as usize)
248            .ok_or_else(|| Error::decoding(pos, "BACnetEventParameter: length overflow"))?;
249        if end > data.len() {
250            return Err(Error::buffer_too_short(end, data.len()));
251        }
252        return Ok((
253            EP::Opaque {
254                tag: u8::MAX,
255                data: data[pos..end].to_vec(),
256            },
257            end,
258        ));
259    }
260
261    if tag.class != tags::TagClass::Context {
262        return Err(Error::decoding(
263            offset,
264            "BACnetEventParameter: expected a context tag",
265        ));
266    }
267
268    if RESERVED_EVENT_TAGS.contains(&tag.number) {
269        return Err(Error::decoding(
270            offset,
271            format!(
272                "BACnetEventParameter: context tag [{}] is omitted/deprecated/reserved in Clause 21",
273                tag.number
274            ),
275        ));
276    }
277
278    // `none [20] NULL` is the only primitive alternative in this CHOICE.
279    if !tag.is_opening && !tag.is_closing {
280        if tag.number == 20 && tag.length == 0 {
281            return Ok((
282                EP::Opaque {
283                    tag: 20,
284                    data: Vec::new(),
285                },
286                pos,
287            ));
288        }
289        return Err(Error::decoding(
290            offset,
291            "BACnetEventParameter: invalid primitive alternative",
292        ));
293    }
294
295    if !tag.is_opening {
296        return Err(Error::decoding(
297            offset,
298            "BACnetEventParameter: unexpected closing tag",
299        ));
300    }
301    if tag.number == 20 {
302        return Err(Error::decoding(
303            offset,
304            "BACnetEventParameter: none [20] must use primitive NULL form",
305        ));
306    }
307
308    let value = match tag.number {
309        0 => {
310            let mut pos = pos;
311            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
312            let time_delay = u32::try_from(raw)
313                .map_err(|_| Error::decoding(pos, "change-of-bitstring: time-delay exceeds u32"))?;
314            pos = p;
315            let (bitmask, p) = decode_ctx_bit_string(data, pos, 1, what)?;
316            pos = p;
317            pos = expect_opening(data, pos, 2, what)?;
318            let mut list_of_values = Vec::new();
319            loop {
320                let (peek, _) = tags::decode_tag(data, pos)?;
321                if peek.is_closing_tag(2) {
322                    break;
323                }
324                if list_of_values.len() >= MAX_FRAMED_ITEMS {
325                    return Err(Error::decoding(
326                        pos,
327                        "change-of-bitstring: list-of-bitstring-values exceeds limit",
328                    ));
329                }
330                let (bits, p) = decode_app_bit_string(data, pos, what)?;
331                list_of_values.push(bits);
332                pos = p;
333            }
334            pos = expect_closing(data, pos, 2, what)?;
335            pos = expect_closing(data, pos, 0, what)?;
336            (
337                EP::ChangeOfBitstring {
338                    time_delay,
339                    bitmask,
340                    list_of_values,
341                },
342                pos,
343            )
344        }
345        1 => {
346            let mut pos = pos;
347            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
348            let time_delay = u32::try_from(raw)
349                .map_err(|_| Error::decoding(pos, "change-of-state: time-delay exceeds u32"))?;
350            pos = p;
351            pos = expect_opening(data, pos, 1, what)?;
352            let mut list_of_values = Vec::new();
353            loop {
354                let (peek, _) = tags::decode_tag(data, pos)?;
355                if peek.is_closing_tag(1) {
356                    break;
357                }
358                if list_of_values.len() >= MAX_FRAMED_ITEMS {
359                    return Err(Error::decoding(
360                        pos,
361                        "change-of-state: list-of-values exceeds limit",
362                    ));
363                }
364                let (state, p) = decode_property_state(data, pos)?;
365                list_of_values.push(state);
366                pos = p;
367            }
368            pos = expect_closing(data, pos, 1, what)?;
369            pos = expect_closing(data, pos, 1, what)?;
370            (
371                EP::ChangeOfState {
372                    time_delay,
373                    list_of_values,
374                },
375                pos,
376            )
377        }
378        2 => {
379            let mut pos = pos;
380            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
381            let time_delay = u32::try_from(raw)
382                .map_err(|_| Error::decoding(pos, "change-of-value: time-delay exceeds u32"))?;
383            pos = p;
384            pos = expect_opening(data, pos, 1, what)?;
385            let (inner, _) = tags::decode_tag(data, pos)?;
386            let criteria = if inner.is_context(0) {
387                let (bits, p) = decode_ctx_bit_string(data, pos, 0, what)?;
388                pos = p;
389                ChangeOfValueCriteria::Bitmask {
390                    unused_bits: bits.0,
391                    data: bits.1,
392                }
393            } else if inner.is_context(1) {
394                let (increment, p) = decode_ctx_real(data, pos, 1, what)?;
395                pos = p;
396                ChangeOfValueCriteria::ReferencedPropertyIncrement(increment)
397            } else {
398                return Err(Error::decoding(
399                    pos,
400                    format!(
401                        "change-of-value: unexpected cov-criteria tag [{}]",
402                        inner.number
403                    ),
404                ));
405            };
406            pos = expect_closing(data, pos, 1, what)?;
407            pos = expect_closing(data, pos, 2, what)?;
408            (
409                EP::ChangeOfValue {
410                    time_delay,
411                    criteria,
412                },
413                pos,
414            )
415        }
416        4 => {
417            let mut pos = pos;
418            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
419            let time_delay = u32::try_from(raw)
420                .map_err(|_| Error::decoding(pos, "floating-limit: time-delay exceeds u32"))?;
421            pos = p;
422            pos = expect_opening(data, pos, 1, what)?;
423            let (setpoint_reference, p) = decode_dopr_body(data, pos, what)?;
424            pos = p;
425            pos = expect_closing(data, pos, 1, what)?;
426            let (low_diff_limit, p) = decode_ctx_real(data, pos, 2, what)?;
427            pos = p;
428            let (high_diff_limit, p) = decode_ctx_real(data, pos, 3, what)?;
429            pos = p;
430            let (deadband, p) = decode_ctx_real(data, pos, 4, what)?;
431            pos = p;
432            pos = expect_closing(data, pos, 4, what)?;
433            (
434                EP::FloatingLimit {
435                    time_delay,
436                    setpoint_reference,
437                    low_diff_limit,
438                    high_diff_limit,
439                    deadband,
440                },
441                pos,
442            )
443        }
444        5 => {
445            let mut pos = pos;
446            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
447            let time_delay = u32::try_from(raw)
448                .map_err(|_| Error::decoding(pos, "out-of-range: time-delay exceeds u32"))?;
449            pos = p;
450            let (low_limit, p) = decode_ctx_real(data, pos, 1, what)?;
451            pos = p;
452            let (high_limit, p) = decode_ctx_real(data, pos, 2, what)?;
453            pos = p;
454            let (deadband, p) = decode_ctx_real(data, pos, 3, what)?;
455            pos = p;
456            pos = expect_closing(data, pos, 5, what)?;
457            (
458                EP::OutOfRange {
459                    time_delay,
460                    low_limit,
461                    high_limit,
462                    deadband,
463                },
464                pos,
465            )
466        }
467        9 => {
468            let mut pos = pos;
469            let (raw, p) = decode_ctx_unsigned(data, pos, 0, what)?;
470            let vendor_id = u16::try_from(raw)
471                .map_err(|_| Error::decoding(pos, "extended: vendor-id exceeds u16"))?;
472            pos = p;
473            let (raw, p) = decode_ctx_unsigned(data, pos, 1, what)?;
474            let extended_event_type = u32::try_from(raw)
475                .map_err(|_| Error::decoding(pos, "extended: extended-event-type exceeds u32"))?;
476            pos = p;
477            pos = expect_opening(data, pos, 2, what)?;
478            let (raw_params, p) = tags::extract_context_value(data, pos, 2)?;
479            validate_extended_parameters(raw_params, "extended")?;
480            pos = p;
481            pos = expect_closing(data, pos, 9, what)?;
482            (
483                EP::Extended {
484                    vendor_id,
485                    extended_event_type,
486                    parameters: raw_params.to_vec(),
487                },
488                pos,
489            )
490        }
491        n => {
492            // Unmodeled constructed alternative: preserve the SEQUENCE body
493            // verbatim after validating its nested tag structure.
494            let (body, p) = tags::extract_context_value(data, pos, n)?;
495            validate_tlv_sequence(body, "opaque event parameters")?;
496            (
497                EP::Opaque {
498                    tag: n,
499                    data: body.to_vec(),
500                },
501                p,
502            )
503        }
504    };
505    let (value, end) = value;
506    validate_tlv_sequence(&data[offset..end], what)?;
507    Ok((value, end))
508}