Skip to main content

bacnet_encoding/constructed/
audit_notification.rs

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
9/// Encode one bare `BACnetAuditNotification` field sequence.
10pub 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) = &notification.source_timestamp {
33        primitives::encode_timestamp(buf, 0, timestamp)?;
34    }
35    if let Some(timestamp) = &notification.target_timestamp {
36        primitives::encode_timestamp(buf, 1, timestamp)?;
37    }
38
39    encode_wrapped_recipient(buf, 2, &notification.source_device);
40    if let Some(object) = &notification.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) = &notification.source_comment {
45        primitives::encode_ctx_character_string(buf, 5, comment)?;
46    }
47    if let Some(comment) = &notification.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, &notification.target_device);
61    if let Some(object) = &notification.target_object {
62        primitives::encode_ctx_object_id(buf, 11, object);
63    }
64    if let Some(property) = &notification.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) = &notification.target_value {
73        encode_raw_value(buf, 14, value);
74    }
75    if let Some(value) = &notification.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
116/// Decode one bare `BACnetAuditNotification` starting at `offset`.
117pub 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, &timestamp)?;
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}