bacnet-services 0.15.0

BACnet service request/response encode/decode per ASHRAE 135-2020
Documentation
//! WritePropertyMultiple service per ASHRAE 135-2020 Clause 15.10.

#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

use bacnet_encoding::constructed::encode_bacnet_property_value;
use bacnet_encoding::primitives;
use bacnet_encoding::tags;
use bacnet_types::enums::RejectReason;
use bacnet_types::error::{DecodingKind, Error};
use bacnet_types::primitives::ObjectIdentifier;
use bytes::BytesMut;

use crate::common::BACnetPropertyValue;

pub mod cursor;
pub mod error;
#[cfg(test)]
mod priority_tests;
#[cfg(test)]
mod validation_tests;

pub use cursor::{
    WritePropertyAttempt, WritePropertyMultipleCursor, WritePropertyMultipleCursorError,
    WritePropertyMultipleDecodeStage, WritePropertyMultipleEvent, WritePropertyMultipleFailureKind,
};
pub use error::WritePropertyMultipleError;

// ---------------------------------------------------------------------------
// WritePropertyMultipleRequest
// ---------------------------------------------------------------------------

/// The [`DecodingKind`] that draws the Reject reason the cursor gave
/// `failure` (see [`DecodingKind::reject_reason`]).
///
/// A priority outside 1..=16 is [`DecodingKind::OutOfRange`], so a responder
/// that converts [`WritePropertyMultipleRequest::decode`]'s error rejects it.
/// This stack's server doesn't: it walks the cursor itself and answers that
/// priority with an Error, PARAMETER_OUT_OF_RANGE, as it answers
/// WriteProperty's. Clause 20.1.8 leaves that choice to the implementation.
pub(crate) fn failure_kind(failure: WritePropertyMultipleFailureKind) -> DecodingKind {
    use RejectReason as R;
    match failure {
        WritePropertyMultipleFailureKind::PriorityOutOfRange => DecodingKind::OutOfRange,
        WritePropertyMultipleFailureKind::Syntax(reason) => match reason {
            R::INVALID_TAG => DecodingKind::InvalidTag,
            R::MISSING_REQUIRED_PARAMETER => DecodingKind::Missing,
            R::TOO_MANY_ARGUMENTS => DecodingKind::Trailing,
            R::PARAMETER_OUT_OF_RANGE => DecodingKind::OutOfRange,
            R::BUFFER_OVERFLOW => DecodingKind::Overflow,
            R::OTHER => DecodingKind::Unsupported,
            _ => DecodingKind::InvalidEncoding,
        },
    }
}

/// A single object + list of property values to write.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteAccessSpecification {
    /// Object to write to.
    pub object_identifier: ObjectIdentifier,
    /// Property values to write to that object, each with optional array index and priority.
    pub list_of_properties: Vec<BACnetPropertyValue>,
}

/// WritePropertyMultiple-Request service parameters.
///
/// Uses SimpleACK (no ACK struct needed).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritePropertyMultipleRequest {
    /// Per-object write specifications; must not be empty when encoding.
    pub list_of_write_access_specs: Vec<WriteAccessSpecification>,
}

impl WritePropertyMultipleRequest {
    /// Validate the complete outbound request without reading remote metadata.
    ///
    /// Each request and object specification must contain writes. Special RPM
    /// selectors are not write targets; supplied priorities must be in 1..=16.
    /// Encoded property values remain opaque, including legal empty list values.
    pub fn validate(&self) -> Result<(), Error> {
        use bacnet_types::enums::PropertyIdentifier as P;
        if self.list_of_write_access_specs.is_empty() {
            return Err(Error::encode("WPM requires at least one object specification"));
        }
        for spec in &self.list_of_write_access_specs {
            if spec.list_of_properties.is_empty() {
                return Err(Error::encode("WPM requires at least one property per object"));
            }
            for property in &spec.list_of_properties {
                if matches!(
                    property.property_identifier,
                    P::ALL | P::REQUIRED | P::OPTIONAL
                ) {
                    return Err(Error::encode("WPM does not accept ALL, REQUIRED or OPTIONAL selectors"));
                }
                crate::write_property::validate_priority(property.priority)?;
            }
        }
        Ok(())
    }

