bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use crate::server::test_transport::TestTransport;
use bacnet_encoding::apdu::decode_apdu;
use bacnet_encoding::npdu::decode_npdu;
use bacnet_objects::file::FileObject;
use bacnet_services::file::{AtomicWriteFileRequest, FileWriteAccessMethod};

fn request(record: bool) -> Bytes {
    let mut wire = BytesMut::new();
    AtomicWriteFileRequest {
        file_identifier: ObjectIdentifier::new(ObjectType::FILE, 1).unwrap(),
        access: if record {
            FileWriteAccessMethod::Record {
                file_start_record: -1,
                record_count: 257,
                file_record_data: vec![vec![42]; 257],
            }
        } else {
            FileWriteAccessMethod::Stream {
                file_start_position: -1,
                file_data: vec![42; 16385],
            }
        },
    }
    .encode(&mut wire);
    wire.freeze()
}

async fn response(
    db: &Arc<RwLock<ObjectDatabase>>,
    wire: Bytes,
    routed: bool,
    segmented: bool,
) -> Apdu {
    let network = Arc::new(NetworkLayer::new(BipTransport::new(
        Ipv4Addr::LOCALHOST,
        0,
        Ipv4Addr::BROADCAST,
    )));
    let route = routed.then(|| NpduAddress {
        network: 7,
        mac_address: MacAddr::from_slice(&[9]),
    });
    let (tx, rx) = oneshot::channel();
    BACnetServer::<BipTransport>::handle_confirmed_request(
        &RequestServices {
            db: Arc::clone(db),
            ..RequestServices::for_test(Arc::clone(&network), ServerConfig::default())
        },
        &Arc::new(ConfirmedRequestTracker::default()),
        &Arc::new(crate::server::request_tasks::RequestTasks::default()).spawner(),
        &MacAddr::from_slice(&[1]),
        route.clone(),
        ConfirmedRequestPdu {
            segmented: false,
            more_follows: false,
            segmented_response_accepted: segmented,
            max_segments: None,
            max_apdu_length: 50,
            invoke_id: 80,
            sequence_number: None,
            proposed_window_size: None,
            service_choice: ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
            service_request: wire,
        },
        Some(tx),
    )
    .await;
    let npdu = decode_npdu(rx.await.unwrap()).unwrap();
    assert_eq!(npdu.destination, route);
    decode_apdu(npdu.payload).unwrap()
}

async fn default_refusal(record: bool) {
    for routed in [false, true] {
        for segmented in [false, true] {
            let mut file = FileObject::new(1, "file", "binary").unwrap();
            if record {
                file.set_file_access_method(
                    bacnet_types::enums::FileAccessMethod::RECORD_ACCESS.to_raw(),
                );
                file.set_records(vec![b"sentinel".to_vec()]);
            } else {
                file.set_data(b"sentinel".to_vec());
            }
            let mut database = ObjectDatabase::new();
            database.add(Box::new(file)).unwrap();
            let db = Arc::new(RwLock::new(database));
            let actual = response(&db, request(record), routed, segmented).await;
            let guard = db.read().await;
            let storage = guard
                .get(&ObjectIdentifier::new(ObjectType::FILE, 1).unwrap())
                .unwrap()
                .file_storage_internal()
                .unwrap();
            let unchanged = if record {
                storage.read_records(0, 10000).unwrap().records == vec![b"sentinel".to_vec()]
            } else {
                storage.read_stream(0, 100000).unwrap().data == b"sentinel"
            };
            assert!(
                matches!(actual, Apdu::Abort(ref a) if a.sent_by_server && a.invoke_id == 80 && a.abort_reason == AbortReason::OUT_OF_RESOURCES),
                "response={actual:?}; unchanged={unchanged}"
            );
            assert!(unchanged);
        }
    }
}

#[tokio::test]
async fn atomic_write_file_default_stream_budget_refuses_without_mutation() {
    default_refusal(false).await;
}
#[tokio::test]
async fn atomic_write_file_default_record_budget_refuses_without_mutation() {
    default_refusal(true).await;
}

