Skip to main content

bacnet_objects/audit/
object_policy.rs

1//! Optional instance-owned overrides for the supported AV/BV Audit profile.
2use crate::{
3    common,
4    property_metadata::{
5        PropertyConformance, PropertyMetadata, PropertyPresenceCondition, PropertyWriteCapability,
6    },
7};
8use bacnet_types::{
9    bitstring::{AuditOperationFlags, BACnetPriorityFilter},
10    enums::{AuditLevel, AuditOperation, ErrorClass, ErrorCode, PropertyIdentifier as P},
11    error::Error,
12    primitives::PropertyValue,
13};
14
15/// Value of a present Audit_Priority_Filter property.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum AuditPriorityPolicy {
18    /// BACnet NULL: inherit the associated Reporter's command-priority filter.
19    Inherit,
20    /// An explicit sixteen-bit filter, including the empty filter.
21    Filter(BACnetPriorityFilter),
22}
23
24/// Independently optional Audit properties owned by an Analog Value or Binary Value.
25///
26/// Provisioning any field opts that instance into the implemented Audit property
27/// subset. Absent fields inherit the associated Reporter. DEFAULT level and a
28/// present NULL priority filter also inherit. AV/BV follow their object-specific
29/// clauses for absent priority inheritance; generic ยง19.6.3 has conflicting wording.
30/// Both supported objects have commandable Present_Value; these properties do not
31/// install a Reporter or enable reporting by themselves.
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
33pub struct ObjectAuditPolicy {
34    /// Absent row or the instance level; DEFAULT inherits.
35    pub level: Option<AuditLevel>,
36    /// Absent row or the instance operation selection, including an empty set.
37    pub operations: Option<AuditOperationFlags>,
38    /// Absent row, present NULL inheritance, or an explicit filter.
39    pub priority_filter: Option<AuditPriorityPolicy>,
40}
41
42impl ObjectAuditPolicy {
43    pub(crate) fn read(
44        &self,
45        property: P,
46        index: Option<u32>,
47    ) -> Option<Result<PropertyValue, Error>> {
48        let value = match property {
49            P::AUDIT_LEVEL => self.level.map(|v| PropertyValue::Enumerated(v.to_raw())),
50            P::AUDITABLE_OPERATIONS => self.operations.map(|v| bits(v.to_bacnet())),
51            P::AUDIT_PRIORITY_FILTER => self.priority_filter.map(|v| match v {
52                AuditPriorityPolicy::Inherit => PropertyValue::Null,
53                AuditPriorityPolicy::Filter(filter) => bits(filter.to_bacnet()),
54            }),
55            _ => return None,
56        };
57        Some(
58            value
59                .ok_or_else(common::unknown_property_error)
60                .and_then(|value| {
61                    if index.is_some() {
62                        Err(common::property_is_not_an_array_error())
63                    } else {
64                        Ok(value)
65                    }
66                }),
67        )
68    }
69
70    pub(crate) fn write(
71        &mut self,
72        property: P,
73        index: Option<u32>,
74        value: &PropertyValue,
75        priority: Option<u8>,
76    ) -> Option<Result<(), Error>> {
77        self.read(property, index).map(|present| {
78            present?;
79            if priority.is_some_and(|v| !(1..=16).contains(&v)) {
80                return Err(Error::Protocol {
81                    class: ErrorClass::SERVICES.to_raw() as u32,
82                    code: ErrorCode::PARAMETER_OUT_OF_RANGE.to_raw() as u32,
83                });
84            }
85            // NULL relinquishes ordinary noncommandable properties without a
86            // change. For the nullable priority filter it is the actual value.
87            if matches!(value, PropertyValue::Null) && property != P::AUDIT_PRIORITY_FILTER {
88                return Ok(());
89            }
90            match (property, value) {
91                (P::AUDIT_LEVEL, PropertyValue::Enumerated(v)) => {
92                    self.level = Some(AuditLevel::from_raw(*v))
93                }
94                (P::AUDITABLE_OPERATIONS, PropertyValue::BitString { unused_bits, data }) => {
95                    self.operations = Some(
96                        AuditOperationFlags::from_bacnet(*unused_bits, data)
97                            .map_err(|_| common::value_out_of_range_error())?,
98                    );
99                }
100                (P::AUDIT_PRIORITY_FILTER, PropertyValue::Null) => {
101                    self.priority_filter = Some(AuditPriorityPolicy::Inherit)
102                }
103                (P::AUDIT_PRIORITY_FILTER, PropertyValue::BitString { unused_bits, data }) => {
104                    self.priority_filter = Some(AuditPriorityPolicy::Filter(
105                        BACnetPriorityFilter::from_bacnet(*unused_bits, data)
106                            .map_err(|_| common::value_out_of_range_error())?,
107                    ));
108                }
109                _ => return Err(common::invalid_data_type_error()),
110            }
111            Ok(())
112        })
113    }
114
115    pub(crate) fn metadata(&self) -> impl Iterator<Item = PropertyMetadata> {
116        [
117            (P::AUDIT_LEVEL, self.level.is_some()),
118            (P::AUDITABLE_OPERATIONS, self.operations.is_some()),
119            (P::AUDIT_PRIORITY_FILTER, self.priority_filter.is_some()),
120        ]
121        .into_iter()
122        .filter(|(_, present)| *present)
123        .map(|(property, _)| {
124            PropertyMetadata::new(
125                property,
126                PropertyConformance::Optional,
127                Some(if property == P::AUDIT_PRIORITY_FILTER {
128                    PropertyPresenceCondition::CommandableAuditReporting
129                } else {
130                    PropertyPresenceCondition::ObjectAuditReporting
131                }),
132                PropertyWriteCapability::Always,
133            )
134        })
135    }
136
137    /// Resolve this AV/BV instance's settings against its associated Reporter.
138    #[doc(hidden)]
139    pub fn effective_internal(
140        self,
141        reporter: &super::AuditReporterConfiguration,
142    ) -> EffectiveAuditPolicy {
143        EffectiveAuditPolicy {
144            reporter_enabled: reporter.audit_level != AuditLevel::NONE,
145            level: self
146                .level
147                .filter(|v| *v != AuditLevel::DEFAULT)
148                .unwrap_or(reporter.audit_level),
149            operations: self.operations.unwrap_or(reporter.auditable_operations),
150            priorities: match self.priority_filter {
151                Some(AuditPriorityPolicy::Filter(value)) => value,
152                _ => reporter.audit_priority_filter,
153            },
154        }
155    }
156}
157
158fn bits((unused_bits, data): (u8, Vec<u8>)) -> PropertyValue {
159    PropertyValue::BitString { unused_bits, data }
160}
161
162/// Immutable per-operation policy snapshot; source reporting does not use it.
163#[doc(hidden)]
164#[derive(Clone, Copy)]
165pub struct EffectiveAuditPolicy {
166    pub reporter_enabled: bool,
167    pub level: AuditLevel,
168    pub operations: AuditOperationFlags,
169    pub priorities: BACnetPriorityFilter,
170}
171impl EffectiveAuditPolicy {
172    pub fn reports(
173        self,
174        operation: AuditOperation,
175        property: Option<P>,
176        priority: Option<u8>,
177    ) -> bool {
178        self.reporter_enabled
179            && self.level != AuditLevel::NONE
180            && self.operations.contains(operation)
181            && !(self.level == AuditLevel::AUDIT_CONFIG && property == Some(P::PRESENT_VALUE))
182            && priority.is_none_or(|v| self.priorities.contains(v))
183    }
184}
185
186#[cfg(test)]
187#[path = "object_policy_tests.rs"]
188mod tests;