1use super::{decode_recipient, encode_recipient, validate_tlv_sequence};
2use crate::{primitives, tags};
3use bacnet_types::constructed::{AuditPropertyReference, BACnetAuditNotification};
4use bacnet_types::enums::{AuditOperation, ErrorClass, ErrorCode};
5use bacnet_types::error::Error;
6use bacnet_types::primitives::ObjectIdentifier;
7use bytes::BytesMut;
8
9pub fn encode_audit_notification(
11 notification: &BACnetAuditNotification,
12 buf: &mut BytesMut,
13) -> Result<(), Error> {
14 let operation = notification.operation.to_raw();
15 if !valid_operation(operation) {
16 return Err(Error::OutOfRange(format!(
17 "AuditNotification operation {operation} is reserved"
18 )));
19 }
20 if notification
21 .target_priority
22 .is_some_and(|priority| !(1..=16).contains(&priority))
23 {
24 return Err(Error::OutOfRange(format!(
25 "AuditNotification target priority {} is outside 1..=16",
26 notification.target_priority.unwrap()
27 )));
28 }
29 validate_raw_value(notification.target_value.as_deref(), "target-value")?;
30 validate_raw_value(notification.current_value.as_deref(), "current-value")?;
31
32 if let Some(timestamp) = ¬ification.source_timestamp {
33 primitives::encode_timestamp(buf, 0, timestamp)?;
34 }
35 if let Some(timestamp) = ¬ification.target_timestamp {
36 primitives::encode_timestamp(buf, 1, timestamp)?;
37 }
38
39 encode_wrapped_recipient(buf, 2, ¬ification.source_device);
40 if let Some(object) = ¬ification.source_object {
41 primitives::encode_ctx_object_id(buf, 3, object);
42 }
43 primitives::encode_ctx_enumerated(buf, 4, operation);
44 if let Some(comment) = ¬ification.source_comment {
45 primitives::encode_ctx_character_string(buf, 5, comment)?;
46 }
47 if let Some(comment) = ¬ification.target_comment {
48 primitives::encode_ctx_character_string(buf, 6, comment)?;
49 }
50 if let Some(invoke_id) = notification.invoke_id {
51 primitives::encode_ctx_unsigned(buf, 7, u64::from(invoke_id));
52 }
53 if let Some(user_id) = notification.source_user_id {
54 primitives::encode_ctx_unsigned(buf, 8, u64::from(user_id));
55 }
56 if let Some(user_role) = notification.source_user_role {
57 primitives::encode_ctx_unsigned(buf, 9, u64::from(user_role));
58 }
59
60 encode_wrapped_recipient(buf, 10, ¬ification.target_device);
61 if let Some(object) = ¬ification.target_object {
62 primitives::encode_ctx_object_id(buf, 11, object);
63 }
64 if let Some(property) = ¬ification.target_property {
65 tags::encode_opening_tag(buf, 12);
66 encode_property_reference(buf, property);
67 tags::encode_closing_tag(buf, 12);
68 }
69 if let Some(priority) = notification.target_priority {
70 primitives::encode_ctx_unsigned(buf, 13, u64::from(priority));
71 }
72 if let Some(value) = ¬ification.target_value {
73 encode_raw_value(buf, 14, value);
74 }
75 if let Some(value) = ¬ification.current_value {
76 encode_raw_value(buf, 15, value);
77 }
78 if let Some((class, code)) = notification.result {
79 tags::encode_opening_tag(buf, 16);
80 primitives::encode_app_enumerated(buf, u32::from(class.to_raw()));
81 primitives::encode_app_enumerated(buf, u32::from(code.to_raw()));
82 tags::encode_closing_tag(buf, 16);
83 }
84 Ok(())
85}
86
87fn encode_wrapped_recipient(
88 buf: &mut BytesMut,
89 field_tag: u8,
90 recipient: &bacnet_types::constructed::BACnetRecipient,
91) {
92 tags::encode_opening_tag(buf, field_tag);
93 encode_recipient(buf, recipient);
94 tags::encode_closing_tag(buf, field_tag);
95}
96
97fn encode_raw_value(buf: &mut BytesMut, field_tag: u8, value: &[u8]) {
98 tags::encode_opening_tag(buf, field_tag);
99 buf.extend_from_slice(value);
100 tags::encode_closing_tag(buf, field_tag);
101}
102
103fn validate_raw_value(value: Option<&[u8]>, field: &str) -> Result<(), Error> {
104 let Some(value) = value else {
105 return Ok(());
106 };
107 if value.is_empty() {
108 return Err(Error::Encoding(format!(
109 "AuditNotification {field} must contain a BACnet value"
110 )));
111 }
112 validate_tlv_sequence(value, &format!("AuditNotification {field}"))
113 .map_err(|error| Error::Encoding(error.to_string()))
114}
115
116pub fn decode_audit_notification_at(
118 data: &[u8],
119 mut offset: usize,
120) -> Result<(BACnetAuditNotification, usize), Error> {
121 let source_timestamp = if next_is_opening(data, offset, 0)? {
122 let (timestamp, next) = decode_canonical_timestamp(data, offset, 0, "source-timestamp")?;
123 offset = next;
124 Some(timestamp)
125 } else {
126 None
127 };
128 let target_timestamp = if next_is_opening(data, offset, 1)? {
129 let (timestamp, next) = decode_canonical_timestamp(data, offset, 1, "target-timestamp")?;
130 offset = next;
131 Some(timestamp)
132 } else {
133 None
134 };
135
136 let (source_device, next) = decode_wrapped_recipient(data, offset, 2, "source-device")?;
137 offset = next;
138
139 let source_object = if next_is_context(data, offset, 3)? {
140 let (object, next) = decode_object(data, offset, 3, "source-object")?;
141 offset = next;
142 Some(object)
143 } else {
144 None
145 };
146
147 let operation_offset = offset;
148 let (operation_raw, next) = decode_context_u32(data, offset, 4, "operation")?;
149 if !valid_operation(operation_raw) {
150 return Err(Error::decoding(
151 operation_offset,
152 format!("AuditNotification operation {operation_raw} is reserved"),
153 ));
154 }
155 let operation = AuditOperation::from_raw(operation_raw);
156 offset = next;
157
158 let source_comment = if next_is_context(data, offset, 5)? {
159 let (comment, next) = decode_string(data, offset, 5, "source-comment")?;
160 offset = next;
161 Some(comment)
162 } else {
163 None
164 };
165 let target_comment = if next_is_context(data, offset, 6)? {
166 let (comment, next) = decode_string(data, offset, 6, "target-comment")?;
167 offset = next;
168 Some(comment)
169 } else {
170 None
171 };
172 let invoke_id = if next_is_context(data, offset, 7)? {
173 let (value, next) = decode_context_u8(data, offset, 7, "invoke-id")?;
174 offset = next;
175 Some(value)
176 } else {
177 None
178 };
179 let source_user_id = if next_is_context(data, offset, 8)? {
180 let (value, next) = decode_context_u16(data, offset, 8, "source-user-id")?;
181 offset = next;
182 Some(value)
183 } else {
184 None
185 };
186 let source_user_role = if next_is_context(data, offset, 9)? {
187 let (value, next) = decode_context_u8(data, offset, 9, "source-user-role")?;
188 offset = next;
189 Some(value)
190 } else {
191 None
192 };
193
194 let (target_device, next) = decode_wrapped_recipient(data, offset, 10, "target-device")?;
195 offset = next;
196
197 let target_object = if next_is_context(data, offset, 11)? {
198 let (object, next) = decode_object(data, offset, 11, "target-object")?;
199 offset = next;
200 Some(object)
201 } else {
202 None
203 };
204 let target_property = if next_is_opening(data, offset, 12)? {
205 let (body, next) = decode_constructed_body(data, offset, 12, "target-property")?;
206 let (property, property_end) = decode_property_reference(body)?;
207 if property_end != body.len() {
208 return Err(Error::decoding(
209 offset,
210 "AuditNotification target-property has trailing fields",
211 ));
212 }
213 let mut canonical = BytesMut::new();
214 encode_property_reference(&mut canonical, &property);
215 if canonical.as_ref() != body {
216 return Err(Error::decoding(
217 offset,
218 "AuditNotification target-property is not canonically encoded",
219 ));
220 }
221 offset = next;
222 Some(property)
223 } else {
224 None
225 };
226 let target_priority = if next_is_context(data, offset, 13)? {
227 let priority_offset = offset;
228 let (priority, next) = decode_context_u8(data, offset, 13, "target-priority")?;
229 if !(1..=16).contains(&priority) {
230 return Err(Error::decoding(
231 priority_offset,
232 format!("AuditNotification target-priority {priority} is outside 1..=16"),
233 ));
234 }
235 offset = next;
236 Some(priority)
237 } else {
238 None
239 };
240 let target_value = if next_is_opening(data, offset, 14)? {
241 let (value, next) = decode_raw_value(data, offset, 14, "target-value")?;
242 offset = next;
243 Some(value)
244 } else {
245 None
246 };
247 let current_value = if next_is_opening(data, offset, 15)? {
248 let (value, next) = decode_raw_value(data, offset, 15, "current-value")?;
249 offset = next;
250 Some(value)
251 } else {
252 None
253 };
254 let result = if next_is_opening(data, offset, 16)? {
255 let (result, next) = decode_error(data, offset)?;
256 offset = next;
257 Some(result)
258 } else {
259 None
260 };
261
262 Ok((
263 BACnetAuditNotification {
264 source_timestamp,
265 target_timestamp,
266 source_device,
267 source_object,
268 operation,
269 source_comment,
270 target_comment,
271 invoke_id,
272 source_user_id,
273 source_user_role,
274 target_device,
275 target_object,
276 target_property,
277 target_priority,
278 target_value,
279 current_value,
280 result,
281 },
282 offset,
283 ))
284}
285
286fn valid_operation(value: u32) -> bool {
287 (0..=15).contains(&value) || (32..=63).contains(&value)
288}
289
290fn encode_property_reference(buf: &mut BytesMut, property: &AuditPropertyReference) {
291 primitives::encode_ctx_enumerated(buf, 0, property.property_identifier.to_raw());
292 if let Some(index) = property.property_array_index {
293 primitives::encode_ctx_unsigned(buf, 1, index);
294 }
295}
296
297fn decode_property_reference(data: &[u8]) -> Result<(AuditPropertyReference, usize), Error> {
298 let (property, mut offset) = decode_context_u32(data, 0, 0, "target-property identifier")?;
299 let property_array_index = if next_is_context(data, offset, 1)? {
300 let (index, next) = decode_context_u64(data, offset, 1, "target-property array-index")?;
301 offset = next;
302 Some(index)
303 } else {
304 None
305 };
306 Ok((
307 AuditPropertyReference {
308 property_identifier: bacnet_types::enums::PropertyIdentifier::from_raw(property),
309 property_array_index,
310 },
311 offset,
312 ))
313}
314
315fn next_is_context(data: &[u8], offset: usize, number: u8) -> Result<bool, Error> {
316 if offset == data.len() {
317 return Ok(false);
318 }
319 let (tag, _) = tags::decode_tag(data, offset)?;
320 Ok(tag.is_context(number))
321}
322
323fn next_is_opening(data: &[u8], offset: usize, number: u8) -> Result<bool, Error> {
324 if offset == data.len() {
325 return Ok(false);
326 }
327 let (tag, _) = tags::decode_tag(data, offset)?;
328 Ok(tag.is_opening_tag(number))
329}
330
331fn decode_context_u32(
332 data: &[u8],
333 offset: usize,
334 tag: u8,
335 field: &str,
336) -> Result<(u32, usize), Error> {
337 let (contents, next) =
338 decode_context(data, offset, tag, &format!("AuditNotification {field}"))?;
339 let raw = decode_canonical_unsigned(contents, offset, field)?;
340 let value = u32::try_from(raw)
341 .map_err(|_| Error::decoding(offset, format!("AuditNotification {field} exceeds u32")))?;
342 Ok((value, next))
343}
344
345fn decode_context_u64(
346 data: &[u8],
347 offset: usize,
348 tag: u8,
349 field: &str,
350) -> Result<(u64, usize), Error> {
351 let (contents, next) =
352 decode_context(data, offset, tag, &format!("AuditNotification {field}"))?;
353 Ok((decode_canonical_unsigned(contents, offset, field)?, next))
354}
355
356fn decode_context_u16(
357 data: &[u8],
358 offset: usize,
359 tag: u8,
360 field: &str,
361) -> Result<(u16, usize), Error> {
362 let (contents, next) =
363 decode_context(data, offset, tag, &format!("AuditNotification {field}"))?;
364 let raw = decode_canonical_unsigned(contents, offset, field)?;
365 let value = u16::try_from(raw)
366 .map_err(|_| Error::decoding(offset, format!("AuditNotification {field} exceeds u16")))?;
367 Ok((value, next))
368}
369
370fn decode_context_u8(
371 data: &[u8],
372 offset: usize,
373 tag: u8,
374 field: &str,
375) -> Result<(u8, usize), Error> {
376 let (contents, next) =
377 decode_context(data, offset, tag, &format!("AuditNotification {field}"))?;
378 let raw = decode_canonical_unsigned(contents, offset, field)?;
379 let value = u8::try_from(raw)
380 .map_err(|_| Error::decoding(offset, format!("AuditNotification {field} exceeds u8")))?;
381 Ok((value, next))
382}
383
384fn decode_object(
385 data: &[u8],
386 offset: usize,
387 tag: u8,
388 field: &str,
389) -> Result<(ObjectIdentifier, usize), Error> {
390 let (contents, next) =
391 decode_context(data, offset, tag, &format!("AuditNotification {field}"))?;
392 Ok((ObjectIdentifier::decode(contents)?, next))
393}
394
395fn decode_string(
396 data: &[u8],
397 offset: usize,
398 tag: u8,
399 field: &str,
400) -> Result<(String, usize), Error> {
401 let (contents, next) =
402 decode_context(data, offset, tag, &format!("AuditNotification {field}"))?;
403 Ok((primitives::decode_character_string(contents)?, next))
404}
405
406fn decode_constructed_body<'a>(
407 data: &'a [u8],
408 offset: usize,
409 tag_number: u8,
410 field: &str,
411) -> Result<(&'a [u8], usize), Error> {
412 let (opening, body_start) = tags::decode_tag(data, offset)?;
413 if !opening.is_opening_tag(tag_number) {
414 return Err(Error::decoding(
415 offset,
416 format!("AuditNotification {field} expected opening tag [{tag_number}]"),
417 ));
418 }
419 tags::extract_context_value(data, body_start, tag_number)
420}
421
422fn decode_wrapped_recipient(
423 data: &[u8],
424 offset: usize,
425 tag_number: u8,
426 field: &str,
427) -> Result<(bacnet_types::constructed::BACnetRecipient, usize), Error> {
428 let (body, next) = decode_constructed_body(data, offset, tag_number, field)?;
429 let (recipient, recipient_end) = decode_recipient(body, 0)?;
430 if recipient_end != body.len() {
431 return Err(Error::decoding(
432 offset,
433 format!("AuditNotification {field} has trailing fields"),
434 ));
435 }
436 let mut canonical = BytesMut::new();
437 encode_recipient(&mut canonical, &recipient);
438 if canonical.as_ref() != body {
439 return Err(Error::decoding(
440 offset,
441 format!("AuditNotification {field} is not canonically encoded"),
442 ));
443 }
444 Ok((recipient, next))
445}
446
447fn decode_canonical_timestamp(
448 data: &[u8],
449 offset: usize,
450 tag_number: u8,
451 field: &str,
452) -> Result<(bacnet_types::primitives::BACnetTimeStamp, usize), Error> {
453 let (timestamp, next) = primitives::decode_timestamp(data, offset, tag_number)?;
454 let mut canonical = BytesMut::new();
455 primitives::encode_timestamp(&mut canonical, tag_number, ×tamp)?;
456 if canonical.as_ref() != &data[offset..next] {
457 return Err(Error::decoding(
458 offset,
459 format!("AuditNotification {field} is not canonically encoded"),
460 ));
461 }
462 Ok((timestamp, next))
463}
464
465fn decode_raw_value(
466 data: &[u8],
467 offset: usize,
468 tag_number: u8,
469 field: &str,
470) -> Result<(Vec<u8>, usize), Error> {
471 let (value, next) = decode_constructed_body(data, offset, tag_number, field)?;
472 if value.is_empty() {
473 return Err(Error::decoding(
474 offset,
475 format!("AuditNotification {field} must not be empty"),
476 ));
477 }
478 validate_tlv_sequence(value, &format!("AuditNotification {field}"))?;
479 Ok((value.to_vec(), next))
480}
481
482fn decode_error(data: &[u8], offset: usize) -> Result<((ErrorClass, ErrorCode), usize), Error> {
483 let (body, next) = decode_constructed_body(data, offset, 16, "result")?;
484 let (class, body_offset) = decode_app_enumerated_u16(body, 0, "result error-class")?;
485 let (code, body_end) = decode_app_enumerated_u16(body, body_offset, "result error-code")?;
486 if body_end != body.len() {
487 return Err(Error::decoding(
488 offset,
489 "AuditNotification result has trailing fields",
490 ));
491 }
492 Ok((
493 (ErrorClass::from_raw(class), ErrorCode::from_raw(code)),
494 next,
495 ))
496}
497
498fn decode_app_enumerated_u16(
499 data: &[u8],
500 offset: usize,
501 field: &str,
502) -> Result<(u16, usize), Error> {
503 let (tag, contents_start) = tags::decode_tag(data, offset)?;
504 if tag.class != tags::TagClass::Application || tag.number != tags::app_tag::ENUMERATED {
505 return Err(Error::decoding(
506 offset,
507 format!("AuditNotification {field} expected application Enumerated"),
508 ));
509 }
510 let end = contents_start
511 .checked_add(tag.length as usize)
512 .ok_or_else(|| {
513 Error::decoding(contents_start, "AuditNotification result length overflow")
514 })?;
515 if end > data.len() {
516 return Err(Error::decoding(
517 contents_start,
518 format!("AuditNotification {field} is truncated"),
519 ));
520 }
521 let raw = decode_canonical_unsigned(&data[contents_start..end], offset, field)?;
522 let value = u16::try_from(raw)
523 .map_err(|_| Error::decoding(offset, format!("AuditNotification {field} exceeds u16")))?;
524 Ok((value, end))
525}
526
527fn decode_context<'a>(
528 data: &'a [u8],
529 offset: usize,
530 expected_tag: u8,
531 field: &str,
532) -> Result<(&'a [u8], usize), Error> {
533 let (tag, contents_start) = tags::decode_tag(data, offset)?;
534 if !tag.is_context(expected_tag) {
535 return Err(Error::decoding(
536 offset,
537 format!("{field} expected context tag [{expected_tag}]"),
538 ));
539 }
540 let end = contents_start
541 .checked_add(tag.length as usize)
542 .ok_or_else(|| Error::decoding(contents_start, format!("{field} length overflow")))?;
543 if end > data.len() {
544 return Err(Error::buffer_too_short(end, data.len()));
545 }
546 Ok((&data[contents_start..end], end))
547}
548
549fn decode_canonical_unsigned(data: &[u8], offset: usize, field: &str) -> Result<u64, Error> {
550 if data.is_empty() {
551 return Err(Error::decoding(
552 offset,
553 format!("{field} must contain at least one octet"),
554 ));
555 }
556 if data.len() > 1 && data.first() == Some(&0) {
557 return Err(Error::decoding(
558 offset,
559 format!("{field} must use the shortest Unsigned/Enumerated encoding"),
560 ));
561 }
562 primitives::decode_unsigned(data)
563}