    /// Encode transactionally after validating every object/property write.
    /// Invalid typed input leaves an existing buffer unchanged. Inbound decoding
    /// remains separate and may preserve external empty-list/no-op requests.
    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
        self.validate()?;
        for spec in &self.list_of_write_access_specs {
            primitives::encode_ctx_object_id(buf, 0, &spec.object_identifier);
            tags::encode_opening_tag(buf, 1);
            for prop_val in &spec.list_of_properties {
                encode_bacnet_property_value(prop_val, buf);
            }
            tags::encode_closing_tag(buf, 1);
        }
        Ok(())
    }

    /// Decode the request from service-request octets; fails on malformed or truncated input.
    ///
    /// The error's [`DecodingKind`] is the one whose Reject reason the
    /// cursor chose, so [`Error::into_request_reject`] gives a responder the
    /// cursor's reason.
    pub fn decode(data: &[u8]) -> Result<Self, Error> {
        let mut cursor = WritePropertyMultipleCursor::new(data);
        let mut specs = Vec::new();
        let mut current = None;

        while let Some(event) = cursor
            .next_event()
            .map_err(|error| Error::Decoding(error.error))?
        {
            match event {
                WritePropertyMultipleEvent::ObjectStart(object_identifier) => {
                    current = Some(WriteAccessSpecification {
                        object_identifier,
                        list_of_properties: Vec::new(),
                    });
                }
                WritePropertyMultipleEvent::WriteAttempt(attempt) => {
                    current
                        .as_mut()
                        .expect("cursor emits attempts inside an object")
                        .list_of_properties
                        .push(BACnetPropertyValue {
                            property_identifier: bacnet_types::enums::PropertyIdentifier::from_raw(
                                attempt.reference.property_identifier,
                            ),
                            property_array_index: attempt.reference.property_array_index,
                            value: attempt.value,
                            priority: attempt.priority,
                        });
                }
                WritePropertyMultipleEvent::ObjectEnd => {
                    specs.push(current.take().expect("cursor ends an open object"));
                }
            }
        }

        Ok(Self {
            list_of_write_access_specs: specs,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bacnet_types::enums::{ObjectType, PropertyIdentifier};

    #[test]
    fn request_round_trip() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![WriteAccessSpecification {
                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
                list_of_properties: vec![BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
                    property_array_index: None,
                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
                    priority: Some(8),
                }],
            }],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();
        let decoded = WritePropertyMultipleRequest::decode(&buf).unwrap();
        assert_eq!(req, decoded);
    }

    #[test]
    fn multi_object_round_trip() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![
                WriteAccessSpecification {
                    object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
                    list_of_properties: vec![BACnetPropertyValue {
                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
                        property_array_index: None,
                        value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
                        priority: None,
                    }],
                },
                WriteAccessSpecification {
                    object_identifier: ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, 2).unwrap(),
                    list_of_properties: vec![BACnetPropertyValue {
                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
                        property_array_index: None,
                        value: vec![0x91, 0x01],
                        priority: Some(8),
                    }],
                },
            ],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();
        let decoded = WritePropertyMultipleRequest::decode(&buf).unwrap();
        assert_eq!(req, decoded);
    }

    #[test]
    fn legacy_event_parameters_do_not_consume_the_next_property() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![WriteAccessSpecification {
                object_identifier: ObjectIdentifier::new(ObjectType::EVENT_ENROLLMENT, 1).unwrap(),
                list_of_properties: vec![
                    BACnetPropertyValue {
                        property_identifier: PropertyIdentifier::EVENT_PARAMETERS,
                        property_array_index: None,
                        value: vec![0xfe, 0xff, 1, 0xff, 0xff, 0x2f, 2, 0xff, 0xff],
                        priority: None,
                    },
                    BACnetPropertyValue {
                        property_identifier: PropertyIdentifier::NOTIFICATION_CLASS,
                        property_array_index: None,
                        value: vec![0x21, 8],
                        priority: None,
                    },
                ],
            }],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();

        assert_eq!(WritePropertyMultipleRequest::decode(&buf).unwrap(), req);
    }

    #[test]
    fn ambiguous_legacy_event_parameters_are_rejected() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![WriteAccessSpecification {
                object_identifier: ObjectIdentifier::new(ObjectType::EVENT_ENROLLMENT, 1).unwrap(),
                list_of_properties: vec![BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::EVENT_PARAMETERS,
                    property_array_index: None,
                    value: vec![
                        0xfe, 0xff, 0xaa, 0xff, 0xff, 0x2f, 0x09, 0x53, 0x2e, 0xfe, 0xff, 0xbb,
                        0xff, 0xff,
                    ],
                    priority: None,
                }],
            }],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();

        assert!(WritePropertyMultipleRequest::decode(&buf).is_err());
    }

    // -----------------------------------------------------------------------
    // Malformed-input decode error tests
    // -----------------------------------------------------------------------

    #[test]
    fn test_decode_wpm_request_truncated_1_byte() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![WriteAccessSpecification {
                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
                list_of_properties: vec![BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
                    property_array_index: None,
                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
                    priority: Some(8),
                }],
            }],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();
        assert!(WritePropertyMultipleRequest::decode(&buf[..1]).is_err());
    }

    #[test]
    fn test_decode_wpm_request_truncated_3_bytes() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![WriteAccessSpecification {
                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
                list_of_properties: vec![BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
                    property_array_index: None,
                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
                    priority: Some(8),
                }],
            }],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();
        assert!(WritePropertyMultipleRequest::decode(&buf[..3]).is_err());
    }

    #[test]
    fn test_decode_wpm_request_truncated_half() {
        let req = WritePropertyMultipleRequest {
            list_of_write_access_specs: vec![WriteAccessSpecification {
                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
                list_of_properties: vec![BACnetPropertyValue {
                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
                    property_array_index: None,
                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
                    priority: Some(8),
                }],
            }],
        };
        let mut buf = BytesMut::new();
        req.encode(&mut buf).unwrap();
        let half = buf.len() / 2;
        assert!(WritePropertyMultipleRequest::decode(&buf[..half]).is_err());
    }

    #[test]
    fn test_decode_wpm_request_invalid_tag() {
        assert!(WritePropertyMultipleRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
    }
}