use super::*;
#[derive(Clone, Copy)]
pub(crate) struct WriteTarget<'a> {
pub oid: ObjectIdentifier,
pub property: PropertyIdentifier,
pub array_index: Option<u32>,
pub priority: Option<u8>,
pub value: &'a [u8],
}
pub(crate) trait WriteCommitObserver: Send {
fn before(&mut self, db: &ObjectDatabase, write: WriteTarget<'_>);
fn commit_policy(
&mut self,
_db: &mut ObjectDatabase,
_write: WriteTarget<'_>,
_value: &PropertyValue,
) -> Option<Result<(), Error>> {
None
}
fn committed(&mut self, db: &mut ObjectDatabase);
fn failed(&mut self, db: &mut ObjectDatabase, error: &Error);
fn applied(&mut self, _db: &ObjectDatabase, _oid: ObjectIdentifier) {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Applied {
Written,
Unchanged,
}
pub(super) fn check_and_prepare_name_write(
db: &ObjectDatabase,
oid: &ObjectIdentifier,
value: &PropertyValue,
) -> Result<(), Error> {
if let PropertyValue::CharacterString(new_name) = value {
db.check_name_available(oid, new_name)?;
}
Ok(())
}
pub(crate) enum WritePropertyMultipleOutcome {
Success {
committed_oids: Vec<ObjectIdentifier>,
},
Error {
error: Error,
first_failed_write_attempt: BACnetObjectPropertyReference,
committed_oids: Vec<ObjectIdentifier>,
},
Reject {
reason: RejectReason,
},
}
pub fn handle_write_property_multiple(
db: &mut ObjectDatabase,
service_data: &[u8],
) -> Result<Vec<ObjectIdentifier>, Error> {
let mut snapshots = crate::life_safety_cov::LifeSafetyCovSnapshots::default();
match handle_write_property_multiple_detailed(db, service_data, &mut snapshots) {
WritePropertyMultipleOutcome::Success { committed_oids } => {
crate::command_lists::end_unmade(db, &committed_oids);
Ok(committed_oids)
}
WritePropertyMultipleOutcome::Error {
error,
committed_oids,
..
} => {
crate::command_lists::end_unmade(db, &committed_oids);
Err(error)
}
WritePropertyMultipleOutcome::Reject { reason } => Err(Error::reject(reason)),
}
}
pub(crate) fn handle_write_property_multiple_detailed(
db: &mut ObjectDatabase,
service_data: &[u8],
snapshots: &mut crate::life_safety_cov::LifeSafetyCovSnapshots,
) -> WritePropertyMultipleOutcome {
handle_write_property_multiple_authorized(db, service_data, snapshots, None)
}
type WritePropertyMultipleGate<'a> =
&'a dyn Fn(&bacnet_services::wpm::WritePropertyAttempt) -> Result<(), Error>;
pub(crate) fn handle_write_property_multiple_authorized(
db: &mut ObjectDatabase,
service_data: &[u8],
snapshots: &mut crate::life_safety_cov::LifeSafetyCovSnapshots,
authorize: Option<WritePropertyMultipleGate<'_>>,
) -> WritePropertyMultipleOutcome {
handle_write_property_multiple_observed(
db,
service_data,
snapshots,
authorize,
None,
None,
None,
)
}
pub(crate) fn handle_write_property_multiple_observed(
db: &mut ObjectDatabase,
service_data: &[u8],
snapshots: &mut crate::life_safety_cov::LifeSafetyCovSnapshots,
authorize: Option<WritePropertyMultipleGate<'_>>,
mut observer: Option<&mut dyn WriteCommitObserver>,
source: Option<&bacnet_objects::device::AuditWriteSource>,
command_origin: Option<&bacnet_objects::command_source::CommandOrigin>,
) -> WritePropertyMultipleOutcome {
let mut cursor = WritePropertyMultipleCursor::new(service_data);
let mut committed_oids = Vec::new();
let mut wrote = false;
loop {
let event = match cursor.next_event() {
Ok(Some(event)) => event,
Ok(None) => {
return WritePropertyMultipleOutcome::Success { committed_oids };
}
Err(cursor_error) => {
use bacnet_services::wpm::WritePropertyMultipleFailureKind;
let code = match cursor_error.kind {
WritePropertyMultipleFailureKind::PriorityOutOfRange => {
ErrorCode::PARAMETER_OUT_OF_RANGE
}
WritePropertyMultipleFailureKind::Syntax(reason) if !wrote => {
return WritePropertyMultipleOutcome::Reject { reason };
}
WritePropertyMultipleFailureKind::Syntax(_) => ErrorCode::INVALID_TAG,
};
return WritePropertyMultipleOutcome::Error {
error: protocol_error(ErrorClass::SERVICES, code),
first_failed_write_attempt: cursor_error
.first_failed_write_attempt
.unwrap_or_else(wpm_undecodable_coordinate),
committed_oids,
};
}
};
let WritePropertyMultipleEvent::WriteAttempt(attempt) = event else {
continue;
};
let reference = attempt.reference.clone();
let oid = reference.object_identifier;
let property = PropertyIdentifier::from_raw(reference.property_identifier);
let Some(object) = db.get(&oid) else {
return semantic_failure(
protocol_error(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT),
reference,
committed_oids,
);
};
let value = match gate_and_decode_write(
object,
property,
reference.property_array_index,
&attempt.value,
) {
Ok(value) => crate::local_references::localize(db, oid, property, value),
Err(error) => return semantic_failure(error, reference, committed_oids),
};
if property == PropertyIdentifier::OBJECT_NAME
&& let Err(error) = check_and_prepare_name_write(db, &oid, &value)
{
return semantic_failure(error, reference, committed_oids);
}
if let Some(authorize) = authorize
&& let Err(error) = authorize(&attempt)
{
return semantic_failure(error, reference, committed_oids);
}
if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT
&& let Some(mut device) = db
.get_mut(&oid)
.and_then(|object| object.device_authority_internal())
&& device.object_identifier() == oid
{
if let Err(error) = device.write_audit_recipient(
reference.property_array_index,
value,
attempt.priority,
source,
) {
return semantic_failure(error, reference, committed_oids);
}
if let Some(observer) = observer.as_deref_mut() {
observer.applied(db, oid);
}
wrote = true;
if !committed_oids.contains(&oid) {
committed_oids.push(oid);
}
continue;
}
snapshots.capture_before_write(db, oid);
let target = WriteTarget {
oid,
property,
array_index: reference.property_array_index,
priority: attempt.priority,
value: &attempt.value,
};
match commit_attempt(
db,
observer.as_deref_mut(),
target,
value,
source,
command_origin,
) {
Ok(Applied::Unchanged) => wrote = true,
Ok(Applied::Written) => {
wrote = true;
if !committed_oids.contains(&oid) {
committed_oids.push(oid);
}
}
Err(error) => return semantic_failure(error, reference, committed_oids),
}
}
}
pub(super) fn commit_attempt(
db: &mut ObjectDatabase,
mut observer: Option<&mut (dyn WriteCommitObserver + '_)>,
target: WriteTarget<'_>,
value: PropertyValue,
source: Option<&bacnet_objects::device::AuditWriteSource>,
command_origin: Option<&bacnet_objects::command_source::CommandOrigin>,
) -> Result<Applied, Error> {
if let Some(observer) = observer.as_deref_mut() {
observer.before(db, target);
}
let prepared = observer
.as_deref_mut()
.and_then(|observer| observer.commit_policy(db, target, &value));
let result = prepared.unwrap_or_else(|| {
write_with_source(
db.get_mut(&target.oid).expect("existence checked above"),
target.property,
target.array_index,
value,
target.priority,
source,
command_origin,
)
});
let applied = match result {
Ok(()) => Applied::Written,
Err(error)
if super::relinquish::is_null_octets(target.value)
&& super::relinquish::leaves_unchanged(
db.get(&target.oid).expect("existence checked above"),
target.property,
target.array_index,
&error,
) =>
{
Applied::Unchanged
}
Err(error) => {
if let Some(observer) = observer {
observer.failed(db, &error);
}
return Err(error);
}
};
if applied == Applied::Written && target.property == PropertyIdentifier::OBJECT_NAME {
db.update_name_index(&target.oid);
}
if applied == Applied::Written && target.property == PropertyIdentifier::INPUT_REFERENCE {
db.check_input_reference(&target.oid);
}
if let Some(observer) = observer {
observer.committed(db);
if applied == Applied::Written {
observer.applied(db, target.oid);
}
}
Ok(applied)
}
fn semantic_failure(
error: Error,
first_failed_write_attempt: BACnetObjectPropertyReference,
committed_oids: Vec<ObjectIdentifier>,
) -> WritePropertyMultipleOutcome {
WritePropertyMultipleOutcome::Error {
error,
first_failed_write_attempt,
committed_oids,
}
}
fn protocol_error(class: ErrorClass, code: ErrorCode) -> Error {
Error::protocol(class, code)
}
pub(crate) fn check_write_array_index(
object: &dyn bacnet_objects::traits::BACnetObject,
property: PropertyIdentifier,
index: Option<u32>,
) -> Result<(), Error> {
if index.is_none() {
return Ok(());
}
let metadata = object.property_metadata();
if !metadata.is_empty()
&& !metadata
.iter()
.any(|row| row.property_identifier == property)
{
return Err(protocol_error(
ErrorClass::PROPERTY,
ErrorCode::UNKNOWN_PROPERTY,
));
}
if !object.is_array_property(property) {
return Err(protocol_error(
ErrorClass::PROPERTY,
ErrorCode::PROPERTY_IS_NOT_AN_ARRAY,
));
}
Ok(())
}
pub(crate) fn gate_and_decode_write(
object: &dyn bacnet_objects::traits::BACnetObject,
property: PropertyIdentifier,
array_index: Option<u32>,
bytes: &[u8],
) -> Result<PropertyValue, Error> {
check_write_array_index(object, property, array_index)?;
decode_write_property_value(
property,
array_index,
object.is_list_property(property),
bytes,
)
}
fn wpm_undecodable_coordinate() -> BACnetObjectPropertyReference {
BACnetObjectPropertyReference {
object_identifier: ObjectIdentifier::new(
ObjectType::DEVICE,
ObjectIdentifier::MAX_INSTANCE,
)
.expect("the wildcard instance is valid service vocabulary"),
property_identifier: PropertyIdentifier::ALL.to_raw(),
property_array_index: None,
}
}
fn invalid_data_encoding_error() -> Error {
protocol_error(ErrorClass::PROPERTY, ErrorCode::INVALID_DATA_ENCODING)
}
pub(crate) fn decode_write_property_value(
property: PropertyIdentifier,
array_index: Option<u32>,
list: bool,
bytes: &[u8],
) -> Result<PropertyValue, Error> {
if property == PropertyIdentifier::EVENT_PARAMETERS && bytes.starts_with(&[0xfe, 0xff]) {
use bacnet_types::constructed::BACnetEventParameter;
return match bacnet_encoding::constructed::decode_event_parameter(bytes, 0) {
Ok((BACnetEventParameter::Opaque { tag, data }, consumed))
if tag == u8::MAX && consumed == bytes.len() =>
{
Ok(PropertyValue::OctetString(data))
}
_ => Err(invalid_data_encoding_error()),
};
}
if matches!(
property,
PropertyIdentifier::RECIPIENT_LIST
| PropertyIdentifier::BBMD_BROADCAST_DISTRIBUTION_TABLE
| PropertyIdentifier::FD_BBMD_ADDRESS
| PropertyIdentifier::BACNET_IP_GLOBAL_ADDRESS
| PropertyIdentifier::SUBSCRIBED_RECIPIENTS
| PropertyIdentifier::VALUE_SOURCE
| PropertyIdentifier::EFFECTIVE_PERIOD
| PropertyIdentifier::LIGHTING_COMMAND
| PropertyIdentifier::COLOR_COMMAND
) || (property == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES
&& array_index != Some(0))
{
return Ok(PropertyValue::ApplicationData(bytes.to_vec()));
}
if array_index != Some(0)
&& matches!(
property,
PropertyIdentifier::WEEKLY_SCHEDULE
| PropertyIdentifier::EXCEPTION_SCHEDULE
| PropertyIdentifier::POSITIVE_ACCESS_RULES
| PropertyIdentifier::NEGATIVE_ACCESS_RULES
| PropertyIdentifier::TAGS
)
{
return Ok(PropertyValue::ApplicationData(bytes.to_vec()));
}
if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
use bacnet_types::constructed::BACnetRecipient;
if let Ok((tag, end)) = bacnet_encoding::tags::decode_tag(bytes, 0)
&& tag.class == bacnet_encoding::tags::TagClass::Application
&& tag.number == bacnet_encoding::tags::app_tag::NULL
&& tag.length == 0
&& end == bytes.len()
{
return Ok(PropertyValue::Null);
}
let (recipient, consumed) = bacnet_encoding::constructed::decode_recipient(bytes, 0)
.map_err(|_| invalid_data_encoding_error())?;
if consumed != bytes.len() {
return Err(invalid_data_encoding_error());
}
if let BACnetRecipient::Device(oid) = recipient
&& (oid.object_type() != ObjectType::DEVICE
|| oid.instance_number() == ObjectIdentifier::MAX_INSTANCE)
{
return Err(invalid_data_encoding_error());
}
return Ok(PropertyValue::ApplicationData(bytes.to_vec()));
}
if array_index != Some(0)
&& matches!(
property,
PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY
| PropertyIdentifier::OBJECT_PROPERTY_REFERENCE
)
{
if bytes == [0x00] {
return Ok(PropertyValue::Null);
}
return Ok(PropertyValue::ApplicationData(bytes.to_vec()));
}
if matches!(
property,
PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE
| PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE
| PropertyIdentifier::SETPOINT_REFERENCE
| PropertyIdentifier::INPUT_REFERENCE
) {
if bytes == [0x00] {
return Ok(PropertyValue::Null);
}
return Ok(PropertyValue::ApplicationData(bytes.to_vec()));
}
if array_index != Some(0) && property == PropertyIdentifier::PORT_FILTER {
return decode_structured_array(bytes, array_index, |data, offset| {
bacnet_encoding::constructed::decode_port_permission(data, offset).map(|(_, end)| end)
});
}
if array_index != Some(0) && property == PropertyIdentifier::STAGES {
return decode_structured_array(bytes, array_index, |data, offset| {
bacnet_encoding::constructed::decode_stage_limit_value(data, offset).map(|(_, end)| end)
});
}
if array_index != Some(0) && property == PropertyIdentifier::TARGET_REFERENCES {
return Ok(PropertyValue::ApplicationData(bytes.to_vec()));
}
let mut values = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
let (value, new_offset) =
bacnet_encoding::primitives::decode_application_value(bytes, offset)
.map_err(|_| invalid_data_encoding_error())?;
values.push(value);
offset = new_offset;
}
if list && array_index.is_none() {
return Ok(PropertyValue::List(values));
}
match values.len() {
0 => Err(invalid_data_encoding_error()),
1 => Ok(values.pop().expect("one element present")),
_ => Ok(PropertyValue::List(values)),
}
}
fn decode_structured_array<F>(
bytes: &[u8],
array_index: Option<u32>,
mut decode: F,
) -> Result<PropertyValue, Error>
where
F: FnMut(&[u8], usize) -> Result<usize, Error>,
{
if super::relinquish::is_null_octets(bytes) {
return Ok(PropertyValue::Null);
}
let mut values = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
if values.len() >= 10_000 {
return Err(invalid_data_encoding_error());
}
let start = offset;
offset = decode(bytes, offset).map_err(|_| invalid_data_encoding_error())?;
if offset <= start || offset > bytes.len() {
return Err(invalid_data_encoding_error());
}
values.push(PropertyValue::ApplicationData(
bytes[start..offset].to_vec(),
));
}
if array_index.is_some() {
if values.len() == 1 {
return Ok(values.pop().unwrap());
}
return Err(invalid_data_encoding_error());
}
Ok(PropertyValue::List(values))
}
pub fn handle_write_property(
db: &mut ObjectDatabase,
service_data: &[u8],
) -> Result<ObjectIdentifier, Error> {
let (oid, _) = handle_write_property_observed(db, service_data, None, None, None)?;
crate::command_lists::end_unmade(db, std::slice::from_ref(&oid));
Ok(oid)
}
pub(crate) fn handle_write_property_observed(
db: &mut ObjectDatabase,
service_data: &[u8],
observer: Option<&mut dyn WriteCommitObserver>,
source: Option<&bacnet_objects::device::AuditWriteSource>,
command_origin: Option<&bacnet_objects::command_source::CommandOrigin>,
) -> Result<(ObjectIdentifier, Applied), Error> {
let request = WritePropertyRequest::decode(service_data).map_err(Error::into_request_reject)?;
let oid = request.object_identifier;
let object = db
.get(&oid)
.ok_or_else(|| protocol_error(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT))?;
let value = gate_and_decode_write(
object,
request.property_identifier,
request.property_array_index,
&request.property_value,
)?;
let value = crate::local_references::localize(db, oid, request.property_identifier, value);
if request.property_identifier == PropertyIdentifier::OBJECT_NAME {
check_and_prepare_name_write(db, &oid, &value)?;
}
if request.property_identifier == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT
&& let Some(mut device) = db
.get_mut(&oid)
.and_then(|object| object.device_authority_internal())
&& device.object_identifier() == oid
{
device.write_audit_recipient(
request.property_array_index,
value,
request.priority,
source,
)?;
if let Some(observer) = observer {
observer.applied(db, oid);
}
return Ok((oid, Applied::Written));
}
let target = WriteTarget {
oid,
property: request.property_identifier,
array_index: request.property_array_index,
priority: request.priority,
value: &request.property_value,
};
let applied = commit_attempt(db, observer, target, value, source, command_origin)?;
Ok((oid, applied))
}
fn write_with_source(
object: &mut dyn bacnet_objects::traits::BACnetObject,
property: PropertyIdentifier,
index: Option<u32>,
value: PropertyValue,
priority: Option<u8>,
source: Option<&bacnet_objects::device::AuditWriteSource>,
command_origin: Option<&bacnet_objects::command_source::CommandOrigin>,
) -> Result<(), Error> {
crate::device_view::check_executor_owned_write(object.object_identifier(), property)?;
if matches!(
property,
PropertyIdentifier::DESCRIPTION
| PropertyIdentifier::MAXIMUM_SEND_DELAY
| PropertyIdentifier::SEND_NOW
) && let Some(mut reporter) = object.audit_reporter_authority_internal()
{
return reporter.write_property(property, value, index, source);
}
match command_origin {
Some(origin) => object.write_property_from(property, index, value, priority, origin),
None => object.write_property(property, index, value, priority),
}
}