bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::mutation_tests::{Fixture, apdu, assert_denied, oid, value, wpm};
use super::*;
use crate::mutation::MutationTarget;
use bacnet_services::wpm::{WriteAccessSpecification, WritePropertyMultipleError};
use std::sync::{Mutex as StdMutex, atomic::AtomicUsize};

fn description(object: ObjectIdentifier, text: &str) -> WriteAccessSpecification {
    WriteAccessSpecification {
        object_identifier: object,
        list_of_properties: vec![BACnetPropertyValue {
            property_identifier: PropertyIdentifier::DESCRIPTION,
            property_array_index: None,
            value: value(PropertyValue::CharacterString(text.into())),
            priority: None,
        }],
    }
}

#[tokio::test]
async fn wpm_per_element_denial_or_panic_keeps_prefix_and_skips_suffix() {
    let first = oid(ObjectType::BINARY_VALUE, 1);
    let second = oid(ObjectType::BINARY_VALUE, 2);
    for panics in [false, true] {
        let calls = Arc::new(StdMutex::new(Vec::new()));
        let seen = calls.clone();
        let fixture = Fixture::new(Some(Arc::new(move |context| {
            let MutationTarget::WritePropertyMultiple(attempt) = &context.target else {
                panic!("wrong target");
            };
            seen.lock().unwrap().push(attempt.clone());
            if attempt.reference.object_identifier == second {
                assert!(!panics, "second element policy panic");
                false
            } else {
                true
            }
        })));
        let initial_second = fixture.read(second, PropertyIdentifier::DESCRIPTION).await;
        let bytes = wpm(vec![
            description(first, "prefix"),
            description(second, "denied"),
            description(first, "suffix"),
        ]);
        let response = fixture
            .dispatch(ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE, bytes, 3)
            .await
            .unwrap();
        assert_denied(
            response.clone(),
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            3,
        );
        let Apdu::Error(error) = apdu(response) else {
            unreachable!()
        };
        let detailed = WritePropertyMultipleError::from_error_pdu(&error).unwrap();
        assert_eq!(
            detailed.first_failed_write_attempt.object_identifier,
            second
        );
        assert_eq!(
            detailed.first_failed_write_attempt.property_identifier,
            PropertyIdentifier::DESCRIPTION.to_raw()
        );
        assert_eq!(
            detailed.first_failed_write_attempt.property_array_index,
            None
        );
        assert_eq!(
            fixture.read(first, PropertyIdentifier::DESCRIPTION).await,
            PropertyValue::CharacterString("prefix".into())
        );
        assert_eq!(
            fixture.read(second, PropertyIdentifier::DESCRIPTION).await,
            initial_second
        );
        let calls = calls.lock().unwrap();
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].reference.object_identifier, first);
        assert_eq!(
            calls[0].value,
            value(PropertyValue::CharacterString("prefix".into()))
        );
        assert_eq!(calls[1].reference.object_identifier, second);
    }
}

#[tokio::test]
async fn wpm_malformed_suffix_preserves_authorized_prefix_and_existing_error_bytes() {
    let first = oid(ObjectType::BINARY_VALUE, 1);
    for exact_coordinate in [false, true] {
        let mut bytes = BytesMut::from(wpm(vec![description(first, "prefix")]).as_ref());
        if exact_coordinate {
            // Incomplete indexed property value in the same object's list.
            bytes.truncate(bytes.len() - 1);
            bacnet_encoding::primitives::encode_ctx_unsigned(
                &mut bytes,
                0,
                PropertyIdentifier::OBJECT_TYPE.to_raw() as u64,
            );
            bacnet_encoding::primitives::encode_ctx_unsigned(&mut bytes, 1, 4);
        } else {
            // An undecodable next object coordinate after a complete prefix.
            bytes.extend_from_slice(&[0x0c]);
        }
        let bytes = bytes.freeze();
        let calls = Arc::new(AtomicUsize::new(0));
        let seen = calls.clone();
        let allow = Fixture::new(Some(Arc::new(move |_| {
            seen.fetch_add(1, Ordering::Relaxed);
            true
        })));
        let absent = Fixture::new(None);
        let baseline = absent
            .dispatch(
                ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
                bytes.clone(),
                1,
            )
            .await
            .unwrap();
        let response = allow
            .dispatch(ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE, bytes, 1)
            .await
            .unwrap();
        assert_eq!(response, baseline);
        let Apdu::Error(error) = apdu(response) else {
            panic!("prefix requires Error, not Reject")
        };
        let detailed = WritePropertyMultipleError::from_error_pdu(&error).unwrap();
        assert_eq!(detailed.error_class, ErrorClass::SERVICES);
        assert_eq!(detailed.error_code, ErrorCode::INVALID_TAG);
        if exact_coordinate {
            assert_eq!(detailed.first_failed_write_attempt.object_identifier, first);
            assert_eq!(
                detailed.first_failed_write_attempt.property_identifier,
                PropertyIdentifier::OBJECT_TYPE.to_raw()
            );
            assert_eq!(
                detailed.first_failed_write_attempt.property_array_index,
                Some(4)
            );
        } else {
            assert_eq!(
                detailed
                    .first_failed_write_attempt
                    .object_identifier
                    .instance_number(),
                ObjectIdentifier::MAX_INSTANCE
            );
        }
        assert_eq!(calls.load(Ordering::Relaxed), 1);
        assert_eq!(
            allow.read(first, PropertyIdentifier::DESCRIPTION).await,
            PropertyValue::CharacterString("prefix".into())
        );
    }
}

