Skip to main content

bacnet_types/constructed/
audit.rs

1//! Dependency-neutral Audit service and Audit Log base models.
2
3#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5
6use crate::bitstring::{AuditOperationFlags, LogStatus};
7use crate::enums::{
8    AuditOperation, BACnetSuccessFilter, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier,
9};
10use crate::primitives::{BACnetTimeStamp, Date, ObjectIdentifier, PropertyValue, Time};
11
12use super::{BACnetAddress, BACnetRecipient};
13
14/// `BACnetObjectSelector` (Clause 21), used by Audit Reporter Monitored_Objects.
15///
16/// This CHOICE uses application tags, not context tags. NULL is an ignored
17/// entry, not a wildcard; ObjectType includes the repository's extensible values.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum BACnetObjectSelector {
20    /// Ignore this array entry.
21    None,
22    /// Select exactly one object.
23    Object(ObjectIdentifier),
24    /// Select all objects of this type.
25    ObjectType(ObjectType),
26}
27
28impl BACnetObjectSelector {
29    /// Project to the primitive application-tagged codec representation.
30    pub fn encode_property_value(&self) -> PropertyValue {
31        match self {
32            Self::None => PropertyValue::Null,
33            Self::Object(object) => PropertyValue::ObjectIdentifier(*object),
34            Self::ObjectType(kind) => PropertyValue::Enumerated(kind.to_raw()),
35        }
36    }
37
38    /// Decode one application value, rejecting alternatives outside the CHOICE.
39    pub fn decode_property_value(value: &PropertyValue) -> Result<Self, crate::error::Error> {
40        match value {
41            PropertyValue::Null => Ok(Self::None),
42            PropertyValue::ObjectIdentifier(object) => Ok(Self::Object(*object)),
43            PropertyValue::Enumerated(kind) => Ok(Self::ObjectType(ObjectType::from_raw(*kind))),
44            _ => Err(crate::error::Error::Encoding(
45                "BACnetObjectSelector requires NULL, ObjectIdentifier, or Enumerated".into(),
46            )),
47        }
48    }
49}
50
51#[cfg(test)]
52mod selector_tests {
53    use super::*;
54
55    #[test]
56    fn audit_object_selector_preserves_choice_and_extensible_type() {
57        let object = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 42).unwrap();
58        for (selector, value) in [
59            (BACnetObjectSelector::None, PropertyValue::Null),
60            (
61                BACnetObjectSelector::Object(object),
62                PropertyValue::ObjectIdentifier(object),
63            ),
64            (
65                BACnetObjectSelector::ObjectType(ObjectType::BINARY_VALUE),
66                PropertyValue::Enumerated(5),
67            ),
68            (
69                BACnetObjectSelector::ObjectType(ObjectType::from_raw(128)),
70                PropertyValue::Enumerated(128),
71            ),
72        ] {
73            assert_eq!(selector.encode_property_value(), value);
74            assert_eq!(
75                BACnetObjectSelector::decode_property_value(&value).unwrap(),
76                selector
77            );
78        }
79        for invalid in [
80            PropertyValue::Unsigned(5),
81            PropertyValue::Boolean(false),
82            PropertyValue::List(vec![]),
83        ] {
84            assert!(matches!(
85                BACnetObjectSelector::decode_property_value(&invalid),
86                Err(crate::error::Error::Encoding(_))
87            ));
88        }
89    }
90}
91
92/// One audit operation record (`BACnetAuditNotification`, Clause 21).
93#[derive(Debug, Clone, PartialEq)]
94pub struct BACnetAuditNotification {
95    /// Optional timestamp at the source device.
96    pub source_timestamp: Option<BACnetTimeStamp>,
97    /// Optional timestamp at the target device.
98    pub target_timestamp: Option<BACnetTimeStamp>,
99    /// Device or address that originated the operation.
100    pub source_device: BACnetRecipient,
101    /// Optional source object.
102    pub source_object: Option<ObjectIdentifier>,
103    /// Operation being audited.
104    pub operation: AuditOperation,
105    /// Optional source-side comment.
106    pub source_comment: Option<String>,
107    /// Optional target-side comment.
108    pub target_comment: Option<String>,
109    /// Optional originating service invoke identifier.
110    pub invoke_id: Option<u8>,
111    /// Optional source user identifier.
112    pub source_user_id: Option<u16>,
113    /// Optional source user role.
114    pub source_user_role: Option<u8>,
115    /// Device or address targeted by the operation.
116    pub target_device: BACnetRecipient,
117    /// Optional target object.
118    pub target_object: Option<ObjectIdentifier>,
119    /// Optional target property reference.
120    pub target_property: Option<AuditPropertyReference>,
121    /// Command priority, constrained to `1..=16` when present.
122    pub target_priority: Option<u8>,
123    /// Raw, structurally validated `ABSTRACT-SYNTAX.&Type` encoding.
124    pub target_value: Option<Vec<u8>>,
125    /// Raw, structurally validated `ABSTRACT-SYNTAX.&Type` encoding.
126    pub current_value: Option<Vec<u8>>,
127    /// Optional BACnet error produced by the audited operation.
128    pub result: Option<(ErrorClass, ErrorCode)>,
129}
130
131/// One result returned by AuditLogQuery.
132#[derive(Debug, Clone, PartialEq)]
133pub struct BACnetAuditLogRecordResult {
134    /// Stable sequence identity assigned when the record was generated.
135    pub sequence_number: u64,
136    /// Typed Audit Log record.
137    pub record: BACnetAuditLogRecord,
138}
139
140/// One Audit Log record, distinct from Trend/Event Log record models.
141#[derive(Debug, Clone, PartialEq)]
142pub struct BACnetAuditLogRecord {
143    /// BACnetDateTime encoded as application Date followed by application Time.
144    pub timestamp: (Date, Time),
145    /// Audit-specific record datum.
146    pub datum: BACnetAuditLogDatum,
147}
148
149/// Audit-specific `BACnetAuditLogRecord` datum CHOICE.
150#[derive(Debug, Clone, PartialEq)]
151#[allow(clippy::large_enum_variant)] // Preserve the existing direct public variant shape.
152pub enum BACnetAuditLogDatum {
153    /// A status change of the log itself.
154    LogStatus(LogStatus),
155    /// A bare BACnetAuditNotification wrapped by choice tag `[1]`.
156    AuditNotification(BACnetAuditNotification),
157    /// Clock adjustment in seconds, encoded as a four-octet REAL under `[2]`.
158    TimeChange(f32),
159}
160
161/// Typed `BACnetAuditLogQueryParameters` CHOICE.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum BACnetAuditLogQueryParameters {
164    /// Match operations by target attributes.
165    ByTarget {
166        /// Required target device identity.
167        target_device_identifier: ObjectIdentifier,
168        /// Optional target device network address.
169        target_device_address: Option<BACnetAddress>,
170        /// Optional target object identity.
171        target_object_identifier: Option<ObjectIdentifier>,
172        /// Optional target property identity.
173        target_property_identifier: Option<PropertyIdentifier>,
174        /// Optional target property array index.
175        target_array_index: Option<u64>,
176        /// Command priority filter, constrained to `1..=16` when present.
177        target_priority: Option<u8>,
178        /// Optional operation bit filter.
179        operations: Option<AuditOperationFlags>,
180        /// Which operation outcomes match: all, successes-only, or
181        /// failures-only (`BACnetSuccessFilter`, Clause 21.6 tags \[7\]/\[4\]).
182        successful_actions_only: BACnetSuccessFilter,
183    },
184    /// Match operations by source attributes.
185    BySource {
186        /// Required source device identity.
187        source_device_identifier: ObjectIdentifier,
188        /// Optional source device network address.
189        source_device_address: Option<BACnetAddress>,
190        /// Optional source object identity.
191        source_object_identifier: Option<ObjectIdentifier>,
192        /// Optional operation bit filter.
193        operations: Option<AuditOperationFlags>,
194        /// Which operation outcomes match: all, successes-only, or
195        /// failures-only (`BACnetSuccessFilter`, Clause 21.6 tags \[7\]/\[4\]).
196        successful_actions_only: BACnetSuccessFilter,
197    },
198}
199
200/// Audit-local wire-equivalent of `BACnetPropertyReference`.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct AuditPropertyReference {
203    /// Property selected by the Audit notification.
204    pub property_identifier: PropertyIdentifier,
205    /// Optional array index, within the primitive layer's `u64` domain.
206    pub property_array_index: Option<u64>,
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn audit_record_models_are_dependency_neutral_and_exactly_typed() {
215        let record = BACnetAuditLogRecordResult {
216            sequence_number: u64::MAX,
217            record: BACnetAuditLogRecord {
218                timestamp: (
219                    Date {
220                        year: 124,
221                        month: 2,
222                        day: 29,
223                        day_of_week: 4,
224                    },
225                    Time {
226                        hour: 12,
227                        minute: 0,
228                        second: 0,
229                        hundredths: 0,
230                    },
231                ),
232                datum: BACnetAuditLogDatum::TimeChange(-1.5),
233            },
234        };
235        assert_eq!(record.clone(), record);
236    }
237
238    #[test]
239    #[allow(deprecated)]
240    fn legacy_bool_helper_maps_old_meaning_to_named_filter() {
241        assert_eq!(
242            BACnetSuccessFilter::from_legacy_bool(true),
243            BACnetSuccessFilter::SUCCESSES_ONLY
244        );
245        assert_eq!(
246            BACnetSuccessFilter::from_legacy_bool(false),
247            BACnetSuccessFilter::ALL
248        );
249    }
250}