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