bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::mutation_tests::{Fixture, SOURCE, apdu, assert_denied, cases, oid, route};
use super::*;
use crate::mutation::MutationTarget;
use crate::server::test_transport::TestTransport;
use bacnet_encoding::{apdu::decode_apdu, npdu::decode_npdu};
use bacnet_network::layer::ReceivedApdu;
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_services::device_mgmt::DeviceCommunicationControlRequest;
use bacnet_services::file::{AtomicWriteFileRequest, FileWriteAccessMethod};
use bacnet_services::read_property::ReadPropertyRequest;
use bacnet_transport::port::TransportProvenance;
use bacnet_types::enums::EnableDisable;
use std::sync::atomic::AtomicUsize;

#[tokio::test]
async fn deny_all_leaves_read_discovery_and_password_authorized_dcc_working() {
    let calls = Arc::new(AtomicUsize::new(0));
    let seen = calls.clone();
    let mut fixture = Fixture::new(Some(Arc::new(move |_| {
        seen.fetch_add(1, Ordering::Relaxed);
        false
    })));
    let mut bytes = BytesMut::new();
    ReadPropertyRequest {
        object_identifier: oid(ObjectType::BINARY_VALUE, 1),
        property_identifier: PropertyIdentifier::PRESENT_VALUE,
        property_array_index: None,
    }
    .encode(&mut bytes);
    let response = fixture
        .dispatch(ConfirmedServiceChoice::READ_PROPERTY, bytes.freeze(), 1)
        .await
        .unwrap();
    assert!(matches!(apdu(response), Apdu::ComplexAck(_)));

    fixture
        .db
        .write()
        .await
        .add(Box::new(
            DeviceObject::new(DeviceConfig::default()).unwrap(),
        ))
        .unwrap();
    BACnetServer::<TestTransport>::handle_unconfirmed_request(
        &UnconfirmedServices {
            db: Arc::clone(&fixture.db),
            comm_state: Arc::clone(&fixture.state),
            discovery_limiter: Arc::new(DiscoveryLimiter::new(DiscoveryPolicy::default(), Some(1))),
            ..UnconfirmedServices::for_test(Arc::clone(&fixture.network), fixture.config.clone())
        },
        UnconfirmedRequestPdu {
            service_choice: UnconfirmedServiceChoice::WHO_IS,
            service_request: Bytes::new(),
        },
        &ReceivedApdu {
            router_response: None,
            direct_response: None,
            apdu: Bytes::new(),
            source_mac: MacAddr::from_slice(SOURCE),
            ingress_network: None,
            source_network: route(),
            link_layer_group: false,
            is_group: false,
            global_broadcast: false,
            data_attributes: vec![],
            provenance: TransportProvenance::unverified(),
            reply_tx: None,
        },
    )
    .await;
    let discovery = fixture
        .network
        .transport()
        .sent()
        .lock()
        .pop()
        .unwrap()
        .npdu;
    let npdu = decode_npdu(discovery).unwrap();
    assert_eq!(npdu.destination, route());
    let Apdu::UnconfirmedRequest(i_am) = decode_apdu(npdu.payload).unwrap() else {
        panic!("expected I-Am")
    };
    assert_eq!(i_am.service_choice, UnconfirmedServiceChoice::I_AM);
    assert_eq!(
        IAmRequest::decode(&i_am.service_request)
            .unwrap()
            .object_identifier,
        oid(ObjectType::DEVICE, 1)
    );

    fixture.config.dcc_policy = DccPolicy::RequirePassword;
    fixture.config.dcc_password = Some("local-policy-test".into());
    fixture.state.set_for_test(DccState::DisableInitiation);
    let mut bytes = BytesMut::new();
    DeviceCommunicationControlRequest {
        time_duration: None,
        enable_disable: EnableDisable::ENABLE,
        password: Some("local-policy-test".into()),
    }
    .encode(&mut bytes)
    .unwrap();
    let response = fixture
        .dispatch(
            ConfirmedServiceChoice::DEVICE_COMMUNICATION_CONTROL,
            bytes.freeze(),
            2,
        )
        .await
        .unwrap();
    assert!(matches!(apdu(response), Apdu::SimpleAck(_)));
    assert_eq!(fixture.state.get(), DccState::Enable);
    assert_eq!(calls.load(Ordering::Relaxed), 0);
}

#[tokio::test]
async fn denied_subscription_cancellations_preserve_existing_entries() {
    for (service, bytes, target) in cases().into_iter().skip(7) {
        let mut fixture = Fixture::new(None);
        assert!(matches!(
            apdu(fixture.dispatch(service, bytes, 1).await.unwrap()),
            Apdu::SimpleAck(_)
        ));
        let before = fixture.table.read().await.len();
        assert!(before > 0);
        fixture.config.policy = Some(Arc::new(crate::FnPolicy::new().mutation(|_| false)));
        let mut cancellation = BytesMut::new();
        match target {
            MutationTarget::SubscribeCov(mut request) => {
                request.issue_confirmed_notifications = None;
                request.lifetime = None;
                request.encode(&mut cancellation).unwrap();
            }
            MutationTarget::SubscribeCovProperty(mut request) => {
                request.issue_confirmed_notifications = None;
                request.lifetime = None;
                request.encode(&mut cancellation).unwrap();
            }
            MutationTarget::SubscribeCovPropertyMultiple(mut request) => {
                request.lifetime = None;
                request.max_notification_delay = None;
                request.encode(&mut cancellation).unwrap();
            }
            _ => unreachable!(),
        }
        let cancellation = cancellation.freeze();
        assert_denied(
            fixture
                .dispatch(service, cancellation.clone(), 2)
                .await
                .unwrap(),
            service,
            2,
        );
        assert_eq!(fixture.table.read().await.len(), before);
        fixture.config.policy = None;
        assert!(matches!(
            apdu(fixture.dispatch(service, cancellation, 3).await.unwrap()),
            Apdu::SimpleAck(_)
        ));
        assert_eq!(fixture.table.read().await.len(), 0);
    }
}

#[tokio::test]
async fn allow_all_preserves_atomic_write_file_budget_abort() {
    let absent = Fixture::new(None);
    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 mut bytes = BytesMut::new();
    AtomicWriteFileRequest {
        file_identifier: oid(ObjectType::FILE, 1),
        access: FileWriteAccessMethod::Stream {
            file_start_position: -1,
            file_data: vec![42; 16_385],
        },
    }
    .encode(&mut bytes);
    let bytes = bytes.freeze();
    let baseline = absent
        .dispatch(ConfirmedServiceChoice::ATOMIC_WRITE_FILE, bytes.clone(), 1)
        .await
        .unwrap();
    let response = allow
        .dispatch(ConfirmedServiceChoice::ATOMIC_WRITE_FILE, bytes, 1)
        .await
        .unwrap();
    assert_eq!(baseline, response);
    assert!(
        matches!(apdu(response), Apdu::Abort(a) if a.abort_reason == AbortReason::OUT_OF_RESOURCES)
    );
    assert_eq!(calls.load(Ordering::Relaxed), 1);
    assert_eq!(
        allow
            .read(oid(ObjectType::FILE, 1), PropertyIdentifier::FILE_SIZE)
            .await,
        PropertyValue::Unsigned(8)
    );
}