Skip to main content

bacnet_encoding/constructed/
audit_record.rs

1use super::{decode_audit_notification_at, encode_audit_notification};
2use crate::{primitives, tags};
3use bacnet_types::constructed::{
4    BACnetAuditLogDatum, BACnetAuditLogRecord, BACnetAuditLogRecordResult,
5};
6use bacnet_types::error::Error;
7use bacnet_types::primitives::{Date, Time};
8use bytes::BytesMut;
9
10/// Encode one adjacent `BACnetAuditLogRecordResult` sequence.
11pub fn encode_audit_log_record_result(
12    result: &BACnetAuditLogRecordResult,
13    buf: &mut BytesMut,
14) -> Result<(), Error> {
15    primitives::encode_ctx_unsigned(buf, 0, result.sequence_number);
16    tags::encode_opening_tag(buf, 1);
17    encode_audit_log_record(&result.record, buf)?;
18    tags::encode_closing_tag(buf, 1);
19    Ok(())
20}
21
22/// Encode one bare `BACnetAuditLogRecord` field sequence.
23pub fn encode_audit_log_record(
24    record: &BACnetAuditLogRecord,
25    buf: &mut BytesMut,
26) -> Result<(), Error> {
27    validate_date_time(&record.timestamp.0, &record.timestamp.1)?;
28
29    tags::encode_opening_tag(buf, 0);
30    primitives::encode_app_date(buf, &record.timestamp.0);
31    primitives::encode_app_time(buf, &record.timestamp.1);
32    tags::encode_closing_tag(buf, 0);
33
34    tags::encode_opening_tag(buf, 1);
35    match &record.datum {
36        BACnetAuditLogDatum::LogStatus(status) => {
37            if status & !0b111 != 0 {
38                return Err(Error::OutOfRange(format!(
39                    "BACnetAuditLogRecord log-status {status:#010b} exceeds three bits"
40                )));
41            }
42            primitives::encode_ctx_bit_string(buf, 0, 5, &[*status << 5]);
43        }
44        BACnetAuditLogDatum::AuditNotification(notification) => {
45            tags::encode_opening_tag(buf, 1);
46            encode_audit_notification(notification, buf)?;
47            tags::encode_closing_tag(buf, 1);
48        }
49        BACnetAuditLogDatum::TimeChange(change) => {
50            primitives::encode_ctx_real(buf, 2, *change);
51        }
52    }
53    tags::encode_closing_tag(buf, 1);
54    Ok(())
55}
56
57/// Decode one adjacent result sequence starting at `offset`.
58pub fn decode_audit_log_record_result_at(
59    data: &[u8],
60    offset: usize,
61) -> Result<(BACnetAuditLogRecordResult, usize), Error> {
62    let (sequence_contents, record_start) =
63        decode_context(data, offset, 0, "AuditLogQuery-ACK record sequence-number")?;
64    if sequence_contents.is_empty() || sequence_contents.len() > 8 {
65        return Err(Error::decoding(
66            offset,
67            "AuditLogQuery-ACK sequence-number must contain one to eight octets",
68        ));
69    }
70    let sequence_number = decode_canonical_unsigned(
71        sequence_contents,
72        offset,
73        "AuditLogQuery-ACK sequence-number",
74    )?;
75
76    let (record_body, next) = decode_constructed_body(data, record_start, 1, "record value")?;
77    let record = decode_audit_log_record(record_body)?;
78    Ok((
79        BACnetAuditLogRecordResult {
80            sequence_number,
81            record,
82        },
83        next,
84    ))
85}
86
87/// Decode a complete bare `BACnetAuditLogRecord` field sequence.
88pub fn decode_audit_log_record(data: &[u8]) -> Result<BACnetAuditLogRecord, Error> {
89    let (timestamp_body, datum_start) = decode_constructed_body(data, 0, 0, "record timestamp")?;
90    let timestamp = decode_date_time(timestamp_body)?;
91
92    let (datum_body, end) = decode_constructed_body(data, datum_start, 1, "record datum")?;
93    if end != data.len() {
94        return Err(Error::decoding(
95            end,
96            "BACnetAuditLogRecord has trailing fields",
97        ));
98    }
99    let datum = decode_datum(datum_body)?;
100
101    Ok(BACnetAuditLogRecord { timestamp, datum })
102}
103
104fn decode_date_time(data: &[u8]) -> Result<(Date, Time), Error> {
105    let (date_tag, date_start) = tags::decode_tag(data, 0)?;
106    if date_tag.class != tags::TagClass::Application
107        || date_tag.number != tags::app_tag::DATE
108        || date_tag.is_opening
109        || date_tag.is_closing
110        || date_tag.length != 4
111    {
112        return Err(Error::decoding(
113            0,
114            "BACnetAuditLogRecord timestamp expected four-octet application Date",
115        ));
116    }
117    let date_end = date_start
118        .checked_add(4)
119        .ok_or_else(|| Error::decoding(date_start, "timestamp Date length overflow"))?;
120    if date_end > data.len() {
121        return Err(Error::decoding(date_start, "timestamp Date is truncated"));
122    }
123    let date = Date::decode(&data[date_start..date_end])?;
124
125    let (time_tag, time_start) = tags::decode_tag(data, date_end)?;
126    if time_tag.class != tags::TagClass::Application
127        || time_tag.number != tags::app_tag::TIME
128        || time_tag.is_opening
129        || time_tag.is_closing
130        || time_tag.length != 4
131    {
132        return Err(Error::decoding(
133            date_end,
134            "BACnetAuditLogRecord timestamp expected four-octet application Time",
135        ));
136    }
137    let time_end = time_start
138        .checked_add(4)
139        .ok_or_else(|| Error::decoding(time_start, "timestamp Time length overflow"))?;
140    if time_end > data.len() {
141        return Err(Error::decoding(time_start, "timestamp Time is truncated"));
142    }
143    if time_end != data.len() {
144        return Err(Error::decoding(
145            time_end,
146            "BACnetAuditLogRecord timestamp has trailing fields",
147        ));
148    }
149    let time = Time::decode(&data[time_start..time_end])?;
150    validate_date_time(&date, &time).map_err(|error| {
151        Error::decoding(
152            0,
153            format!("BACnetAuditLogRecord timestamp is malformed: {error}"),
154        )
155    })?;
156    Ok((date, time))
157}
158
159fn decode_datum(data: &[u8]) -> Result<BACnetAuditLogDatum, Error> {
160    let (choice, _) = tags::decode_tag(data, 0)?;
161    if choice.is_context(0) {
162        let (contents, end) = decode_context(data, 0, 0, "BACnetAuditLogRecord log-status")?;
163        if end != data.len() {
164            return Err(Error::decoding(
165                end,
166                "log-status choice has trailing fields",
167            ));
168        }
169        if contents.len() != 2 || contents[0] != 5 || contents[1] & 0x1f != 0 {
170            return Err(Error::decoding(
171                0,
172                "BACnetAuditLogRecord log-status must be a canonical three-bit BitString",
173            ));
174        }
175        Ok(BACnetAuditLogDatum::LogStatus(contents[1] >> 5))
176    } else if choice.is_opening_tag(1) {
177        let (notification_body, end) =
178            decode_constructed_body(data, 0, 1, "AuditNotification choice")?;
179        if end != data.len() {
180            return Err(Error::decoding(
181                end,
182                "AuditNotification choice has trailing fields",
183            ));
184        }
185        let (notification, notification_end) = decode_audit_notification_at(notification_body, 0)?;
186        if notification_end != notification_body.len() {
187            return Err(Error::decoding(
188                notification_end,
189                "AuditNotification choice has trailing notification fields",
190            ));
191        }
192        Ok(BACnetAuditLogDatum::AuditNotification(notification))
193    } else if choice.is_context(2) {
194        let (contents, end) = decode_context(data, 0, 2, "BACnetAuditLogRecord time-change")?;
195        if end != data.len() {
196            return Err(Error::decoding(
197                end,
198                "time-change choice has trailing fields",
199            ));
200        }
201        Ok(BACnetAuditLogDatum::TimeChange(primitives::decode_real(
202            contents,
203        )?))
204    } else {
205        Err(Error::decoding(
206            0,
207            "BACnetAuditLogRecord datum expected context [0], constructed [1], or context [2]",
208        ))
209    }
210}
211
212fn decode_constructed_body<'a>(
213    data: &'a [u8],
214    offset: usize,
215    tag_number: u8,
216    field: &str,
217) -> Result<(&'a [u8], usize), Error> {
218    let (opening, body_start) = tags::decode_tag(data, offset)?;
219    if !opening.is_opening_tag(tag_number) {
220        return Err(Error::decoding(
221            offset,
222            format!("AuditLogQuery-ACK {field} expected opening tag [{tag_number}]"),
223        ));
224    }
225    tags::extract_context_value(data, body_start, tag_number)
226}
227
228fn validate_date_time(date: &Date, time: &Time) -> Result<(), Error> {
229    let date_valid = (date.month == Date::UNSPECIFIED || (1..=14).contains(&date.month))
230        && (date.day == Date::UNSPECIFIED || (1..=34).contains(&date.day))
231        && (date.day_of_week == Date::UNSPECIFIED || (1..=7).contains(&date.day_of_week));
232    let time_valid = (time.hour == Time::UNSPECIFIED || time.hour <= 23)
233        && (time.minute == Time::UNSPECIFIED || time.minute <= 59)
234        && (time.second == Time::UNSPECIFIED || time.second <= 59)
235        && (time.hundredths == Time::UNSPECIFIED || time.hundredths <= 99);
236    if !date_valid || !time_valid {
237        return Err(Error::Encoding(
238            "BACnetAuditLogRecord timestamp contains an invalid Date or Time component".into(),
239        ));
240    }
241    Ok(())
242}
243
244fn decode_context<'a>(
245    data: &'a [u8],
246    offset: usize,
247    expected_tag: u8,
248    field: &str,
249) -> Result<(&'a [u8], usize), Error> {
250    let (tag, contents_start) = tags::decode_tag(data, offset)?;
251    if !tag.is_context(expected_tag) {
252        return Err(Error::decoding(
253            offset,
254            format!("{field} expected context tag [{expected_tag}]"),
255        ));
256    }
257    let end = contents_start
258        .checked_add(tag.length as usize)
259        .ok_or_else(|| Error::decoding(contents_start, format!("{field} length overflow")))?;
260    if end > data.len() {
261        return Err(Error::buffer_too_short(end, data.len()));
262    }
263    Ok((&data[contents_start..end], end))
264}
265
266fn decode_canonical_unsigned(data: &[u8], offset: usize, field: &str) -> Result<u64, Error> {
267    if data.is_empty() || data.len() > 8 {
268        return Err(Error::decoding(
269            offset,
270            format!("{field} must contain one to eight octets"),
271        ));
272    }
273    if data.len() > 1 && data.first() == Some(&0) {
274        return Err(Error::decoding(
275            offset,
276            format!("{field} must use the shortest Unsigned/Enumerated encoding"),
277        ));
278    }
279    primitives::decode_unsigned(data)
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    fn audit_record() -> BACnetAuditLogRecord {
287        BACnetAuditLogRecord {
288            timestamp: (
289                Date {
290                    year: 124,
291                    month: 2,
292                    day: 29,
293                    day_of_week: 4,
294                },
295                Time {
296                    hour: 12,
297                    minute: 34,
298                    second: 56,
299                    hundredths: 78,
300                },
301            ),
302            datum: BACnetAuditLogDatum::LogStatus(0b010),
303        }
304    }
305
306    #[test]
307    fn audit_record_shared_codec_round_trips_and_rejects_trailing_data() {
308        let expected = audit_record();
309        let mut encoded = BytesMut::new();
310        encode_audit_log_record(&expected, &mut encoded).unwrap();
311        assert_eq!(decode_audit_log_record(&encoded).unwrap(), expected);
312
313        encoded.extend_from_slice(&[0]);
314        assert!(decode_audit_log_record(&encoded).is_err());
315    }
316
317    #[test]
318    fn audit_record_result_preserves_u64_sequence_identity() {
319        let expected = BACnetAuditLogRecordResult {
320            sequence_number: u64::MAX,
321            record: audit_record(),
322        };
323        let mut encoded = BytesMut::new();
324        encode_audit_log_record_result(&expected, &mut encoded).unwrap();
325        let (decoded, end) = decode_audit_log_record_result_at(&encoded, 0).unwrap();
326        assert_eq!(end, encoded.len());
327        assert_eq!(decoded, expected);
328    }
329}