bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::mutation_tests::{Fixture, apdu, oid};
use super::*;
use bacnet_encoding::{primitives, tags};
use bacnet_objects::analog::AnalogOutputObject;
use bacnet_services::wpm::WritePropertyMultipleError;
use bacnet_types::constructed::BACnetObjectPropertyReference;
use bacnet_types::enums::EngineeringUnits;
use std::sync::atomic::AtomicUsize;

fn write_value(bytes: &mut BytesMut, value: f32, index: Option<u32>) {
    primitives::encode_ctx_unsigned(bytes, 0, PropertyIdentifier::PRESENT_VALUE.to_raw() as u64);
    if let Some(index) = index {
        primitives::encode_ctx_unsigned(bytes, 1, index as u64);
    }
    tags::encode_opening_tag(bytes, 2);
    primitives::encode_app_real(bytes, value);
    tags::encode_closing_tag(bytes, 2);
}

fn request(prefix: bool, index: Option<u32>, priority: &[u8], suffix: bool) -> Bytes {
    let mut bytes = BytesMut::new();
    primitives::encode_ctx_object_id(&mut bytes, 0, &oid(ObjectType::ANALOG_OUTPUT, 7));
    tags::encode_opening_tag(&mut bytes, 1);
    if prefix {
        write_value(&mut bytes, 10.0, None);
        primitives::encode_ctx_unsigned(&mut bytes, 3, 8);
    }
    write_value(&mut bytes, 20.0, index);
    bytes.extend_from_slice(priority);
    if suffix {
        write_value(&mut bytes, 99.0, None);
        primitives::encode_ctx_unsigned(&mut bytes, 3, 1);
        tags::encode_closing_tag(&mut bytes, 1);
    }
    bytes.freeze()
}

async fn fixture() -> (Fixture, Arc<AtomicUsize>) {
    let calls = Arc::new(AtomicUsize::new(0));
    let seen = calls.clone();
    let fixture = Fixture::new(Some(Arc::new(move |_| {
        seen.fetch_add(1, Ordering::Relaxed);
        true
    })));
    fixture
        .db
        .write()
        .await
        .add(Box::new(
            AnalogOutputObject::new(7, "AO", EngineeringUnits::DEGREES_CELSIUS).unwrap(),
        ))
        .unwrap();
    (fixture, calls)
}

async fn assert_state(fixture: &Fixture, calls: &AtomicUsize, prefix: bool) {
    let db = fixture.db.read().await;
    let object = db.get(&oid(ObjectType::ANALOG_OUTPUT, 7)).unwrap();
    assert_eq!(
        object
            .read_property(PropertyIdentifier::PRESENT_VALUE, None)
            .unwrap(),
        PropertyValue::Real(if prefix { 10.0 } else { 0.0 })
    );
    for slot in 1..=16 {
        assert_eq!(
            object
                .read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(slot))
                .unwrap(),
            if prefix && slot == 8 {
                PropertyValue::Real(10.0)
            } else {
                PropertyValue::Null
            },
            "slot {slot}"
        );
    }
    assert_eq!(
        calls.load(Ordering::Relaxed),
        usize::from(prefix),
        "only the valid prefix reaches policy"
    );
}

fn assert_error(response: Bytes, index: Option<u32>, code: ErrorCode) {
    let Apdu::Error(error) = apdu(response) else {
        panic!("expected formal WPM Error")
    };
    assert_eq!(error.invoke_id, 71);
    assert_eq!(
        error.service_choice,
        ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE
    );
    assert_eq!(
        WritePropertyMultipleError::from_error_pdu(&error).unwrap(),
        WritePropertyMultipleError {
            error_class: ErrorClass::SERVICES,
            error_code: code,
            first_failed_write_attempt: BACnetObjectPropertyReference {
                object_identifier: oid(ObjectType::ANALOG_OUTPUT, 7),
                property_identifier: PropertyIdentifier::PRESENT_VALUE.to_raw(),
                property_array_index: index,
            },
        }
    );
}

#[tokio::test]
async fn wpm_priority_range_wire_error_is_semantic_before_and_after_prefix() {
    for priority in [0, 17, 257] {
        for prefix in [false, true] {
            for index in [None, Some(4)] {
                let mut encoded = BytesMut::new();
                primitives::encode_ctx_unsigned(&mut encoded, 3, priority);
                let (fixture, calls) = fixture().await;
                let response = fixture
                    .dispatch(
                        ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
                        request(prefix, index, &encoded, true),
                        71,
                    )
                    .await
                    .unwrap();
                assert_error(response, index, ErrorCode::PARAMETER_OUT_OF_RANGE);
                assert_state(&fixture, &calls, prefix).await;
            }
        }
    }
}

#[tokio::test]
async fn wpm_priority_syntax_wire_result_retains_reject_and_prefix_invalid_tag() {
    // A priority cut short is missing octets (#1446); one of no octets is an
    // invalid encoding.
    for (malformed, reason) in [
        (&[0x39][..], RejectReason::MISSING_REQUIRED_PARAMETER),
        (&[0x3a, 1], RejectReason::MISSING_REQUIRED_PARAMETER),
        (&[0x38], RejectReason::INVALID_DATA_ENCODING),
    ] {
        for prefix in [false, true] {
            let (fixture, calls) = fixture().await;
            let response = fixture
                .dispatch(
                    ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
                    request(prefix, None, malformed, false),
                    71,
                )
                .await
                .unwrap();
            if prefix {
                assert_error(response, None, ErrorCode::INVALID_TAG);
            } else {
                let Apdu::Reject(reject) = apdu(response) else {
                    panic!("expected initial syntax Reject")
                };
                assert_eq!(reject.invoke_id, 71);
                assert_eq!(reject.reject_reason, reason);
            }
            assert_state(&fixture, &calls, prefix).await;
        }
    }
}