Skip to main content

bacnet_encoding/constructed/
calendar.rs

1//! Clause 21 codecs for `BACnetCalendarEntry` and `BACnetDateRange`.
2//!
3//! A calendar entry is the element of the Calendar object's Date_List
4//! (Clause 12.9) and the inline period of a Schedule `BACnetSpecialEvent`
5//! (Clause 12.24). It is a CHOICE whose alternatives carry their own context
6//! tags:
7//!
8//! | Alternative | Tag   | Encoding                                          |
9//! |-------------|-------|---------------------------------------------------|
10//! | date        | `[0]` | primitive, the four Date octets                   |
11//! | date-range  | `[1]` | opening/closing pair around a `BACnetDateRange`   |
12//! | weekNDay    | `[2]` | primitive, the three `BACnetWeekNDay` octets      |
13//!
14//! `BACnetDateRange` is a SEQUENCE of two untagged Dates, so each is
15//! application-tagged: start date, then end date. Unframed, as a Schedule's
16//! Effective_Period carries it, that pair is the whole encoding. A BACnetLIST
17//! of calendar entries is the plain concatenation of its elements.
18
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21
22use bacnet_types::constructed::{BACnetCalendarEntry, BACnetDateRange, BACnetWeekNDay};
23use bacnet_types::error::Error;
24use bacnet_types::primitives::Date;
25use bytes::BytesMut;
26
27use super::tagged::{contents, expect_closing};
28use super::MAX_FRAMED_ITEMS;
29use crate::primitives;
30use crate::tags::{self, TagClass};
31
32/// Encode one unframed `BACnetDateRange`: its start and end application Dates.
33pub fn encode_date_range(buf: &mut BytesMut, range: &BACnetDateRange) {
34    primitives::encode_app_date(buf, &range.start_date);
35    primitives::encode_app_date(buf, &range.end_date);
36}
37
38/// Decode one unframed `BACnetDateRange` at `offset`.
39///
40/// Returns the range and the offset just past it. Both members must be
41/// application-tagged Dates of exactly four octets.
42pub fn decode_date_range(data: &[u8], offset: usize) -> Result<(BACnetDateRange, usize), Error> {
43    let (start_date, offset) = decode_app_date(data, offset, "date range start-date")?;
44    let (end_date, offset) = decode_app_date(data, offset, "date range end-date")?;
45    Ok((
46        BACnetDateRange {
47            start_date,
48            end_date,
49        },
50        offset,
51    ))
52}
53
54/// Encode one `BACnetCalendarEntry` under its CHOICE tag.
55pub fn encode_calendar_entry(buf: &mut BytesMut, entry: &BACnetCalendarEntry) {
56    match entry {
57        BACnetCalendarEntry::Date(date) => primitives::encode_ctx_date(buf, 0, date),
58        BACnetCalendarEntry::DateRange(range) => {
59            tags::encode_opening_tag(buf, 1);
60            encode_date_range(buf, range);
61            tags::encode_closing_tag(buf, 1);
62        }
63        BACnetCalendarEntry::WeekNDay(week_n_day) => {
64            primitives::encode_ctx_octet_string(buf, 2, &week_n_day.encode());
65        }
66    }
67}
68
69/// Decode one `BACnetCalendarEntry` at `offset`.
70///
71/// Returns the entry and the offset just past it. Fails with
72/// [`Error::Decoding`] or [`Error::BufferTooShort`] when the leading tag is
73/// not one of the three context tags (an application-tagged Date or Octet
74/// String included), a primitive alternative has the wrong length, the
75/// date-range frame holds anything but two application Dates or is not
76/// closed by `[1]`, or the data ends early.
77pub fn decode_calendar_entry(
78    data: &[u8],
79    offset: usize,
80) -> Result<(BACnetCalendarEntry, usize), Error> {
81    let (tag, content) = tags::decode_tag(data, offset)?;
82    if tag.is_context(0) {
83        let (octets, end) = fixed_content(data, offset, content, tag.length, 4, "date [0]")?;
84        return Ok((BACnetCalendarEntry::Date(Date::decode(octets)?), end));
85    }
86    if tag.is_opening_tag(1) {
87        let (range, after) = decode_date_range(data, content)?;
88        let end = expect_closing(data, after, 1, "calendar entry date-range")?;
89        return Ok((BACnetCalendarEntry::DateRange(range), end));
90    }
91    if tag.is_context(2) {
92        let (octets, end) = fixed_content(data, offset, content, tag.length, 3, "weekNDay [2]")?;
93        return Ok((
94            BACnetCalendarEntry::WeekNDay(BACnetWeekNDay::decode(octets)?),
95            end,
96        ));
97    }
98    Err(Error::decoding(
99        offset,
100        "calendar entry requires date [0], date-range [1] or weekNDay [2]",
101    ))
102}
103
104/// Encode a BACnetLIST of `BACnetCalendarEntry` as concatenated elements.
105pub fn encode_calendar_entry_list(buf: &mut BytesMut, entries: &[BACnetCalendarEntry]) {
106    for entry in entries {
107        encode_calendar_entry(buf, entry);
108    }
109}
110
111/// Decode a complete BACnetLIST of `BACnetCalendarEntry`.
112///
113/// Every byte must belong to an entry; an empty input is an empty list.
114pub fn decode_calendar_entry_list(data: &[u8]) -> Result<Vec<BACnetCalendarEntry>, Error> {
115    let mut entries = Vec::new();
116    let mut offset = 0;
117    while offset < data.len() {
118        if entries.len() >= MAX_FRAMED_ITEMS {
119            return Err(Error::overflow(
120                offset,
121                "calendar entry list exceeds item limit",
122            ));
123        }
124        let (entry, end) = decode_calendar_entry(data, offset)?;
125        entries.push(entry);
126        offset = end;
127    }
128    Ok(entries)
129}
130
131/// Decode one application-tagged Date of exactly four octets.
132fn decode_app_date(data: &[u8], offset: usize, what: &str) -> Result<(Date, usize), Error> {
133    let (tag, content) = tags::decode_tag(data, offset)?;
134    if tag.class != TagClass::Application || tag.number != tags::app_tag::DATE {
135        return Err(Error::decoding(
136            offset,
137            format!("{what}: expected an application-tagged Date"),
138        ));
139    }
140    let (octets, end) = fixed_content(data, offset, content, tag.length, 4, what)?;
141    Ok((Date::decode(octets)?, end))
142}
143
144/// The content octets of a primitive tag that must hold exactly `expected`
145/// octets, and the offset just past them.
146fn fixed_content<'a>(
147    data: &'a [u8],
148    offset: usize,
149    content: usize,
150    length: u32,
151    expected: usize,
152    what: &str,
153) -> Result<(&'a [u8], usize), Error> {
154    if usize::try_from(length).ok() != Some(expected) {
155        return Err(Error::decoding(
156            offset,
157            format!("{what}: expected {expected} content octets, got {length}"),
158        ));
159    }
160    contents(data, content, length)
161}