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
15pub 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
27pub 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
55pub 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
78pub 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
85pub 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(|_| {
109 Error::decoding(
110 0,
111 "BACnetAuditLogRecord timestamp contains an invalid Date or Time component",
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::encode("BACnetAuditLogRecord timestamp contains an invalid Date or Time component"));
165 }
166 Ok(())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use bacnet_types::bitstring::LogStatus;
173
174 fn audit_record() -> BACnetAuditLogRecord {
175 BACnetAuditLogRecord {
176 timestamp: (
177 Date {
178 year: 124,
179 month: 2,
180 day: 29,
181 day_of_week: 4,
182 },
183 Time {
184 hour: 12,
185 minute: 34,
186 second: 56,
187 hundredths: 78,
188 },
189 ),
190 datum: BACnetAuditLogDatum::LogStatus(LogStatus::BUFFER_PURGED),
191 }
192 }
193
194 #[test]
197 fn audit_record_log_status_has_bit0_first_wire_bytes() {
198 for (status, octet) in [
199 (LogStatus::LOG_DISABLED, 0x80),
200 (LogStatus::LOG_DISABLED | LogStatus::BUFFER_PURGED, 0xC0),
201 (LogStatus::LOG_INTERRUPTED, 0x20),
202 ] {
203 let record = BACnetAuditLogRecord {
204 datum: BACnetAuditLogDatum::LogStatus(status),
205 ..audit_record()
206 };
207 let mut encoded = BytesMut::new();
208 encode_audit_log_record(&record, &mut encoded).unwrap();
209 assert_eq!(&encoded[12..], &[0x1E, 0x0A, 0x05, octet, 0x1F], "{status}");
211 assert_eq!(decode_audit_log_record(&encoded).unwrap(), record);
212 }
213 }
214
215 #[test]
216 fn audit_record_shared_codec_round_trips_and_rejects_trailing_data() {
217 let expected = audit_record();
218 let mut encoded = BytesMut::new();
219 encode_audit_log_record(&expected, &mut encoded).unwrap();
220 assert_eq!(decode_audit_log_record(&encoded).unwrap(), expected);
221
222 encoded.extend_from_slice(&[0]);
223 assert!(decode_audit_log_record(&encoded).is_err());
224 }
225
226 #[test]
227 fn consecutive_audit_records_decode_by_returned_offset() {
228 let first = audit_record();
229 let second = BACnetAuditLogRecord {
230 datum: BACnetAuditLogDatum::TimeChange(-1.5),
231 ..audit_record()
232 };
233 let mut encoded = BytesMut::new();
234 encode_audit_log_record(&first, &mut encoded).unwrap();
235 let split = encoded.len();
236 encode_audit_log_record(&second, &mut encoded).unwrap();
237 assert_eq!(
238 decode_audit_log_record_at(&encoded, 0).unwrap(),
239 (first, split)
240 );
241 assert_eq!(
242 decode_audit_log_record_at(&encoded, split).unwrap(),
243 (second, encoded.len())
244 );
245 assert!(decode_audit_log_record_at(&encoded[..encoded.len() - 1], split).is_err());
246 }
247
248 #[test]
249 fn audit_record_result_preserves_u64_sequence_identity() {
250 let expected = BACnetAuditLogRecordResult {
251 sequence_number: u64::MAX,
252 record: audit_record(),
253 };
254 let mut encoded = BytesMut::new();
255 encode_audit_log_record_result(&expected, &mut encoded).unwrap();
256 let (decoded, end) = decode_audit_log_record_result_at(&encoded, 0).unwrap();
257 assert_eq!(end, encoded.len());
258 assert_eq!(decoded, expected);
259 }
260}