Skip to main content

bacnet_services/
audit.rs

1//! Audit notification and query wire models.
2//!
3//! These codecs follow the formal Clause 21 field and tag productions in
4//! ASHRAE 135-2020 within the library's `u64` Unsigned implementation limit.
5//! Clause 13.19 conflicts with them by describing an `Unsigned64` start
6//! sequence and a three-state success filter. This model intentionally uses
7//! Clause 21's `Unsigned32` and strict mandatory Boolean forms.
8
9pub use bacnet_types::constructed::{
10    AuditPropertyReference, BACnetAuditLogDatum, BACnetAuditLogQueryParameters,
11    BACnetAuditLogRecord, BACnetAuditLogRecordResult, BACnetAuditNotification,
12};
13use bacnet_types::error::Error;
14use bacnet_types::primitives::ObjectIdentifier;
15use bytes::BytesMut;
16
17use crate::common::PropertyReference;
18
19#[path = "audit/notification_codec.rs"]
20mod notification_codec;
21#[path = "audit/query_ack_codec.rs"]
22mod query_ack_codec;
23#[path = "audit/query_codec.rs"]
24mod query_codec;
25
26/// Parameters shared by confirmed and unconfirmed AuditNotification.
27#[derive(Debug, Clone, PartialEq)]
28pub struct AuditNotificationRequest {
29    /// One or more notifications, encoded under context tag `[0]`.
30    pub notifications: Vec<BACnetAuditNotification>,
31}
32
33/// AuditLogQuery-ACK service parameters.
34///
35/// This wire model does not perform storage queries or infer record ordering,
36/// continuity, filtering, or the truth of `no_more_items`.
37#[derive(Debug, Clone, PartialEq)]
38pub struct AuditLogQueryAck {
39    pub audit_log: ObjectIdentifier,
40    /// Zero or more adjacent record results encoded under context tag `[1]`.
41    pub records: Vec<BACnetAuditLogRecordResult>,
42    pub no_more_items: bool,
43}
44
45/// Audit-local wire-equivalent of `BACnetPropertyReference`.
46///
47/// The shared service [`PropertyReference`] predates these codecs and narrows
48/// the optional array index to `u32`. Clause 21 defines it as unconstrained
49/// Unsigned, so Audit preserves every value supported by the primitive layer.
50impl From<PropertyReference> for AuditPropertyReference {
51    fn from(value: PropertyReference) -> Self {
52        Self {
53            property_identifier: value.property_identifier,
54            property_array_index: value.property_array_index.map(u64::from),
55        }
56    }
57}
58
59impl TryFrom<AuditPropertyReference> for PropertyReference {
60    type Error = Error;
61
62    fn try_from(value: AuditPropertyReference) -> Result<Self, Self::Error> {
63        Ok(Self {
64            property_identifier: value.property_identifier,
65            property_array_index: value
66                .property_array_index
67                .map(u32::try_from)
68                .transpose()
69                .map_err(|_| {
70                    Error::OutOfRange(
71                        "Audit property-array-index exceeds shared PropertyReference u32 limit"
72                            .into(),
73                    )
74                })?,
75        })
76    }
77}
78
79/// AuditLogQuery-Request service parameters.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct AuditLogQueryRequest {
82    pub audit_log: ObjectIdentifier,
83    pub query_parameters: BACnetAuditLogQueryParameters,
84    /// Clause 21 constrains this field to `Unsigned32`.
85    pub start_at_sequence_number: Option<u32>,
86    pub requested_count: u16,
87}
88
89fn decode_canonical_unsigned(data: &[u8], offset: usize, field: &str) -> Result<u64, Error> {
90    if data.len() > 1 && data.first() == Some(&0) {
91        return Err(Error::decoding(
92            offset,
93            format!("{field} must use the shortest Unsigned/Enumerated encoding"),
94        ));
95    }
96    bacnet_encoding::primitives::decode_unsigned(data)
97}
98
99impl AuditNotificationRequest {
100    /// Encode after validation, leaving `buf` unchanged on failure.
101    pub fn try_encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
102        notification_codec::encode(self, buf)
103    }
104
105    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
106        self.try_encode(buf)
107    }
108
109    /// Decode a complete payload; trailing bytes are rejected.
110    pub fn decode(data: &[u8]) -> Result<Self, Error> {
111        notification_codec::decode(data)
112    }
113}
114
115impl AuditLogQueryRequest {
116    /// Encode after validation, leaving `buf` unchanged on failure.
117    pub fn try_encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
118        query_codec::encode(self, buf)
119    }
120
121    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
122        self.try_encode(buf)
123    }
124
125    /// Decode a complete payload; trailing bytes are rejected.
126    pub fn decode(data: &[u8]) -> Result<Self, Error> {
127        query_codec::decode(data)
128    }
129}
130
131impl AuditLogQueryAck {
132    /// Encode after validation, leaving `buf` unchanged on failure.
133    pub fn try_encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
134        query_ack_codec::encode(self, buf)
135    }
136
137    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
138        self.try_encode(buf)
139    }
140
141    /// Decode a complete payload; trailing bytes are rejected.
142    pub fn decode(data: &[u8]) -> Result<Self, Error> {
143        query_ack_codec::decode(data)
144    }
145}
146
147#[cfg(test)]
148#[path = "audit/tests.rs"]
149mod tests;
150
151#[cfg(test)]
152#[path = "audit/malformed_tests.rs"]
153mod malformed_tests;