#[tokio::test]
async fn wpm_allow_all_preserves_order_index_priority_and_raw_value() {
    let calls = Arc::new(StdMutex::new(Vec::new()));
    let seen = calls.clone();
    let allow = Fixture::new(Some(Arc::new(move |context| {
        seen.lock().unwrap().push(context.target.clone());
        true
    })));
    let absent = Fixture::new(None);
    for fixture in [&allow, &absent] {
        fixture
            .db
            .write()
            .await
            .add(Box::new(
                bacnet_objects::multistate::MultiStateValueObject::new(1, "array-policy", 8)
                    .unwrap(),
            ))
            .unwrap();
    }
    let mut spec = description(oid(ObjectType::MULTI_STATE_VALUE, 1), "first");
    spec.list_of_properties.push(BACnetPropertyValue {
        property_identifier: PropertyIdentifier::STATE_TEXT,
        property_array_index: Some(8),
        value: value(PropertyValue::CharacterString("eighth".into())),
        priority: Some(4),
    });
    let bytes = wpm(vec![
        spec,
        description(oid(ObjectType::BINARY_VALUE, 2), "third"),
    ]);
    let baseline = absent
        .dispatch(
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            bytes.clone(),
            1,
        )
        .await
        .unwrap();
    let response = allow
        .dispatch(ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE, bytes, 1)
        .await
        .unwrap();
    assert_eq!(baseline, response);
    assert!(matches!(apdu(response), Apdu::SimpleAck(_)));
    let calls = calls.lock().unwrap();
    assert_eq!(calls.len(), 3);
    let MutationTarget::WritePropertyMultiple(second) = &calls[1] else {
        unreachable!()
    };
    assert_eq!(
        second.reference.object_identifier,
        oid(ObjectType::MULTI_STATE_VALUE, 1)
    );
    assert_eq!(
        second.reference.property_identifier,
        PropertyIdentifier::STATE_TEXT.to_raw()
    );
    assert_eq!(second.reference.property_array_index, Some(8));
    assert_eq!(second.priority, Some(4));
    assert_eq!(
        second.value,
        value(PropertyValue::CharacterString("eighth".into()))
    );
}

#[tokio::test]
async fn wpm_empty_request_never_calls_policy() {
    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);
        false
    })));
    let response = fixture
        .dispatch(
            ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
            Bytes::new(),
            1,
        )
        .await
        .unwrap();
    assert!(matches!(apdu(response), Apdu::SimpleAck(_)));
    assert_eq!(calls.load(Ordering::Relaxed), 0);
}

/// A NULL Recipient_List leaves the list as it was and succeeds (#1396). It
/// commits no object, but it is a successful write, so a syntax error after
/// it gets the Error PDU with SERVICES / INVALID_TAG, as after any other
/// successful write, not a Reject (Clause 15.10.2).
#[tokio::test]
async fn wpm_syntax_error_after_a_null_left_unchanged_is_an_error_not_a_reject() {
    let class = oid(ObjectType::NOTIFICATION_CLASS, 1);
    let witness = oid(ObjectType::BINARY_VALUE, 1);
    let null = WriteAccessSpecification {
        object_identifier: class,
        list_of_properties: vec![BACnetPropertyValue {
            property_identifier: PropertyIdentifier::RECIPIENT_LIST,
            property_array_index: None,
            value: vec![0x00],
            priority: None,
        }],
    };
    // A second specification, cut short.
    let suffix = wpm(vec![description(witness, "unreached")]);
    let truncated = &suffix[..suffix.len() - 2];
    for first in [null, description(witness, "prefix")] {
        let fixture = Fixture::new(None);
        fixture
            .db
            .write()
            .await
            .add(Box::new(
                bacnet_objects::notification_class::NotificationClass::new(1, "NC-1").unwrap(),
            ))
            .unwrap();
        let before = fixture
            .read(class, PropertyIdentifier::RECIPIENT_LIST)
            .await;
        let mut bytes = BytesMut::from(wpm(vec![first]).as_ref());
        bytes.extend_from_slice(truncated);
        let response = fixture
            .dispatch(
                ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
                bytes.freeze(),
                1,
            )
            .await
            .unwrap();
        let Apdu::Error(error) = apdu(response) else {
            panic!("a successful write before the syntax error requires Error, not Reject")
        };
        let detailed = WritePropertyMultipleError::from_error_pdu(&error).unwrap();
        assert_eq!(detailed.error_class, ErrorClass::SERVICES);
        assert_eq!(detailed.error_code, ErrorCode::INVALID_TAG);
        assert_eq!(
            fixture
                .read(class, PropertyIdentifier::RECIPIENT_LIST)
                .await,
            before
        );
        assert_ne!(
            fixture.read(witness, PropertyIdentifier::DESCRIPTION).await,
            PropertyValue::CharacterString("unreached".into())
        );
    }
}