Skip to main content

bacnet_encoding/constructed/
schedule.rs

1//! Clause 21 codecs for the Schedule object's constructed properties
2//! (Clause 12.24): Weekly_Schedule and Exception_Schedule.
3//!
4//! - `BACnetTimeValue` is a SEQUENCE of two untagged members, so it is an
5//!   application-tagged Time followed by one application-tagged primitive
6//!   value, with no frame. [`BACnetTimeValue::value`] holds that value typed.
7//! - `BACnetDailySchedule` is a SEQUENCE with one member, day-schedule `[0]`,
8//!   a SEQUENCE OF `BACnetTimeValue`: an opening/closing `[0]` pair around the
9//!   time-values. Weekly_Schedule is a BACnetARRAY\[7\] of them, Monday first.
10//! - `BACnetSpecialEvent` is three members with no outer frame:
11//!
12//! | Member              | Tag          | Encoding                                         |
13//! |---------------------|--------------|--------------------------------------------------|
14//! | period              | (untagged)   | CHOICE: calendar-entry `[0]` or calendar-reference `[1]` |
15//! | list-of-time-values | `[2]`        | opening/closing pair around the time-values      |
16//! | event-priority      | `[3]`        | primitive Unsigned, 1 through 16 when valid      |
17//!
18//! The calendar-entry alternative wraps a `BACnetCalendarEntry`, itself a
19//! CHOICE, so it is an opening/closing `[0]` pair around the entry's own
20//! tagged encoding (see [`super::calendar`]). The calendar-reference
21//! alternative is a primitive `[1]` Calendar object identifier.
22//! Exception_Schedule is a BACnetARRAY of special events, concatenated when
23//! read whole.
24//!
25//! These codecs check structure only, like the calendar-entry codec: a
26//! well-formed event with a priority outside 1 through 16 decodes, and the
27//! consumer range-checks it (#1087).
28
29#[cfg(not(feature = "std"))]
30use alloc::vec::Vec;
31
32use bacnet_types::constructed::{BACnetSpecialEvent, BACnetTimeValue, SpecialEventPeriod};
33use bacnet_types::error::{DecodeError, DecodingKind, Error};
34use bacnet_types::primitives::Time;
35use bytes::BytesMut;
36
37use super::calendar::{decode_calendar_entry, encode_calendar_entry};
38use super::tagged::{
39    contents, decode_ctx_object_id, decode_ctx_unsigned, expect_closing, expect_end,
40    expect_opening, next_is_closing,
41};
42use super::MAX_FRAMED_ITEMS;
43use crate::primitives;
44use crate::tags::{self, TagClass};
45
46/// Encode one `BACnetTimeValue`: the application-tagged Time, then the
47/// application-tagged value.
48///
49/// Refuses a value that is not of a primitive datatype (a `List` or
50/// `ApplicationData`), which no time-value can carry.
51pub fn encode_time_value(buf: &mut BytesMut, time_value: &BACnetTimeValue) -> Result<(), Error> {
52    if !time_value.value.is_primitive() {
53        return Err(Error::encode("time-value: the value must be of a primitive datatype"));
54    }
55    primitives::encode_app_time(buf, &time_value.time);
56    primitives::encode_property_value(buf, &time_value.value)
57}
58
59/// Decode one `BACnetTimeValue` at `offset`.
60///
61/// The value must be a single application-tagged primitive. Returns the
62/// time-value and the offset just past it.
63pub fn decode_time_value(data: &[u8], offset: usize) -> Result<(BACnetTimeValue, usize), Error> {
64    let (tag, content) = tags::decode_tag(data, offset)?;
65    if tag.class != TagClass::Application || tag.number != tags::app_tag::TIME || tag.length != 4 {
66        return Err(Error::decoding(
67            offset,
68            "time-value: expected an application-tagged Time (4 octets)",
69        ));
70    }
71    let (octets, value_start) = contents(data, content, 4)?;
72    let time = Time::decode(octets)?;
73
74    let (tag, _) = tags::decode_tag(data, value_start)?;
75    if tag.class != TagClass::Application {
76        return Err(DecodeError::new(
77            DecodingKind::InvalidEncoding,
78            value_start,
79            "time-value: expected an application-tagged value, got context tag [{}]",
80        )
81        .with_value(tag.number.into())
82        .into());
83    }
84    let (value, value_end) = primitives::decode_application_value(data, value_start)?;
85    Ok((BACnetTimeValue { time, value }, value_end))
86}
87
88/// Encode a SEQUENCE OF `BACnetTimeValue` inside an opening/closing `tag` pair.
89fn encode_time_values(
90    buf: &mut BytesMut,
91    tag: u8,
92    time_values: &[BACnetTimeValue],
93) -> Result<(), Error> {
94    tags::encode_opening_tag(buf, tag);
95    for time_value in time_values {
96        encode_time_value(buf, time_value)?;
97    }
98    tags::encode_closing_tag(buf, tag);
99    Ok(())
100}
101
102/// Decode a SEQUENCE OF `BACnetTimeValue` framed by an opening/closing `tag`
103/// pair at `offset`; returns the time-values and the offset past the closing tag.
104fn decode_time_values(
105    data: &[u8],
106    offset: usize,
107    tag: u8,
108    what: &'static str,
109) -> Result<(Vec<BACnetTimeValue>, usize), Error> {
110    let mut pos = expect_opening(data, offset, tag, what)?;
111    let mut time_values = Vec::new();
112    loop {
113        if next_is_closing(data, pos, tag)? {
114            return Ok((time_values, expect_closing(data, pos, tag, what)?));
115        }
116        if time_values.len() >= MAX_FRAMED_ITEMS {
117            return Err(DecodeError::new(
118                DecodingKind::Overflow,
119                pos,
120                "time-values exceed item limit",
121            )
122            .within(what)
123            .into());
124        }
125        let (time_value, next) = decode_time_value(data, pos)?;
126        time_values.push(time_value);
127        pos = next;
128    }
129}
130
131/// Encode the period CHOICE of a `BACnetSpecialEvent`.
132pub fn encode_special_event_period(buf: &mut BytesMut, period: &SpecialEventPeriod) {
133    match period {
134        SpecialEventPeriod::CalendarEntry(entry) => {
135            tags::encode_opening_tag(buf, 0);
136            encode_calendar_entry(buf, entry);
137            tags::encode_closing_tag(buf, 0);
138        }
139        SpecialEventPeriod::CalendarReference(oid) => primitives::encode_ctx_object_id(buf, 1, oid),
140    }
141}
142
143/// Decode the period CHOICE of a `BACnetSpecialEvent` at `offset`.
144pub fn decode_special_event_period(
145    data: &[u8],
146    offset: usize,
147) -> Result<(SpecialEventPeriod, usize), Error> {
148    let (tag, content) = tags::decode_tag(data, offset)?;
149    if tag.is_opening_tag(0) {
150        let (entry, after) = decode_calendar_entry(data, content)?;
151        let end = expect_closing(data, after, 0, "special event calendar-entry")?;
152        return Ok((SpecialEventPeriod::CalendarEntry(entry), end));
153    }
154    if tag.is_context(1) {
155        let (oid, end) = decode_ctx_object_id(data, offset, 1, "special event calendar-reference")?;
156        return Ok((SpecialEventPeriod::CalendarReference(oid), end));
157    }
158    Err(Error::decoding(
159        offset,
160        "special event period requires calendar-entry [0] or calendar-reference [1]",
161    ))
162}
163
164/// Encode one `BACnetSpecialEvent`; fails only on a time-value that
165/// [`encode_time_value`] refuses. The event-priority goes out as held, in or
166/// out of its range.
167pub fn encode_special_event(buf: &mut BytesMut, event: &BACnetSpecialEvent) -> Result<(), Error> {
168    encode_special_event_period(buf, &event.period);
169    encode_time_values(buf, 2, &event.list_of_time_values)?;
170    primitives::encode_ctx_unsigned(buf, 3, event.event_priority);
171    Ok(())
172}
173
174/// Decode one `BACnetSpecialEvent` at `offset`; returns the event and the
175/// offset just past it.
176///
177/// The event-priority decodes as any Unsigned. Its 1 through 16 range is a
178/// property of the value, not of the encoding, so a consumer that stores or
179/// acts on the event checks it: the Schedule object refuses a priority out of
180/// range with VALUE_OUT_OF_RANGE (Clause 15.9.1.3), as it does for an inline
181/// calendar entry holding an octet out of range.
182pub fn decode_special_event(
183    data: &[u8],
184    offset: usize,
185) -> Result<(BACnetSpecialEvent, usize), Error> {
186    let (period, pos) = decode_special_event_period(data, offset)?;
187    let (list_of_time_values, pos) =
188        decode_time_values(data, pos, 2, "special event list-of-time-values")?;
189    let (event_priority, end) =
190        decode_ctx_unsigned::<u64>(data, pos, 3, "special event event-priority")?;
191    Ok((
192        BACnetSpecialEvent {
193            period,
194            list_of_time_values,
195            event_priority,
196        },
197        end,
198    ))
199}
200
201/// Encode one `BACnetDailySchedule`: its time-values inside `[0]`. Fails only
202/// on a time-value that [`encode_time_value`] refuses.
203pub fn encode_daily_schedule(
204    buf: &mut BytesMut,
205    time_values: &[BACnetTimeValue],
206) -> Result<(), Error> {
207    encode_time_values(buf, 0, time_values)
208}
209
210/// Decode one `BACnetDailySchedule` at `offset`; returns its time-values and
211/// the offset just past the closing `[0]`.
212pub fn decode_daily_schedule(
213    data: &[u8],
214    offset: usize,
215) -> Result<(Vec<BACnetTimeValue>, usize), Error> {
216    decode_time_values(data, offset, 0, "daily schedule")
217}
218
219/// Encode a whole Weekly_Schedule: seven daily schedules, Monday first.
220pub fn encode_weekly_schedule(
221    buf: &mut BytesMut,
222    days: &[Vec<BACnetTimeValue>; 7],
223) -> Result<(), Error> {
224    for day in days {
225        encode_daily_schedule(buf, day)?;
226    }
227    Ok(())
228}
229
230/// The refusal of a schedule's entry `index` at `offset`, named by `what`:
231/// the inner reader's fault, restated there without formatting (#1776).
232#[cold]
233fn entry_refusal(error: &Error, offset: usize, what: &'static str, index: u16) -> Error {
234    error
235        .to_decode_error(offset)
236        .within(what)
237        .at_item(index)
238        .into()
239}
240
241/// Decode a whole Weekly_Schedule.
242///
243/// The data must hold exactly seven daily schedules and nothing after them:
244/// the property is a fixed-size BACnetARRAY\[7\].
245pub fn decode_weekly_schedule(data: &[u8]) -> Result<[Vec<BACnetTimeValue>; 7], Error> {
246    let mut days: [Vec<BACnetTimeValue>; 7] = Default::default();
247    let mut pos = 0;
248    for (index, day) in (0u16..).zip(days.iter_mut()) {
249        let (time_values, next) = decode_daily_schedule(data, pos)
250            .map_err(|error| entry_refusal(&error, pos, "weekly schedule day", index))?;
251        *day = time_values;
252        pos = next;
253    }
254    expect_end(data, pos, pos, "weekly schedule after 7 daily schedules")?;
255    Ok(days)
256}
257
258/// Encode a whole Exception_Schedule: the special events, concatenated.
259pub fn encode_exception_schedule(
260    buf: &mut BytesMut,
261    events: &[BACnetSpecialEvent],
262) -> Result<(), Error> {
263    for event in events {
264        encode_special_event(buf, event)?;
265    }
266    Ok(())
267}
268
269/// Decode a whole Exception_Schedule: zero or more special events, back to
270/// back, with every byte belonging to one. Like [`decode_special_event`], it
271/// leaves each event-priority's range to the caller.
272pub fn decode_exception_schedule(data: &[u8]) -> Result<Vec<BACnetSpecialEvent>, Error> {
273    let mut events = Vec::new();
274    let mut pos = 0;
275    while pos < data.len() {
276        if events.len() >= MAX_FRAMED_ITEMS {
277            return Err(Error::overflow(
278                pos,
279                "exception schedule exceeds item limit",
280            ));
281        }
282        // The item limit keeps the count within u16.
283        let index = u16::try_from(events.len()).unwrap_or(u16::MAX);
284        let (event, next) = decode_special_event(data, pos)
285            .map_err(|error| entry_refusal(&error, pos, "exception schedule entry", index))?;
286        events.push(event);
287        pos = next;
288    }
289    Ok(events)
290}