Skip to main content

bacnet_encoding/constructed/
mod.rs

1//! Full ASN.1 framing codecs for constructed Clause-21 types whose property
2//! values are CHOICE/SEQUENCE productions — context-tagged framing per
3//! ASHRAE 135-2020 Clause 20.2.1.5/20.2.1.6.
4//!
5//! These codecs encode to and decode from raw application-layer bytes, the
6//! same bytes carried by `PropertyValue::ApplicationData`.
7//! Objects serve framed properties (e.g. `Event_Parameters`,
8//! `Fault_Parameters`, `Recipient_List`) by encoding with these functions;
9//! the flat application-tagged model in `primitives::encode_property_value`
10//! cannot express their wire form.
11//!
12//! Tag-form rules applied here:
13//!
14//! - A CHOICE alternative over a `SEQUENCE` is an opening/closing context tag
15//!   pair around the SEQUENCE's members.
16//! - A CHOICE alternative over a primitive base type is a context-specific
17//!   primitive tag holding the raw contents (e.g. `none [0] NULL`).
18//! - A context-tagged inner `CHOICE` is always explicitly tagged: an
19//!   opening/closing pair around the alternative's own encoding.
20//! - Inner members declared `[n] T` over primitive `T` are context-tagged
21//!   primitives; inner `SEQUENCE OF` / embedded `SEQUENCE` members are
22//!   constructed opening/closing pairs holding application-tagged elements.
23
24alloc_only! {
25    use bacnet_types::constructed::{
26        BACnetDeviceObjectPropertyReference, BACnetExtendedPropertyState, BACnetPropertyStates,
27        BACnetProprietaryPropertyState,
28    };
29    use bacnet_types::error::{DecodeError, DecodingKind, Error};
30    use bytes::BytesMut;
31
32    use crate::primitives;
33    use crate::tags::{self, TagClass};
34    use tagged::{
35        contents, decode_ctx_object_id, decode_ctx_unsigned, decode_optional_ctx, expect_closing,
36        expect_end, expect_opening,
37    };
38}
39
40// The tagged-field readers and the property-value boundary need no
41// allocator; every codec below decodes to or encodes from owned types.
42pub mod property_element;
43mod property_value;
44pub mod tagged;
45
46pub use property_element::{ConstructedElement, ElementShape, property_element};
47pub use property_value::{PropertyValueBoundary, extract_property_value};
48
49alloc_only! {
50    pub mod access_credential;
51    pub mod access_rule;
52    mod action_list;
53    pub mod assigned_landing_calls;
54    mod audit_notification;
55    mod audit_record;
56    pub mod authentication_policy;
57    pub mod calendar;
58    mod channel_value;
59    mod color_command;
60    pub mod cov_subscription;
61    mod event_log_record;
62    mod event_notification;
63    pub mod event_notification_subscription;
64    pub mod event_parameter;
65    pub mod fault_parameter;
66    mod floor_pairs;
67    pub mod landing_call_status;
68    pub mod landing_door_status;
69    pub mod lift_car_call_list;
70    mod lighting_command;
71    mod log_fields;
72    mod log_multiple_record;
73    mod log_record;
74    mod members;
75    mod name_value;
76    pub mod network_port;
77    pub mod object_property_reference;
78    pub mod port_permission;
79    mod property_access_result;
80    mod read_access;
81    pub mod recipient;
82    mod reference_sinks;
83    mod scale;
84    pub mod schedule;
85    mod shed_level;
86    pub mod staging;
87    mod value_source;
88
89    pub use access_credential::{
90        decode_assigned_access_rights, decode_authentication_factor,
91        decode_authentication_factor_format, decode_credential_authentication_factor,
92        encode_assigned_access_rights, encode_authentication_factor,
93        encode_authentication_factor_format, encode_credential_authentication_factor,
94    };
95    pub use access_rule::{decode_access_rule, encode_access_rule};
96    pub use action_list::{
97        decode_action_command, decode_action_list, encode_action_command, encode_action_list,
98    };
99    pub use assigned_landing_calls::{decode_assigned_landing_calls, encode_assigned_landing_calls};
100    pub use audit_notification::{decode_audit_notification_at, encode_audit_notification};
101    pub use audit_record::{
102        decode_audit_log_record, decode_audit_log_record_at, decode_audit_log_record_result_at,
103        encode_audit_log_record, encode_audit_log_record_result,
104    };
105    pub use authentication_policy::{decode_authentication_policy, encode_authentication_policy};
106    pub use calendar::{
107        decode_calendar_entry, decode_calendar_entry_list, decode_date_range, encode_calendar_entry,
108        encode_calendar_entry_list, encode_date_range,
109    };
110    pub use channel_value::{channel_value_end, constructed_channel_value, ConstructedChannelValue};
111    pub use color_command::{
112        decode_color_command, decode_color_command_value, decode_xy_color, encode_color_command,
113        encode_xy_color,
114    };
115    pub use cov_subscription::{
116        decode_cov_multiple_subscription, decode_cov_subscription, encode_cov_multiple_subscription,
117        encode_cov_multiple_subscription_list, encode_cov_subscription, encode_cov_subscription_list,
118    };
119    pub use event_log_record::{decode_event_log_record, encode_event_log_record};
120    pub use event_notification::{
121        decode_event_notification, decode_event_notification_tolerant, decode_notification_parameters,
122        encode_event_notification, encode_notification_parameters,
123    };
124    pub use event_notification_subscription::{
125        decode_event_notification_subscription, encode_event_notification_subscription,
126        encode_event_notification_subscription_list,
127    };
128    pub use event_parameter::{decode_event_parameter, encode_event_parameter};
129    pub use fault_parameter::{decode_fault_parameters, encode_fault_parameters};
130    pub use landing_call_status::{
131        decode_landing_call_status, decode_landing_call_status_list, encode_landing_call_status,
132        encode_landing_call_status_list,
133    };
134    pub use landing_door_status::{decode_landing_door_status, encode_landing_door_status};
135    pub use lift_car_call_list::{decode_lift_car_call_list, encode_lift_car_call_list};
136    pub use lighting_command::{
137        decode_lighting_command, decode_lighting_command_value, encode_lighting_command,
138    };
139    pub use log_multiple_record::{decode_log_multiple_record, encode_log_multiple_record};
140    pub use log_record::{decode_log_record, encode_log_record};
141    pub use name_value::{decode_name_value, encode_name_value};
142    pub use network_port::{
143        decode_bdt_entry, decode_bdt_entry_list, decode_fdt_entry, decode_fdt_entry_list,
144        decode_host_address, decode_host_n_port, encode_bdt_entry, encode_bdt_entry_list,
145        encode_fdt_entry, encode_fdt_entry_list, encode_host_address, encode_host_n_port,
146    };
147    pub use object_property_reference::{
148        decode_object_property_reference, decode_object_property_reference_at,
149        decode_setpoint_reference, decode_setpoint_reference_at, encode_object_property_reference,
150        encode_object_property_reference_to_sink, encode_setpoint_reference,
151        encode_setpoint_reference_to_sink,
152    };
153    pub use port_permission::{decode_port_permission, encode_port_permission};
154    pub use property_access_result::{decode_property_access_result, encode_property_access_result};
155    pub use read_access::{
156        decode_property_reference, decode_read_access_specification, encode_property_reference,
157        encode_property_reference_to_sink, encode_read_access_specification,
158    };
159    pub use recipient::{
160        check_decoded_mac_len, check_encoded_mac_len, decode_destination, decode_destination_list,
161        decode_recipient, encode_destination, encode_destination_list, encode_recipient,
162    };
163    pub use reference_sinks::encode_device_object_property_reference_to_sink;
164    pub use scale::{decode_prescale, decode_scale, encode_prescale, encode_scale};
165    pub use schedule::{
166        decode_daily_schedule, decode_exception_schedule, decode_special_event,
167        decode_special_event_period, decode_time_value, decode_weekly_schedule, encode_daily_schedule,
168        encode_exception_schedule, encode_special_event, encode_special_event_period,
169        encode_time_value, encode_weekly_schedule,
170    };
171    pub use shed_level::{decode_shed_level, encode_shed_level};
172    pub use staging::{
173        decode_device_object_reference, decode_stage_limit_value, encode_device_object_reference,
174        encode_device_object_reference_to_sink, encode_stage_limit_value,
175    };
176
177    pub use property_element::split_elements;
178    pub use value_source::{decode_value_source, encode_value_source};
179    pub use property_value::{
180        decode_bacnet_property_value, decode_bacnet_property_value_in_list,
181        decode_bacnet_property_value_in_list_detailed, encode_bacnet_property_value,
182        PropertyValueDecodeError, PropertyValueDecodeFailure, PropertyValueDecodeStage,
183    };
184}
185
186/// Upper bound on decoded SEQUENCE OF / list lengths, mirroring the socket-
187/// facing posture of `bacnet-services`' `MAX_DECODED_ITEMS`. Prevents memory
188/// exhaustion from malformed framed payloads.
189#[cfg(feature = "alloc")]
190const MAX_FRAMED_ITEMS: usize = 10_000;
191
192// ---------------------------------------------------------------------------
193// BACnetPropertyStates (Clause 21 CHOICE) — spec-tagged framing
194// ---------------------------------------------------------------------------
195
196/// Encode a [`BACnetPropertyStates`] using the Standard 135-2020 Clause 21 tags.
197///
198/// Returns an error without modifying `buf` when a constructed proprietary
199/// value does not contain a BACnet TLV sequence.
200#[cfg(feature = "alloc")]
201pub fn encode_property_state(
202    buf: &mut BytesMut,
203    state: &BACnetPropertyStates,
204) -> Result<(), Error> {
205    use BACnetPropertyStates as S;
206    if let S::Other(value) = state
207        && value.is_constructed()
208    {
209        validate_tlv_sequence(value.data(), "proprietary property-state body")?;
210        let mut framed = BytesMut::new();
211        tags::encode_opening_tag(&mut framed, value.tag());
212        let (_, body_start) = tags::decode_tag(&framed, 0)?;
213        framed.extend_from_slice(value.data());
214        tags::encode_closing_tag(&mut framed, value.tag());
215        let (_, end) = tags::extract_context_value(&framed, body_start, value.tag())?;
216        expect_end(&framed, end, end, "proprietary property-state body")?;
217    }
218    match state {
219        S::BooleanValue(v) => primitives::encode_ctx_boolean(buf, 0, *v),
220        S::BinaryValue(v) => primitives::encode_ctx_enumerated(buf, 1, *v),
221        S::EventType(v) => primitives::encode_ctx_enumerated(buf, 2, *v),
222        S::Polarity(v) => primitives::encode_ctx_enumerated(buf, 3, *v),
223        S::ProgramChange(v) => primitives::encode_ctx_enumerated(buf, 4, *v),
224        S::ProgramState(v) => primitives::encode_ctx_enumerated(buf, 5, *v),
225        S::ReasonForHalt(v) => primitives::encode_ctx_enumerated(buf, 6, *v),
226        S::Reliability(v) => primitives::encode_ctx_enumerated(buf, 7, *v),
227        S::State(v) => primitives::encode_ctx_enumerated(buf, 8, *v),
228        S::SystemStatus(v) => primitives::encode_ctx_enumerated(buf, 9, *v),
229        S::Units(v) => primitives::encode_ctx_enumerated(buf, 10, *v),
230        S::UnsignedValue(v) => primitives::encode_ctx_unsigned(buf, 11, *v as u64),
231        S::LifeSafetyMode(v) => primitives::encode_ctx_enumerated(buf, 12, *v),
232        S::LifeSafetyState(v) => primitives::encode_ctx_enumerated(buf, 13, *v),
233        S::RestartReason(v) => primitives::encode_ctx_enumerated(buf, 14, *v),
234        S::DoorAlarmState(v) => primitives::encode_ctx_enumerated(buf, 15, *v),
235        S::Action(v) => primitives::encode_ctx_enumerated(buf, 16, *v),
236        S::DoorSecuredStatus(v) => primitives::encode_ctx_enumerated(buf, 17, *v),
237        S::DoorStatus(v) => primitives::encode_ctx_enumerated(buf, 18, *v),
238        S::DoorValue(v) => primitives::encode_ctx_enumerated(buf, 19, *v),
239        S::FileAccessMethod(v) => primitives::encode_ctx_enumerated(buf, 20, *v),
240        S::LockStatus(v) => primitives::encode_ctx_enumerated(buf, 21, *v),
241        S::LifeSafetyOperation(v) => primitives::encode_ctx_enumerated(buf, 22, *v),
242        S::Maintenance(v) => primitives::encode_ctx_enumerated(buf, 23, *v),
243        S::NodeType(v) => primitives::encode_ctx_enumerated(buf, 24, *v),
244        S::NotifyType(v) => primitives::encode_ctx_enumerated(buf, 25, *v),
245        S::ShedState(v) => primitives::encode_ctx_enumerated(buf, 27, *v),
246        S::SilencedState(v) => primitives::encode_ctx_enumerated(buf, 28, *v),
247        S::AccessEvent(v) => primitives::encode_ctx_enumerated(buf, 30, *v),
248        S::ZoneOccupancyState(v) => primitives::encode_ctx_enumerated(buf, 31, *v),
249        S::AccessCredentialDisableReason(v) => primitives::encode_ctx_enumerated(buf, 32, *v),
250        S::AccessCredentialDisable(v) => primitives::encode_ctx_enumerated(buf, 33, *v),
251        S::AuthenticationStatus(v) => primitives::encode_ctx_enumerated(buf, 34, *v),
252        S::BackupState(v) => primitives::encode_ctx_enumerated(buf, 36, *v),
253        S::WriteStatus(v) => primitives::encode_ctx_enumerated(buf, 37, *v),
254        S::LightingInProgress(v) => primitives::encode_ctx_enumerated(buf, 38, *v),
255        S::LightingOperation(v) => primitives::encode_ctx_enumerated(buf, 39, *v),
256        S::LightingTransition(v) => primitives::encode_ctx_enumerated(buf, 40, *v),
257        S::IntegerValue(v) => primitives::encode_ctx_signed(buf, 41, *v),
258        S::BinaryLightingValue(v) => primitives::encode_ctx_enumerated(buf, 42, *v),
259        S::TimerState(v) => primitives::encode_ctx_enumerated(buf, 43, *v),
260        S::TimerTransition(v) => primitives::encode_ctx_enumerated(buf, 44, *v),
261        S::BacnetIpMode(v) => primitives::encode_ctx_enumerated(buf, 45, *v),
262        S::NetworkPortCommand(v) => primitives::encode_ctx_enumerated(buf, 46, *v),
263        S::NetworkType(v) => primitives::encode_ctx_enumerated(buf, 47, *v),
264        S::NetworkNumberQuality(v) => primitives::encode_ctx_enumerated(buf, 48, *v),
265        S::EscalatorOperationDirection(v) => primitives::encode_ctx_enumerated(buf, 49, *v),
266        S::EscalatorFault(v) => primitives::encode_ctx_enumerated(buf, 50, *v),
267        S::EscalatorMode(v) => primitives::encode_ctx_enumerated(buf, 51, *v),
268        S::LiftCarDirection(v) => primitives::encode_ctx_enumerated(buf, 52, *v),
269        S::LiftCarDoorCommand(v) => primitives::encode_ctx_enumerated(buf, 53, *v),
270        S::LiftCarDriveStatus(v) => primitives::encode_ctx_enumerated(buf, 54, *v),
271        S::LiftCarMode(v) => primitives::encode_ctx_enumerated(buf, 55, *v),
272        S::LiftGroupMode(v) => primitives::encode_ctx_enumerated(buf, 56, *v),
273        S::LiftFault(v) => primitives::encode_ctx_enumerated(buf, 57, *v),
274        S::ProtocolLevel(v) => primitives::encode_ctx_enumerated(buf, 58, *v),
275        S::AuditLevel(v) => primitives::encode_ctx_enumerated(buf, 59, *v),
276        S::AuditOperation(v) => primitives::encode_ctx_enumerated(buf, 60, *v),
277        S::ExtendedValue(v) => primitives::encode_ctx_unsigned(buf, 63, v.encoded() as u64),
278        S::Other(v) if v.is_constructed() => {
279            tags::encode_opening_tag(buf, v.tag());
280            buf.extend_from_slice(v.data());
281            tags::encode_closing_tag(buf, v.tag());
282        }
283        S::Other(v) => primitives::encode_ctx_octet_string(buf, v.tag(), v.data()),
284    }
285    Ok(())
286}
287
288/// Decode one [`BACnetPropertyStates`] CHOICE element at `offset`.
289///
290/// Proprietary tags 64 through 254 retain their encoded contents in
291/// [`BACnetPropertyStates::Other`]. Reserved standard tags are rejected.
292#[cfg(feature = "alloc")]
293pub fn decode_property_state(
294    data: &[u8],
295    offset: usize,
296) -> Result<(BACnetPropertyStates, usize), Error> {
297    use BACnetPropertyStates as S;
298    let (tag, pos) = tags::decode_tag(data, offset)?;
299    if tag.class != TagClass::Context || tag.is_closing {
300        return Err(tagged::misplaced_tag(
301            data,
302            &tag,
303            None,
304            offset,
305            "BACnetPropertyStates: expected a context tag",
306        ));
307    }
308    if tag.is_opening {
309        if !(64..=254).contains(&tag.number) {
310            return Err(Error::invalid_tag(
311                offset,
312                "BACnetPropertyStates: constructed form requires a proprietary tag",
313            ));
314        }
315        let (content, end) = tags::extract_context_value(data, pos, tag.number)?;
316        validate_tlv_sequence(content, "proprietary property-state body")?;
317        return Ok((
318            S::Other(BACnetProprietaryPropertyState::constructed(
319                tag.number,
320                content.to_vec(),
321            )?),
322            end,
323        ));
324    }
325    let (content, end) = contents(data, pos, tag.length)?;
326    let unsigned = || -> Result<u32, Error> {
327        u32::try_from(primitives::decode_unsigned(content)?)
328            .map_err(|_| Error::out_of_range(pos, "BACnetPropertyStates: contents exceed u32"))
329    };
330    let state = match tag.number {
331        0 => {
332            if content.len() != 1 {
333                return Err(DecodeError::new(
334                    DecodingKind::InvalidEncoding,
335                    offset,
336                    "BACnetPropertyStates boolean-value: expected 1 contents octet, got {}",
337                )
338                .with_value(content.len() as u64)
339                .into());
340            }
341            match content[0] {
342                0 => S::BooleanValue(false),
343                1 => S::BooleanValue(true),
344                value => {
345                    return Err(DecodeError::new(
346                        DecodingKind::InvalidEncoding,
347                        pos,
348                        "BACnetPropertyStates boolean-value must be 0 or 1, got {}",
349                    )
350                    .with_value(value.into())
351                    .into());
352                }
353            }
354        }
355        1 => S::BinaryValue(unsigned()?),
356        2 => S::EventType(unsigned()?),
357        3 => S::Polarity(unsigned()?),
358        4 => S::ProgramChange(unsigned()?),
359        5 => S::ProgramState(unsigned()?),
360        6 => S::ReasonForHalt(unsigned()?),
361        7 => S::Reliability(unsigned()?),
362        8 => S::State(unsigned()?),
363        9 => S::SystemStatus(unsigned()?),
364        10 => S::Units(unsigned()?),
365        11 => S::UnsignedValue(unsigned()?),
366        12 => S::LifeSafetyMode(unsigned()?),
367        13 => S::LifeSafetyState(unsigned()?),
368        14 => S::RestartReason(unsigned()?),
369        15 => S::DoorAlarmState(unsigned()?),
370        16 => S::Action(unsigned()?),
371        17 => S::DoorSecuredStatus(unsigned()?),
372        18 => S::DoorStatus(unsigned()?),
373        19 => S::DoorValue(unsigned()?),
374        20 => S::FileAccessMethod(unsigned()?),
375        21 => S::LockStatus(unsigned()?),
376        22 => S::LifeSafetyOperation(unsigned()?),
377        23 => S::Maintenance(unsigned()?),
378        24 => S::NodeType(unsigned()?),
379        25 => S::NotifyType(unsigned()?),
380        27 => S::ShedState(unsigned()?),
381        28 => S::SilencedState(unsigned()?),
382        30 => S::AccessEvent(unsigned()?),
383        31 => S::ZoneOccupancyState(unsigned()?),
384        32 => S::AccessCredentialDisableReason(unsigned()?),
385        33 => S::AccessCredentialDisable(unsigned()?),
386        34 => S::AuthenticationStatus(unsigned()?),
387        36 => S::BackupState(unsigned()?),
388        37 => S::WriteStatus(unsigned()?),
389        38 => S::LightingInProgress(unsigned()?),
390        39 => S::LightingOperation(unsigned()?),
391        40 => S::LightingTransition(unsigned()?),
392        41 => S::IntegerValue(primitives::decode_signed_canonical(content)?),
393        42 => S::BinaryLightingValue(unsigned()?),
394        43 => S::TimerState(unsigned()?),
395        44 => S::TimerTransition(unsigned()?),
396        45 => S::BacnetIpMode(unsigned()?),
397        46 => S::NetworkPortCommand(unsigned()?),
398        47 => S::NetworkType(unsigned()?),
399        48 => S::NetworkNumberQuality(unsigned()?),
400        49 => S::EscalatorOperationDirection(unsigned()?),
401        50 => S::EscalatorFault(unsigned()?),
402        51 => S::EscalatorMode(unsigned()?),
403        52 => S::LiftCarDirection(unsigned()?),
404        53 => S::LiftCarDoorCommand(unsigned()?),
405        54 => S::LiftCarDriveStatus(unsigned()?),
406        55 => S::LiftCarMode(unsigned()?),
407        56 => S::LiftGroupMode(unsigned()?),
408        57 => S::LiftFault(unsigned()?),
409        58 => S::ProtocolLevel(unsigned()?),
410        59 => S::AuditLevel(unsigned()?),
411        60 => S::AuditOperation(unsigned()?),
412        63 => S::ExtendedValue(BACnetExtendedPropertyState::from_encoded(unsigned()?)?),
413        other @ 64..=254 => S::Other(BACnetProprietaryPropertyState::primitive(
414            other,
415            content.to_vec(),
416        )?),
417        reserved => {
418            return Err(DecodeError::new(
419                DecodingKind::InvalidTag,
420                offset,
421                "BACnetPropertyStates context tag {} is reserved",
422            )
423            .with_value(reserved.into())
424            .into());
425        }
426    };
427    Ok((state, end))
428}
429
430// ---------------------------------------------------------------------------
431// BACnetDeviceObjectPropertyReference (Clause 21 SEQUENCE)
432// ---------------------------------------------------------------------------
433
434/// Encode the context-tagged members of a `BACnetDeviceObjectPropertyReference`:
435/// `object-identifier [0]`, `property-identifier [1]`, optional
436/// `property-array-index [2]`, optional `device-identifier [3]` — the body
437/// that sits between the enclosing field's opening/closing tags.
438#[cfg(feature = "alloc")]
439pub(crate) fn encode_dopr_body(buf: &mut BytesMut, r: &BACnetDeviceObjectPropertyReference) {
440    encode_device_object_property_reference_to_sink(buf, r)
441        .expect("BytesMut accepts each reference");
442}
443
444/// Decode the members written by [`encode_dopr_body`], stopping before the
445/// enclosing closing tag.
446#[cfg(feature = "alloc")]
447pub(crate) fn decode_dopr_body(
448    data: &[u8],
449    offset: usize,
450    what: &'static str,
451) -> Result<(BACnetDeviceObjectPropertyReference, usize), Error> {
452    let (object_identifier, offset) = decode_ctx_object_id(data, offset, 0, what)?;
453    let (property_identifier, offset) = decode_ctx_unsigned::<u32>(data, offset, 1, what)?;
454    let (property_array_index, offset) =
455        decode_optional_ctx(data, offset, 2, what, decode_ctx_unsigned::<u32>)?;
456    let (device_identifier, offset) =
457        decode_optional_ctx(data, offset, 3, what, decode_ctx_object_id)?;
458    Ok((
459        BACnetDeviceObjectPropertyReference {
460            object_identifier,
461            property_identifier,
462            property_array_index,
463            device_identifier,
464        },
465        offset,
466    ))
467}
468
469/// Encode one bare `BACnetDeviceObjectPropertyReference`, the counterpart of
470/// [`decode_device_object_property_reference`]: its members with no enclosing
471/// frame, so a BACnetLIST of these references is their encodings back to back.
472#[cfg(feature = "alloc")]
473pub fn encode_device_object_property_reference(
474    buf: &mut BytesMut,
475    r: &BACnetDeviceObjectPropertyReference,
476) {
477    encode_dopr_body(buf, r);
478}
479
480/// Decode one bare `BACnetDeviceObjectPropertyReference` at `offset`; returns
481/// it and the offset past its last member. A BACnetLIST of these references
482/// concatenates its elements with no frame, so walking the list calls this at
483/// each element's start.
484#[cfg(feature = "alloc")]
485pub fn decode_device_object_property_reference(
486    data: &[u8],
487    offset: usize,
488) -> Result<(BACnetDeviceObjectPropertyReference, usize), Error> {
489    decode_dopr_body(data, offset, "BACnetDeviceObjectPropertyReference")
490}
491
492/// Validate a BACnet TLV sequence without normalizing its encoded values.
493///
494/// This checks matching context tags, the context nesting limit, and
495/// application-value forms while preserving defined CharacterString encodings.
496#[cfg(feature = "alloc")]
497pub fn validate_tlv_sequence(data: &[u8], what: &'static str) -> Result<(), Error> {
498    let mut offset = 0;
499    let mut count = 0;
500    while offset < data.len() {
501        if count >= MAX_FRAMED_ITEMS {
502            return Err(DecodeError::new(
503                DecodingKind::Overflow,
504                offset,
505                "sequence exceeds item limit",
506            )
507            .within(what)
508            .into());
509        }
510        let (tag, content) = tags::decode_tag(data, offset)?;
511        if tag.is_opening {
512            let (inner, next) = tags::extract_context_value(data, content, tag.number)?;
513            validate_tlv_sequence(inner, what)?;
514            offset = next;
515        } else if tag.class == TagClass::Application {
516            offset = primitives::validate_application_value(data, offset)?;
517        } else {
518            let (_, next) = primitives::decode_application_value(data, offset)?;
519            offset = next;
520        }
521        count += 1;
522    }
523    Ok(())
524}
525
526/// Validate the shared Extended Event/Fault `parameters` production.
527/// Its only context-tagged CHOICE is `reference [0]` over
528/// `BACnetDeviceObjectPropertyReference`; NotificationParameters uses a
529/// different Extended production with `property-value [0]`.
530#[cfg(feature = "alloc")]
531pub(crate) fn validate_extended_parameters(data: &[u8], what: &'static str) -> Result<(), Error> {
532    let mut offset = 0;
533    let mut count = 0;
534    while offset < data.len() {
535        if count >= MAX_FRAMED_ITEMS {
536            return Err(DecodeError::new(
537                DecodingKind::Overflow,
538                offset,
539                "parameters exceed item limit",
540            )
541            .within(what)
542            .into());
543        }
544        let (tag, _) = tags::decode_tag(data, offset)?;
545        if tag.class == TagClass::Context {
546            let content = expect_opening(data, offset, 0, what)?;
547            let (_, after_reference) = decode_dopr_body(data, content, what)?;
548            offset = expect_closing(data, after_reference, 0, what)?;
549        } else {
550            offset = primitives::validate_application_value(data, offset)?;
551        }
552        count += 1;
553    }
554    Ok(())
555}
556
557#[cfg(all(test, feature = "alloc"))]
558pub(crate) mod tests;