use super::access_control_arrays::{Expected, array_cases, assert_reads, db_with};
use super::access_rights_rules::{ANYWHERE_OFF, BUSINESS_HOURS, REMOTE_ZONE, configured};
use super::*;
use PropertyIdentifier as P;
use bacnet_encoding::constructed::encode_access_rule;
use bacnet_objects::access_control::AccessRightsObject;
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_types::constructed::{
BACnetAccessRule, BACnetDeviceObjectPropertyReference, BACnetDeviceObjectReference,
PropertyReference, ReadAccessSpecification,
};
use bacnet_types::enums::{AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier};
use bacnet_types::error::RemoteError;
const ARRAYS: [P; 2] = [P::POSITIVE_ACCESS_RULES, P::NEGATIVE_ACCESS_RULES];
const GROWN: &[u8] = &[
0x09, 0x00, 0x1E, 0x0C, 0x04, 0x7F, 0xFF, 0xFF, 0x19, 0x55, 0x1F, 0x29, 0x00, 0x3E, 0x1C, 0x08, 0x7F, 0xFF, 0xFF, 0x3F, 0x49, 0x00,
];
fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}
fn size(size: u8) -> Vec<u8> {
vec![0x21, size]
}
fn octets(rule: &BACnetAccessRule) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_access_rule(&mut buf, rule);
buf.to_vec()
}
fn rule(
time_range_specifier: AccessRuleTimeRangeSpecifier,
time_range: Option<BACnetDeviceObjectPropertyReference>,
location_specifier: AccessRuleLocationSpecifier,
location: Option<BACnetDeviceObjectReference>,
) -> Vec<u8> {
octets(&BACnetAccessRule {
time_range_specifier,
time_range,
location_specifier,
location,
enable: true,
})
}
fn write(
db: &mut ObjectDatabase,
rights: ObjectIdentifier,
property: P,
index: Option<u32>,
property_value: Vec<u8>,
) -> Result<(), Error> {
let mut request = BytesMut::new();
WritePropertyRequest {
object_identifier: rights,
property_identifier: property,
property_array_index: index,
property_value,
priority: None,
}
.encode(&mut request)
.unwrap();
handle_write_property(db, &request).map(|_| ())
}
fn write_multiple(
db: &mut ObjectDatabase,
rights: ObjectIdentifier,
writes: Vec<(P, Option<u32>, Vec<u8>)>,
) -> Result<(), Error> {
let mut request = BytesMut::new();
WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: rights,
list_of_properties: writes
.into_iter()
.map(|(property, index, value)| BACnetPropertyValue {
property_identifier: property,
property_array_index: index,
value,
priority: None,
})
.collect(),
}],
}
.encode(&mut request)
.unwrap();
handle_write_property_multiple(db, &request).map(|_| ())
}
fn assert_error(result: Result<(), Error>, class: ErrorClass, code: ErrorCode) {
assert!(
matches!(result, Err(Error::Remote(RemoteError::Error { class: c, code: e }))
if c == class && e == code),
"expected {class:?} / {code:?}, got {result:?}"
);
}
fn configured_cases() -> Vec<(P, Option<u32>, Expected)> {
let mut cases = array_cases(P::POSITIVE_ACCESS_RULES, &[BUSINESS_HOURS, ANYWHERE_OFF]);
cases.extend(array_cases(P::NEGATIVE_ACCESS_RULES, &[REMOTE_ZONE]));
cases
}
#[test]
fn access_rights_rule_arrays_take_whole_indexed_and_resize_writes() {
for property in ARRAYS {
let (mut db, rights) = db_with(Box::new(AccessRightsObject::new(7, "AR-7").unwrap()));
write(
&mut db,
rights,
property,
None,
[BUSINESS_HOURS, ANYWHERE_OFF].concat(),
)
.unwrap();
assert_reads(
&db,
rights,
&array_cases(property, &[BUSINESS_HOURS, ANYWHERE_OFF]),
);
write(&mut db, rights, property, Some(2), REMOTE_ZONE.to_vec()).unwrap();
assert_reads(
&db,
rights,
&array_cases(property, &[BUSINESS_HOURS, REMOTE_ZONE]),
);
write(&mut db, rights, property, Some(0), size(4)).unwrap();
let mut cases = array_cases(property, &[BUSINESS_HOURS, REMOTE_ZONE, GROWN, GROWN]);
cases.push((property, Some(2), Ok(REMOTE_ZONE.to_vec())));
cases.push((property, Some(3), Ok(GROWN.to_vec())));
assert_reads(&db, rights, &cases);
write(&mut db, rights, property, Some(0), size(1)).unwrap();
assert_reads(&db, rights, &array_cases(property, &[BUSINESS_HOURS]));
write(&mut db, rights, property, None, Vec::new()).unwrap();
assert_reads(&db, rights, &array_cases(property, &[]));
}
}
#[test]
fn access_rights_wpm_writes_the_arrays_and_enable() {
let (mut db, rights) = db_with(Box::new(configured()));
write_multiple(
&mut db,
rights,
vec![
(P::POSITIVE_ACCESS_RULES, None, REMOTE_ZONE.to_vec()),
(P::NEGATIVE_ACCESS_RULES, Some(1), ANYWHERE_OFF.to_vec()),
(P::NEGATIVE_ACCESS_RULES, Some(0), size(2)),
(P::LOG_ENABLE, None, vec![0x10]),
],
)
.unwrap();
let mut cases = array_cases(P::POSITIVE_ACCESS_RULES, &[REMOTE_ZONE]);
cases.extend(array_cases(
P::NEGATIVE_ACCESS_RULES,
&[ANYWHERE_OFF, GROWN],
));
cases.push((P::LOG_ENABLE, None, Ok(vec![0x10])));
assert_reads(&db, rights, &cases);
let door = rule(
AccessRuleTimeRangeSpecifier::ALWAYS,
None,
AccessRuleLocationSpecifier::SPECIFIED,
Some(oid(ObjectType::ACCESS_DOOR, 2).into()),
);
assert_error(
write_multiple(
&mut db,
rights,
vec![
(P::LOG_ENABLE, None, vec![0x11]),
(P::POSITIVE_ACCESS_RULES, Some(1), door),
(P::NEGATIVE_ACCESS_RULES, None, Vec::new()),
],
),
ErrorClass::PROPERTY,
ErrorCode::VALUE_OUT_OF_RANGE,
);
cases.pop();
cases.push((P::LOG_ENABLE, None, Ok(vec![0x11])));
assert_reads(&db, rights, &cases);
}
#[test]
fn access_rights_refused_rule_writes_leave_the_arrays_unchanged() {
use AccessRuleLocationSpecifier as L;
use AccessRuleTimeRangeSpecifier as T;
let schedule = BACnetDeviceObjectPropertyReference::new_local(
oid(ObjectType::SCHEDULE, 1),
P::PRESENT_VALUE.to_raw(),
);
let not_a_device = BACnetDeviceObjectPropertyReference {
device_identifier: Some(oid(ObjectType::ANALOG_VALUE, 99)),
..schedule.clone()
};
let value_out_of_range = |bytes| (bytes, ErrorClass::PROPERTY, ErrorCode::VALUE_OUT_OF_RANGE);
let whole = vec![
value_out_of_range(
[
BUSINESS_HOURS.to_vec(),
rule(T::SPECIFIED, Some(not_a_device), L::ALL, None),
]
.concat(),
),
(
[BUSINESS_HOURS, &[0x09, 0x01, 0x29, 0x01]].concat(),
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_ENCODING,
),
(
vec![0x21, 0x01],
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
),
];
let indexed = vec![
value_out_of_range(rule(
T::ALWAYS,
None,
L::SPECIFIED,
Some(oid(ObjectType::ACCESS_DOOR, 2).into()),
)),
value_out_of_range(rule(T::SPECIFIED, None, L::ALL, None)),
value_out_of_range(rule(T::ALWAYS, Some(schedule), L::ALL, None)),
value_out_of_range(rule(T::from_raw(2), None, L::ALL, None)),
(
[ANYWHERE_OFF, ANYWHERE_OFF].concat(),
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_ENCODING,
),
(
vec![0x91, 0x00],
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
),
];
let mut cases: Vec<(Option<u32>, Vec<u8>, ErrorClass, ErrorCode)> = Vec::new();
cases.extend(
whole
.into_iter()
.map(|(bytes, class, code)| (None, bytes, class, code)),
);
cases.extend(
indexed
.into_iter()
.map(|(bytes, class, code)| (Some(1), bytes, class, code)),
);
cases.extend([
(
Some(3),
ANYWHERE_OFF.to_vec(),
ErrorClass::PROPERTY,
ErrorCode::INVALID_ARRAY_INDEX,
),
(
Some(0),
vec![0x91, 0x02],
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
),
(
Some(0),
vec![0x22, 0x04, 0x01],
ErrorClass::RESOURCES,
ErrorCode::NO_SPACE_TO_WRITE_PROPERTY,
),
]);
for property in ARRAYS {
for (index, bytes, class, code) in cases.clone() {
let (mut db, rights) = db_with(Box::new(configured()));
assert_error(write(&mut db, rights, property, index, bytes), class, code);
assert_reads(&db, rights, &configured_cases());
}
}
}
#[test]
fn access_rights_enable_reads_and_writes_over_the_wire() {
let (mut db, rights) = db_with(Box::new(AccessRightsObject::new(7, "AR-7").unwrap()));
assert_reads(&db, rights, &[(P::LOG_ENABLE, None, Ok(vec![0x11]))]);
write(&mut db, rights, P::LOG_ENABLE, None, vec![0x10]).unwrap();
assert_reads(&db, rights, &[(P::LOG_ENABLE, None, Ok(vec![0x10]))]);
assert_error(
write(&mut db, rights, P::LOG_ENABLE, None, size(1)),
ErrorClass::PROPERTY,
ErrorCode::INVALID_DATA_TYPE,
);
assert_error(
write(&mut db, rights, P::LOG_ENABLE, Some(1), vec![0x11]),
ErrorClass::PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
);
assert_reads(&db, rights, &[(P::LOG_ENABLE, None, Ok(vec![0x10]))]);
}
#[test]
fn rpm_all_after_network_writes_carries_the_written_rules_and_enable() {
let (mut db, rights) = db_with(Box::new(AccessRightsObject::new(7, "AR-7").unwrap()));
write(
&mut db,
rights,
P::POSITIVE_ACCESS_RULES,
None,
[BUSINESS_HOURS, ANYWHERE_OFF].concat(),
)
.unwrap();
write(&mut db, rights, P::NEGATIVE_ACCESS_RULES, Some(0), size(1)).unwrap();
write(&mut db, rights, P::LOG_ENABLE, None, vec![0x10]).unwrap();
let mut request = BytesMut::new();
ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: rights,
list_of_property_references: vec![PropertyReference {
property_identifier: P::ALL,
property_array_index: None,
}],
}],
}
.encode(&mut request)
.unwrap();
let mut response = BytesMut::new();
handle_read_property_multiple(&db, &request, &mut response).unwrap();
let ack = ReadPropertyMultipleACK::decode(&response).unwrap();
let results = &ack.list_of_read_access_results[0].list_of_results;
for (property, octets) in [
(
P::POSITIVE_ACCESS_RULES,
[BUSINESS_HOURS, ANYWHERE_OFF].concat(),
),
(P::NEGATIVE_ACCESS_RULES, GROWN.to_vec()),
(P::LOG_ENABLE, vec![0x10]),
] {
let found: Vec<_> = results
.iter()
.filter(|result| result.property_identifier == property)
.collect();
assert_eq!(found.len(), 1, "{property:?}");
assert_eq!(found[0].error, None, "{property:?}");
assert_eq!(
found[0].property_value.as_deref(),
Some(octets.as_slice()),
"{property:?}"
);
}
}