use bacnet_encoding::constructed::encode_access_rule;
use bacnet_types::constructed::BACnetAccessRule;
use bacnet_types::enums::{
AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier, ErrorClass, ErrorCode,
};
use super::*;
use crate::durable::DurableWrites;
#[path = "rights_persistence.rs"]
mod persistence;
#[path = "rights_saving.rs"]
mod saving;
pub use persistence::{AccessRightsPersistence, AccessRightsSnapshot, FileAccessRightsPersistence};
pub const MAX_ACCESS_RULES: usize = 1024;
pub struct AccessRightsObject {
oid: ObjectIdentifier,
name: String,
description: String,
global_identifier: u64,
enable: bool,
positive_access_rules: Vec<BACnetAccessRule>,
negative_access_rules: Vec<BACnetAccessRule>,
accompaniment: Option<BACnetDeviceObjectReference>,
status_flags: StatusFlags,
reliability: Reliability,
storage: Option<saving::Storage>,
writes: u64,
written: saving::Written,
}
impl AccessRightsObject {
pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::ACCESS_RIGHTS, instance)?;
Ok(Self {
oid,
name: name.into(),
description: String::new(),
global_identifier: 0,
enable: true,
positive_access_rules: Vec::new(),
negative_access_rules: Vec::new(),
accompaniment: None,
status_flags: StatusFlags::empty(),
reliability: Reliability::NO_FAULT_DETECTED,
storage: None,
writes: 0,
written: saving::Written::default(),
})
}
pub fn set_enable(&mut self, enable: bool) {
if !self.keeps_saved(PropertyIdentifier::LOG_ENABLE) {
self.enable = enable;
}
}
pub fn enable(&self) -> bool {
self.enable
}
pub fn set_positive_access_rules(
&mut self,
rules: impl IntoIterator<Item = BACnetAccessRule>,
) -> Result<(), Error> {
let rules = checked_rules(rules)?;
if !self.keeps_saved(PropertyIdentifier::POSITIVE_ACCESS_RULES) {
self.positive_access_rules = rules;
}
Ok(())
}
pub fn set_negative_access_rules(
&mut self,
rules: impl IntoIterator<Item = BACnetAccessRule>,
) -> Result<(), Error> {
let rules = checked_rules(rules)?;
if !self.keeps_saved(PropertyIdentifier::NEGATIVE_ACCESS_RULES) {
self.negative_access_rules = rules;
}
Ok(())
}
pub fn positive_access_rules(&self) -> &[BACnetAccessRule] {
&self.positive_access_rules
}
pub fn negative_access_rules(&self) -> &[BACnetAccessRule] {
&self.negative_access_rules
}
pub fn set_accompaniment(
&mut self,
accompaniment: Option<BACnetDeviceObjectReference>,
) -> Result<(), Error> {
if let Some(reference) = &accompaniment {
check_accompaniment(reference)?;
}
if !self.keeps_saved(PropertyIdentifier::ACCOMPANIMENT) {
self.accompaniment = accompaniment;
}
Ok(())
}
pub fn accompaniment(&self) -> Option<&BACnetDeviceObjectReference> {
self.accompaniment.as_ref()
}
}
pub(super) fn check_accompaniment(reference: &BACnetDeviceObjectReference) -> Result<(), Error> {
crate::device_reference::check_device_member(reference.device_identifier)?;
let named = matches!(
reference.object_identifier.object_type(),
ObjectType::ACCESS_RIGHTS | ObjectType::ACCESS_CREDENTIAL | ObjectType::ACCESS_USER
);
if named || unspecified(reference.object_identifier, reference.device_identifier) {
Ok(())
} else {
Err(common::value_out_of_range_error())
}
}
pub(super) fn checked_rules(
rules: impl IntoIterator<Item = BACnetAccessRule>,
) -> Result<Vec<BACnetAccessRule>, Error> {
let rules: Vec<BACnetAccessRule> = rules.into_iter().collect();
check_rule_count(rules.len())?;
rules.iter().try_for_each(check_access_rule)?;
Ok(rules)
}
pub(super) fn check_rule_count(count: usize) -> Result<(), Error> {
if count > MAX_ACCESS_RULES {
Err(common::protocol_error(
ErrorClass::RESOURCES,
ErrorCode::NO_SPACE_TO_WRITE_PROPERTY,
))
} else {
Ok(())
}
}
pub(super) fn check_access_rule(rule: &BACnetAccessRule) -> Result<(), Error> {
let time_range_device = rule.time_range.as_ref().and_then(|r| r.device_identifier);
crate::device_reference::check_device_member(time_range_device)?;
let location_device = rule.location.as_ref().and_then(|r| r.device_identifier);
crate::device_reference::check_device_member(location_device)?;
let time_range_ok = match rule.time_range_specifier {
AccessRuleTimeRangeSpecifier::SPECIFIED => rule.time_range.is_some(),
AccessRuleTimeRangeSpecifier::ALWAYS => rule
.time_range
.as_ref()
.is_none_or(|r| unspecified(r.object_identifier, r.device_identifier)),
_ => false,
};
let location_ok = match rule.location_specifier {
AccessRuleLocationSpecifier::SPECIFIED => rule.location.as_ref().is_some_and(|r| {
matches!(
r.object_identifier.object_type(),
ObjectType::ACCESS_POINT | ObjectType::ACCESS_ZONE
) || unspecified(r.object_identifier, r.device_identifier)
}),
AccessRuleLocationSpecifier::ALL => rule
.location
.as_ref()
.is_none_or(|r| unspecified(r.object_identifier, r.device_identifier)),
_ => false,
};
if time_range_ok && location_ok {
Ok(())
} else {
Err(common::value_out_of_range_error())
}
}
fn unspecified(object: ObjectIdentifier, device: Option<ObjectIdentifier>) -> bool {
let unused = |oid: ObjectIdentifier| oid.instance_number() == ObjectIdentifier::MAX_INSTANCE;
unused(object) && device.is_none_or(unused)
}
fn rule_values(rules: &[BACnetAccessRule]) -> Vec<PropertyValue> {
rules
.iter()
.map(|rule| {
let mut buf = BytesMut::new();
encode_access_rule(&mut buf, rule);
PropertyValue::ApplicationData(buf.to_vec())
})
.collect()
}
impl BACnetObject for AccessRightsObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
&self.name
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if let Some(result) =
read_common_properties!(self, property, array_index, no_out_of_service)
{
return result;
}
match property {
p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
ObjectType::ACCESS_RIGHTS.to_raw(),
)),
p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
Ok(PropertyValue::Unsigned(self.global_identifier))
}
p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.enable)),
p if p == PropertyIdentifier::POSITIVE_ACCESS_RULES => {
common::read_array(rule_values(&self.positive_access_rules), array_index)
}
p if p == PropertyIdentifier::NEGATIVE_ACCESS_RULES => {
common::read_array(rule_values(&self.negative_access_rules), array_index)
}
p if p == PropertyIdentifier::ACCOMPANIMENT => match &self.accompaniment {
Some(reference) => Ok(crate::device_reference::reference_value(reference)),
None => Err(common::unknown_property_error()),
},
_ => Err(common::unknown_property_error()),
}
}
fn write_property(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
if let Some(result) = common::write_description(&mut self.description, property, &value) {
return result;
}
match property {
p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
if let PropertyValue::Unsigned(v) = value {
self.global_identifier = v;
Ok(())
} else {
Err(common::invalid_data_type_error())
}
}
p if p == PropertyIdentifier::LOG_ENABLE
|| p == PropertyIdentifier::POSITIVE_ACCESS_RULES
|| p == PropertyIdentifier::NEGATIVE_ACCESS_RULES =>
{
self.write_saved(property, array_index, value)
}
p if p == PropertyIdentifier::ACCOMPANIMENT => {
if self.accompaniment.is_none() {
return Err(common::unknown_property_error());
}
self.write_saved(property, array_index, value)
}
_ => Err(crate::common::unhandled_write_error(
self.property_metadata().as_ref(),
property,
array_index,
)),
}
}
fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
super::metadata_identity::for_access_rights_object(self)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
}
fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
self.expire_staged_write(now);
false
}
fn durable_writes_internal(&mut self) -> Option<&mut dyn DurableWrites> {
Some(self)
}
}
#[cfg(test)]
#[path = "rights_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "rights_test_storage.rs"]
mod test_storage;
#[cfg(test)]
#[path = "rights_persistence_tests.rs"]
mod persistence_tests;
#[cfg(test)]
#[path = "rights_staging_tests.rs"]
mod staging_tests;
#[cfg(test)]
#[path = "rights_fold_tests.rs"]
mod fold_tests;
#[cfg(test)]
#[path = "rights_accompaniment_tests.rs"]
mod accompaniment_tests;