bacnet-encoding 0.12.0

BACnet ASN.1/BER encoding, APDU and NPDU codecs per ASHRAE 135-2020
Documentation
use super::*;
use bacnet_types::constructed::{BACnetExtendedPropertyState, BACnetProprietaryPropertyState};

fn encode_change_of_state(state: BACnetPropertyStates) -> BytesMut {
    let mut encoded = BytesMut::new();
    encode_notification_parameters(
        &NotificationParameters::ChangeOfState {
            new_state: state,
            status_flags: StatusFlags::IN_ALARM,
        },
        &mut encoded,
    )
    .unwrap();
    encoded
}

fn raw_change_of_state(tag: u8, content: &[u8]) -> BytesMut {
    let mut encoded = BytesMut::new();
    tags::encode_opening_tag(&mut encoded, 1);
    tags::encode_opening_tag(&mut encoded, 0);
    tags::encode_tag(
        &mut encoded,
        tag,
        tags::TagClass::Context,
        content.len() as u32,
    );
    encoded.extend_from_slice(content);
    tags::encode_closing_tag(&mut encoded, 0);
    primitives::encode_ctx_bit_string(&mut encoded, 1, 4, &[0x80]);
    tags::encode_closing_tag(&mut encoded, 1);
    encoded
}

#[test]
fn change_of_state_uses_clause_21_property_state_tags() {
    let variants = [
        BACnetPropertyStates::RestartReason(1),
        BACnetPropertyStates::DoorAlarmState(1),
        BACnetPropertyStates::LightingInProgress(1),
        BACnetPropertyStates::IntegerValue(-1),
        BACnetPropertyStates::TimerState(1),
        BACnetPropertyStates::LiftCarDirection(1),
        BACnetPropertyStates::AuditOperation(1),
        BACnetPropertyStates::ExtendedValue(BACnetExtendedPropertyState::new(255, 7).unwrap()),
        BACnetPropertyStates::Other(
            BACnetProprietaryPropertyState::primitive(64, vec![0xde]).unwrap(),
        ),
        BACnetPropertyStates::Other(
            BACnetProprietaryPropertyState::constructed(65, vec![0x21, 0x07]).unwrap(),
        ),
    ];

    for state in variants {
        let encoded = encode_change_of_state(state.clone());
        assert_eq!(
            decode_notification_parameters(&encoded, 0).unwrap(),
            NotificationParameters::ChangeOfState {
                new_state: state,
                status_flags: StatusFlags::IN_ALARM,
            }
        );
    }
}

#[test]
fn change_of_state_property_values_enforce_u32() {
    let unsigned_tags = [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
        27, 28, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52,
        53, 54, 55, 56, 57, 58, 59, 60, 63,
    ];

    for tag in unsigned_tags {
        assert!(
            decode_notification_parameters(&raw_change_of_state(tag, &[1, 0, 0, 0, 0]), 0).is_err()
        );
        assert!(decode_notification_parameters(&raw_change_of_state(tag, &[0xff; 8]), 0).is_err());

        let decoded = decode_notification_parameters(
            &raw_change_of_state(tag, &[0, 0xff, 0xff, 0xff, 0xff]),
            0,
        )
        .unwrap();
        let NotificationParameters::ChangeOfState { new_state, .. } = decoded else {
            unreachable!();
        };
        let expected = if tag == 63 {
            Some(u32::MAX % 100_000)
        } else {
            Some(u32::MAX)
        };
        assert_eq!(new_state.as_u32(), expected, "context tag {tag}");
    }
}

#[test]
fn change_of_state_rejects_malformed_and_reserved_property_states() {
    for content in [&[][..], &[2], &[0, 0]] {
        assert!(decode_notification_parameters(&raw_change_of_state(0, content), 0).is_err());
    }
    for tag in [26, 29, 35, 61, 62] {
        assert!(decode_notification_parameters(&raw_change_of_state(tag, &[0]), 0).is_err());
    }

    let proprietary = decode_notification_parameters(&raw_change_of_state(64, &[0xde]), 0).unwrap();
    assert!(matches!(
        proprietary,
        NotificationParameters::ChangeOfState {
            new_state: BACnetPropertyStates::Other(value),
            ..
        } if value.tag() == 64 && value.data() == [0xde] && !value.is_constructed()
    ));

    let proprietary = decode_notification_parameters(
        &encode_change_of_state(BACnetPropertyStates::Other(
            BACnetProprietaryPropertyState::constructed(65, vec![0x21, 0x07]).unwrap(),
        )),
        0,
    )
    .unwrap();
    assert!(matches!(
        proprietary,
        NotificationParameters::ChangeOfState {
            new_state: BACnetPropertyStates::Other(value),
            ..
        } if value.tag() == 65 && value.data() == [0x21, 0x07] && value.is_constructed()
    ));

    let malformed = NotificationParameters::ChangeOfState {
        new_state: BACnetPropertyStates::Other(
            BACnetProprietaryPropertyState::constructed(64, vec![0xde]).unwrap(),
        ),
        status_flags: StatusFlags::empty(),
    };
    let mut untouched = BytesMut::from(&[0xaa][..]);
    assert!(encode_notification_parameters(&malformed, &mut untouched).is_err());
    assert_eq!(untouched.as_ref(), &[0xaa]);
}

#[test]
fn change_of_state_encoder_accounts_for_outer_nesting_atomically() {
    let accepted_depth = tags::MAX_CONTEXT_NESTING_DEPTH - 3;
    let mut accepted_body = vec![0x0e; accepted_depth];
    accepted_body.extend(vec![0x0f; accepted_depth]);
    let accepted = NotificationParameters::ChangeOfState {
        new_state: BACnetPropertyStates::Other(
            BACnetProprietaryPropertyState::constructed(64, accepted_body).unwrap(),
        ),
        status_flags: StatusFlags::empty(),
    };
    let mut encoded = BytesMut::new();
    encode_notification_parameters(&accepted, &mut encoded).unwrap();
    assert_eq!(
        decode_notification_parameters(&encoded, 0).unwrap(),
        accepted
    );

    let body_depth = tags::MAX_CONTEXT_NESTING_DEPTH - 2;
    let mut body = vec![0x0e; body_depth];
    body.extend(vec![0x0f; body_depth]);
    let too_deep_state =
        BACnetPropertyStates::Other(BACnetProprietaryPropertyState::constructed(64, body).unwrap());
    let value = NotificationParameters::ChangeOfState {
        new_state: too_deep_state.clone(),
        status_flags: StatusFlags::empty(),
    };

    let mut untouched = BytesMut::from(&[0xaa][..]);
    assert!(encode_notification_parameters(&value, &mut untouched).is_err());
    assert_eq!(untouched.as_ref(), &[0xaa]);

    let mut raw = BytesMut::new();
    tags::encode_opening_tag(&mut raw, 1);
    tags::encode_opening_tag(&mut raw, 0);
    encode_property_state(&mut raw, &too_deep_state).unwrap();
    tags::encode_closing_tag(&mut raw, 0);
    primitives::encode_ctx_bit_string(&mut raw, 1, 4, &[0]);
    tags::encode_closing_tag(&mut raw, 1);
    assert!(decode_notification_parameters(&raw, 0).is_err());
}