Skip to main content

bacnet_encoding/constructed/
audit_record.rs

1use super::log_fields::{decode_log_status, encode_log_status};
2use super::tagged::{
3    decode_app_fixed, decode_ctx_canonical_unsigned, decode_ctx_constructed, decode_ctx_primitive,
4    expect_end,
5};
6use super::{decode_audit_notification_at, encode_audit_notification};
7use crate::{primitives, tags};
8use bacnet_types::constructed::{
9    BACnetAuditLogDatum, BACnetAuditLogRecord, BACnetAuditLogRecordResult,
10};
11use bacnet_types::error::Error;
12use bacnet_types::primitives::{Date, Time};
13use bytes::BytesMut;
14
15/// Encode one adjacent `BACnetAuditLogRecordResult` sequence.
16pub fn encode_audit_log_record_result(
17    result: &BACnetAuditLogRecordResult,
18    buf: &mut BytesMut,
19) -> Result<(), Error> {
20    primitives::encode_ctx_unsigned(buf, 0, result.sequence_number);
21    tags::encode_opening_tag(buf, 1);
22    encode_audit_log_record(&result.record, buf)?;
23    tags::encode_closing_tag(buf, 1);
24    Ok(())
25}
26
27/// Encode one bare `BACnetAuditLogRecord` field sequence.
28pub fn encode_audit_log_record(
29    record: &BACnetAuditLogRecord,
30    buf: &mut BytesMut,
31) -> Result<(), Error> {
32    validate_date_time(&record.timestamp.0, &record.timestamp.1)?;
33
34    tags::encode_opening_tag(buf, 0);
35    primitives::encode_app_date(buf, &record.timestamp.0);
36    primitives::encode_app_time(buf, &record.timestamp.1);
37    tags::encode_closing_tag(buf, 0);
38
39    tags::encode_opening_tag(buf, 1);
40    match &record.datum {
41        BACnetAuditLogDatum::LogStatus(status) => encode_log_status(buf, 0, *status),
42        BACnetAuditLogDatum::AuditNotification(notification) => {
43            tags::encode_opening_tag(buf, 1);
44            encode_audit_notification(notification, buf)?;
45            tags::encode_closing_tag(buf, 1);
46        }
47        BACnetAuditLogDatum::TimeChange(change) => {
48            primitives::encode_ctx_real(buf, 2, *change);
49        }
50    }
51    tags::encode_closing_tag(buf, 1);
52    Ok(())
53}
54
55/// Decode one adjacent result sequence starting at `offset`.
56pub fn decode_audit_log_record_result_at(
57    data: &[u8],
58    offset: usize,
59) -> Result<(BACnetAuditLogRecordResult, usize), Error> {
60    let (sequence_number, record_start) = decode_ctx_canonical_unsigned::<u64>(
61        data,
62        offset,
63        0,
64        "AuditLogQuery-ACK record sequence-number",
65    )?;
66    let (record_body, next) =
67        decode_ctx_constructed(data, record_start, 1, "AuditLogQuery-ACK record value")?;
68    let record = decode_audit_log_record(record_body)?;
69    Ok((
70        BACnetAuditLogRecordResult {
71            sequence_number,
72            record,
73        },
74        next,
75    ))
76}
77
78/// Decode a complete bare `BACnetAuditLogRecord` field sequence.
79pub fn decode_audit_log_record(data: &[u8]) -> Result<BACnetAuditLogRecord, Error> {
80    let (record, end) = decode_audit_log_record_at(data, 0)?;
81    expect_end(data, end, end, "BACnetAuditLogRecord")?;
82    Ok(record)
83}
84
85/// Decode one bare `BACnetAuditLogRecord` starting at `offset`, returning it
86/// and the offset just past it, so the records of a ReadRange item list
87/// decode one after another.
88pub fn decode_audit_log_record_at(
89    data: &[u8],
90    offset: usize,
91) -> Result<(BACnetAuditLogRecord, usize), Error> {
92    let (timestamp_body, datum_start) =
93        decode_ctx_constructed(data, offset, 0, "AuditLogQuery-ACK record timestamp")?;
94    let timestamp = decode_date_time(timestamp_body)?;
95    let (datum_body, end) =
96        decode_ctx_constructed(data, datum_start, 1, "AuditLogQuery-ACK record datum")?;
97    let datum = decode_datum(datum_body)?;
98    Ok((BACnetAuditLogRecord { timestamp, datum }, end))
99}
100
101fn decode_date_time(data: &[u8]) -> Result<(Date, Time), Error> {
102    const WHAT: &str = "BACnetAuditLogRecord timestamp";
103    let (date, date_end) = decode_app_fixed(data, 0, tags::app_tag::DATE, 4, WHAT)?;
104    let (time, time_end) = decode_app_fixed(data, date_end, tags::app_tag::TIME, 4, WHAT)?;
105    expect_end(data, time_end, time_end, WHAT)?;
106    let date = Date::decode(date)?;
107    let time = Time::decode(time)?;
108    validate_date_time(&date, &time).map_err(|error| {
109        Error::decoding(
110            0,
111            format!("BACnetAuditLogRecord timestamp is malformed: {error}"),
112        )
113    })?;
114    Ok((date, time))
115}
116
117fn decode_datum(data: &[u8]) -> Result<BACnetAuditLogDatum, Error> {
118    let (choice, _) = tags::decode_tag(data, 0)?;
119    if choice.is_context(0) {
120        const WHAT: &str = "BACnetAuditLogRecord log-status";
121        let (contents, end) = decode_ctx_primitive(data, 0, 0, WHAT)?;
122        expect_end(data, end, end, WHAT)?;
123        Ok(BACnetAuditLogDatum::LogStatus(decode_log_status(
124            contents,
125            0,
126            "BACnetAuditLogRecord",
127        )?))
128    } else if choice.is_opening_tag(1) {
129        const WHAT: &str = "AuditLogQuery-ACK AuditNotification choice";
130        let (notification_body, end) = decode_ctx_constructed(data, 0, 1, WHAT)?;
131        expect_end(data, end, end, WHAT)?;
132        let (notification, notification_end) = decode_audit_notification_at(notification_body, 0)?;
133        expect_end(
134            notification_body,
135            notification_end,
136            notification_end,
137            "AuditNotification choice notification",
138        )?;
139        Ok(BACnetAuditLogDatum::AuditNotification(notification))
140    } else if choice.is_context(2) {
141        const WHAT: &str = "BACnetAuditLogRecord time-change";
142        let (contents, end) = decode_ctx_primitive(data, 0, 2, WHAT)?;
143        expect_end(data, end, end, WHAT)?;
144        Ok(BACnetAuditLogDatum::TimeChange(primitives::decode_real(
145            contents,
146        )?))
147    } else {
148        Err(Error::decoding(
149            0,
150            "BACnetAuditLogRecord datum expected context [0], constructed [1], or context [2]",
151        ))
152    }
153}
154
155fn validate_date_time(date: &Date, time: &Time) -> Result<(), Error> {
156    let date_valid = (date.month == Date::UNSPECIFIED || (1..=14).contains(&date.month))
157        && (date.day == Date::UNSPECIFIED || (1..=34).contains(&date.day))
158        && (date.day_of_week == Date::UNSPECIFIED || (1..=7).contains(&date.day_of_week));
159    let time_valid = (time.hour == Time::UNSPECIFIED || time.hour <= 23)
160        && (time.minute == Time::UNSPECIFIED || time.minute <= 59)
161        && (time.second == Time::UNSPECIFIED || time.second <= 59)
162        && (time.hundredths == Time::UNSPECIFIED || time.hundredths <= 99);
163    if !date_valid || !time_valid {
164        return Err(Error::Encoding(
165            "BACnetAuditLogRecord timestamp contains an invalid Date or Time component".into(),
166        ));
167    }
168    Ok(())
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use bacnet_types::bitstring::LogStatus;
175
176    fn audit_record() -> BACnetAuditLogRecord {
177        BACnetAuditLogRecord {
178            timestamp: (
179                Date {
180                    year: 124,
181                    month: 2,
182                    day: 29,
183                    day_of_week: 4,
184                },
185                Time {
186                    hour: 12,
187                    minute: 34,
188                    second: 56,
189                    hundredths: 78,
190                },
191            ),
192            datum: BACnetAuditLogDatum::LogStatus(LogStatus::BUFFER_PURGED),
193        }
194    }
195
196    /// The log-status choice puts log-disabled, bit 0, in the top bit of the
197    /// octet (Clause 20.2.10), so it never reads back as log-interrupted.
198    #[test]
199    fn audit_record_log_status_has_bit0_first_wire_bytes() {
200        for (status, octet) in [
201            (LogStatus::LOG_DISABLED, 0x80),
202            (LogStatus::LOG_DISABLED | LogStatus::BUFFER_PURGED, 0xC0),
203            (LogStatus::LOG_INTERRUPTED, 0x20),
204        ] {
205            let record = BACnetAuditLogRecord {
206                datum: BACnetAuditLogDatum::LogStatus(status),
207                ..audit_record()
208            };
209            let mut encoded = BytesMut::new();
210            encode_audit_log_record(&record, &mut encoded).unwrap();
211            // Timestamp [0], then [1] around log-status [0] = 05 <octet>.
212            assert_eq!(&encoded[12..], &[0x1E, 0x0A, 0x05, octet, 0x1F], "{status}");
213            assert_eq!(decode_audit_log_record(&encoded).unwrap(), record);
214        }
215    }
216
217    #[test]
218    fn audit_record_shared_codec_round_trips_and_rejects_trailing_data() {
219        let expected = audit_record();
220        let mut encoded = BytesMut::new();
221        encode_audit_log_record(&expected, &mut encoded).unwrap();
222        assert_eq!(decode_audit_log_record(&encoded).unwrap(), expected);
223
224        encoded.extend_from_slice(&[0]);
225        assert!(decode_audit_log_record(&encoded).is_err());
226    }
227
228    #[test]
229    fn consecutive_audit_records_decode_by_returned_offset() {
230        let first = audit_record();
231        let second = BACnetAuditLogRecord {
232            datum: BACnetAuditLogDatum::TimeChange(-1.5),
233            ..audit_record()
234        };
235        let mut encoded = BytesMut::new();
236        encode_audit_log_record(&first, &mut encoded).unwrap();
237        let split = encoded.len();
238        encode_audit_log_record(&second, &mut encoded).unwrap();
239        assert_eq!(
240            decode_audit_log_record_at(&encoded, 0).unwrap(),
241            (first, split)
242        );
243        assert_eq!(
244            decode_audit_log_record_at(&encoded, split).unwrap(),
245            (second, encoded.len())
246        );
247        assert!(decode_audit_log_record_at(&encoded[..encoded.len() - 1], split).is_err());
248    }
249
250    #[test]
251    fn audit_record_result_preserves_u64_sequence_identity() {
252        let expected = BACnetAuditLogRecordResult {
253            sequence_number: u64::MAX,
254            record: audit_record(),
255        };
256        let mut encoded = BytesMut::new();
257        encode_audit_log_record_result(&expected, &mut encoded).unwrap();
258        let (decoded, end) = decode_audit_log_record_result_at(&encoded, 0).unwrap();
259        assert_eq!(end, encoded.len());
260        assert_eq!(decoded, expected);
261    }
262}