#[tokio::test]
async fn atomic_write_file_all_builders_validate_before_start_or_dial() {
    let default = AtomicWriteFileBudget::default();
    assert_eq!(
        (
            default.max_stream_payload_octets,
            default.max_records,
            default.max_record_payload_bytes
        ),
        (16384, 256, 16384)
    );
    assert_eq!(ServerConfig::default().atomic_write_file_budget, default);
    assert!(format!("{:?}", ServerConfig::default()).contains("atomic_write_file_budget"));
    AtomicWriteFileBudget {
        max_stream_payload_octets: usize::MAX,
        max_records: usize::MAX,
        max_record_payload_bytes: usize::MAX,
    }
    .validate()
    .unwrap();
    for budget in [
        AtomicWriteFileBudget {
            max_stream_payload_octets: 0,
            ..default
        },
        AtomicWriteFileBudget {
            max_records: 0,
            ..default
        },
        AtomicWriteFileBudget {
            max_record_payload_bytes: 0,
            ..default
        },
    ] {
        let direct = BACnetServer::start(
            ServerConfig {
                atomic_write_file_budget: budget,
                ..Default::default()
            },
            ObjectDatabase::new(),
            TestTransport::never_start(),
        )
        .await
        .err();
        let generic = BACnetServer::generic_builder()
            .transport(TestTransport::never_start())
            .atomic_write_file_budget(budget)
            .build()
            .await
            .err();
        let bip = BACnetServer::bip_builder()
            .port(0)
            .atomic_write_file_budget(budget)
            .build()
            .await
            .err();
        for result in [direct, generic, bip] {
            assert!(
                matches!(result, Some(Error::Encoding(m)) if m.contains("atomic_write_file_max_"))
            );
        }
        #[cfg(feature = "sc-tls")]
        {
            let result = BACnetServer::sc_builder()
                .hub_url("not-a-websocket-url")
                .tls_config(crate::server::sc_builder::test_tls_config())
                .device_uuid(crate::server::sc_builder::TEST_DEVICE_UUID)
                .atomic_write_file_budget(budget)
                .build()
                .await
                .err();
            assert!(
                matches!(result, Some(Error::Encoding(m)) if m.contains("atomic_write_file_max_"))
            );
        }
    }
}

#[tokio::test]
async fn atomic_write_file_opaque_abort_keeps_legacy_service_mapping() {
    use bacnet_objects::file::{FileRecordRead, FileStorage, FileStreamRead, FileWriteStart};
    use bacnet_objects::traits::BACnetObject;
    use std::borrow::Cow;
    struct ErrorFile(FileObject);
    impl BACnetObject for ErrorFile {
        fn object_identifier(&self) -> ObjectIdentifier {
            self.0.object_identifier()
        }
        fn object_name(&self) -> &str {
            self.0.object_name()
        }
        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
            Cow::Borrowed(&[])
        }
        fn read_property(
            &self,
            p: PropertyIdentifier,
            i: Option<u32>,
        ) -> Result<PropertyValue, Error> {
            self.0.read_property(p, i)
        }
        fn write_property(
            &mut self,
            _: PropertyIdentifier,
            _: Option<u32>,
            _: PropertyValue,
            _: Option<u8>,
        ) -> Result<(), Error> {
            unreachable!()
        }
        fn file_storage_internal(&self) -> Option<&dyn FileStorage> {
            Some(self)
        }
        fn file_storage_internal_mut(&mut self) -> Option<&mut dyn FileStorage> {
            Some(self)
        }
    }
    impl FileStorage for ErrorFile {
        fn read_stream(&self, _: u64, _: u64) -> Result<FileStreamRead, Error> {
            unreachable!()
        }
        fn read_records(&self, _: u64, _: u64) -> Result<FileRecordRead, Error> {
            unreachable!()
        }
        fn write_stream(&mut self, _: FileWriteStart, _: &[u8]) -> Result<u64, Error> {
            Err(Error::abort(AbortReason::OUT_OF_RESOURCES))
        }
        fn write_records(&mut self, _: FileWriteStart, _: &[Vec<u8>]) -> Result<u64, Error> {
            Err(Error::abort(AbortReason::OUT_OF_RESOURCES))
        }
    }
    for record in [false, true] {
        let mut file = FileObject::new(1, "file", "binary").unwrap();
        if record {
            file.set_file_access_method(
                bacnet_types::enums::FileAccessMethod::RECORD_ACCESS.to_raw(),
            );
        }
        let mut db = ObjectDatabase::new();
        db.add(Box::new(ErrorFile(file))).unwrap();
        let mut wire = BytesMut::new();
        AtomicWriteFileRequest {
            file_identifier: ObjectIdentifier::new(ObjectType::FILE, 1).unwrap(),
            access: if record {
                FileWriteAccessMethod::Record {
                    file_start_record: -1,
                    record_count: 1,
                    file_record_data: vec![vec![42]],
                }
            } else {
                FileWriteAccessMethod::Stream {
                    file_start_position: -1,
                    file_data: vec![42],
                }
            },
        }
        .encode(&mut wire);
        let mut out = BytesMut::new();
        let error = handlers::handle_atomic_write_file(&mut db, &wire, &mut out).unwrap_err();
        assert!(out.is_empty());
        let expected = BACnetServer::<BipTransport>::error_apdu_from_error(
            80,
            ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
            &error,
        );
        assert!(matches!(expected, Apdu::Error(_)));
        let actual = response(&Arc::new(RwLock::new(db)), wire.freeze(), true, true).await;
        let mut expected_wire = BytesMut::new();
        let mut actual_wire = BytesMut::new();
        bacnet_encoding::apdu::encode_apdu(&mut expected_wire, &expected).unwrap();
        bacnet_encoding::apdu::encode_apdu(&mut actual_wire, &actual).unwrap();
        assert_eq!(actual_wire, expected_wire);
    }
}