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
10pub 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
22pub 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
57pub 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
87pub 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}