use bacnet_encoding::apdu::{self, AbortPdu, Apdu, ComplexAck, SegmentAck, SimpleAck, encode_apdu};
use bacnet_encoding::npdu::{Npdu, decode_npdu, encode_npdu};
use bacnet_transport::loopback::LoopbackTransport;
use bacnet_transport::port::{ReceivedNpdu, TransportPort};
use bacnet_types::enums::{AbortReason, ConfirmedServiceChoice};
use bacnet_types::error::{Error, RemoteError};
use bytes::{Bytes, BytesMut};
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::time::{Duration, timeout};
use super::{BACnetClient, ClientConfig};
const CLIENT_MAC: [u8; 1] = [0x01];
const PEER_MAC: [u8; 1] = [0x02];
const SILENCE_WINDOW: Duration = Duration::from_millis(300);
fn config() -> ClientConfig {
ClientConfig {
apdu_timeout_ms: 2_000,
apdu_retries: 0,
proposed_window_size: 1,
..ClientConfig::default()
}
}
struct PeerLink {
transport: LoopbackTransport,
rx: mpsc::Receiver<ReceivedNpdu>,
request: Option<JoinHandle<Result<Bytes, Error>>>,
}
impl PeerLink {
async fn issuing(
config: ClientConfig,
service_choice: ConfirmedServiceChoice,
service_data: Vec<u8>,
) -> Self {
let (client_transport, mut transport) =
LoopbackTransport::pair(CLIENT_MAC.to_vec(), PEER_MAC.to_vec());
let rx = transport.start().await.expect("peer transport starts");
let mut client = BACnetClient::start(config, client_transport)
.await
.expect("client starts");
let request = tokio::spawn(async move {
let result = client
.confirmed_request(&PEER_MAC, service_choice, &service_data)
.await;
client.stop().await.expect("client stops");
result
});
Self {
transport,
rx,
request: Some(request),
}
}
async fn idle(config: ClientConfig) -> (Self, BACnetClient<LoopbackTransport>) {
let (client_transport, mut transport) =
LoopbackTransport::pair(CLIENT_MAC.to_vec(), PEER_MAC.to_vec());
let rx = transport.start().await.expect("peer transport starts");
let client = BACnetClient::start(config, client_transport)
.await
.expect("client starts");
(
Self {
transport,
rx,
request: None,
},
client,
)
}
async fn send(&self, apdu: Apdu) {
let mut apdu_buf = BytesMut::new();
encode_apdu(&mut apdu_buf, &apdu).expect("valid APDU encoding");
let npdu = Npdu {
payload: apdu_buf.freeze(),
..Npdu::default()
};
let mut npdu_buf = BytesMut::new();
encode_npdu(&mut npdu_buf, &npdu).expect("valid NPDU encoding");
self.transport
.send_unicast(&npdu_buf, &CLIENT_MAC)
.await
.expect("loopback delivers");
}
async fn recv(&mut self, context: &str) -> Apdu {
match timeout(Duration::from_secs(5), self.rx.recv()).await {
Ok(Some(received)) => {
let npdu = decode_npdu(received.npdu).expect("valid NPDU");
apdu::decode_apdu(npdu.payload).expect("valid APDU")
}
Ok(None) => panic!("{context}: {}", self.epitaph().await),
Err(_) => panic!("{context}: timed out waiting for an APDU"),
}
}
async fn expect_silence(&mut self, context: &str) {
match timeout(SILENCE_WINDOW, self.rx.recv()).await {
Err(_) => {}
Ok(None) => panic!("{context}: {}", self.epitaph().await),
Ok(Some(received)) => {
let npdu = decode_npdu(received.npdu).expect("valid NPDU");
let decoded = apdu::decode_apdu(npdu.payload).expect("valid APDU");
panic!("{context}: expected no transmission, got {decoded:?}");
}
}
}
async fn epitaph(&mut self) -> String {
match self.request.take() {
Some(handle) => format!(
"client shut down, its request returned {:?}",
handle.await.expect("request task did not panic")
),
None => "client shut down".into(),
}
}
async fn request_invoke_id(&mut self) -> u8 {
match self.recv("initial request").await {
Apdu::ConfirmedRequest(req) => req.invoke_id,
other => panic!("expected ConfirmedRequest, got {other:?}"),
}
}
async fn finish(mut self) -> Result<Bytes, Error> {
let result = self
.request
.take()
.expect("link was created with a request")
.await
.expect("request task did not panic");
self.transport.stop().await.expect("peer transport stops");
result
}
}
async fn feed_segments(
link: &mut PeerLink,
invoke_id: u8,
service_choice: ConfirmedServiceChoice,
count: usize,
terminates: bool,
) {
for i in 0..count {
let is_last = i == count - 1;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: !(is_last && terminates),
invoke_id,
sequence_number: Some(i as u8),
proposed_window_size: Some(1),
service_choice,
service_ack: Bytes::from(vec![i as u8]),
}))
.await;
if is_last && terminates {
break;
}
match link.recv("segment ack").await {
Apdu::SegmentAck(ack) => {
assert!(!ack.negative_ack, "segment {i} was negatively acked");
assert_eq!(
ack.sequence_number, i as u8,
"segment {i} acked out of step"
);
}
other => panic!("segment {i}: expected SegmentAck, got {other:?}"),
}
}
}
#[tokio::test]
async fn complex_ack_for_a_different_service_does_not_answer_the_request() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::WRITE_PROPERTY,
service_ack: Bytes::from_static(b"imposter"),
}))
.await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(b"genuine"),
}))
.await;
let payload = link.finish().await.expect("request succeeds");
assert_eq!(
payload.as_ref(),
b"genuine",
"the mismatched ComplexACK was delivered to the caller"
);
}
#[tokio::test]
async fn simple_ack_for_a_different_service_does_not_answer_the_request() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
link.send(Apdu::SimpleAck(SimpleAck {
invoke_id,
service_choice: ConfirmedServiceChoice::WRITE_PROPERTY,
}))
.await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(b"genuine"),
}))
.await;
let payload = link.finish().await.expect("request succeeds");
assert_eq!(
payload.as_ref(),
b"genuine",
"the mismatched SimpleACK completed the transaction"
);
}
#[tokio::test]
async fn reassembled_ack_for_a_different_service_is_discarded() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
feed_segments(
&mut link,
invoke_id,
ConfirmedServiceChoice::ATOMIC_READ_FILE,
3,
true,
)
.await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(b"genuine"),
}))
.await;
let payload = link.finish().await.expect("request succeeds");
assert_eq!(
payload.as_ref(),
b"genuine",
"the reassembled mismatched response reached the caller"
);
}
#[tokio::test]
async fn abort_with_server_false_does_not_end_the_transaction() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
link.send(Apdu::Abort(AbortPdu {
sent_by_server: false,
invoke_id,
abort_reason: AbortReason::BUFFER_OVERFLOW,
}))
.await;
link.send(Apdu::SimpleAck(SimpleAck {
invoke_id,
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
}))
.await;
link.finish()
.await
.expect("the server=FALSE Abort aborted the transaction");
}
#[tokio::test]
async fn segment_ack_with_server_false_does_not_advance_the_send_window() {
let mut config = config();
config.max_apdu_length = 50;
config.proposed_window_size = 2;
let service_data: Vec<u8> = (0..260).map(|i| (i % 251) as u8).collect();
let mut link =
PeerLink::issuing(config, ConfirmedServiceChoice::WRITE_PROPERTY, service_data).await;
let invoke_id = link.request_invoke_id().await;
match link.recv("second segment of initial window").await {
Apdu::ConfirmedRequest(req) => assert_eq!(req.sequence_number, Some(1)),
other => panic!("expected ConfirmedRequest, got {other:?}"),
}
link.send(Apdu::SegmentAck(SegmentAck {
negative_ack: false,
sent_by_server: false,
invoke_id,
sequence_number: 1,
actual_window_size: 2,
}))
.await;
link.expect_silence("after a server=FALSE SegmentACK").await;
link.send(Apdu::SegmentAck(SegmentAck {
negative_ack: false,
sent_by_server: true,
invoke_id,
sequence_number: 1,
actual_window_size: 2,
}))
.await;
for expected_seq in [2, 3] {
match link.recv("second window").await {
Apdu::ConfirmedRequest(req) => {
assert_eq!(req.sequence_number, Some(expected_seq));
}
other => panic!("expected ConfirmedRequest, got {other:?}"),
}
}
}
#[tokio::test]
async fn segmented_ack_for_an_unknown_invoke_id_is_aborted_not_reassembled() {
let (mut link, mut client) = PeerLink::idle(config()).await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: true,
invoke_id: 42,
sequence_number: Some(0),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(&[0xAA]),
}))
.await;
match link.recv("unsolicited segment").await {
Apdu::Abort(abort) => {
assert_eq!(abort.invoke_id, 42);
assert!(!abort.sent_by_server, "this client is not the server");
assert_eq!(
abort.abort_reason,
AbortReason::INVALID_APDU_IN_THIS_STATE,
"Clause 5.4.4.1 names this reason specifically"
);
}
other => panic!("expected Abort, got {other:?}"),
}
link.expect_silence("after the unsolicited-segment Abort")
.await;
client.stop().await.expect("client stops");
link.transport.stop().await.expect("peer transport stops");
}
#[tokio::test]
async fn segment_ack_during_an_outstanding_request_is_discarded_not_aborted() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
link.send(Apdu::SegmentAck(SegmentAck {
negative_ack: false,
sent_by_server: true,
invoke_id,
sequence_number: 0,
actual_window_size: 1,
}))
.await;
link.expect_silence("stale SegmentACK during AWAIT_CONFIRMATION")
.await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(b"genuine"),
}))
.await;
let payload = link.finish().await.expect("request survives the stale ack");
assert_eq!(payload.as_ref(), b"genuine");
}
#[tokio::test]
async fn segment_ack_during_reassembly_aborts_the_transaction() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
feed_segments(
&mut link,
invoke_id,
ConfirmedServiceChoice::READ_PROPERTY,
1,
false,
)
.await;
link.send(Apdu::SegmentAck(SegmentAck {
negative_ack: false,
sent_by_server: true,
invoke_id,
sequence_number: 0,
actual_window_size: 1,
}))
.await;
match link.recv("unexpected SegmentACK during reassembly").await {
Apdu::Abort(abort) => {
assert_eq!(abort.invoke_id, invoke_id);
assert!(!abort.sent_by_server);
assert_eq!(abort.abort_reason, AbortReason::INVALID_APDU_IN_THIS_STATE);
}
other => panic!("expected Abort, got {other:?}"),
}
match link.finish().await {
Err(Error::Remote(RemoteError::Abort { reason })) => {
assert_eq!(reason, AbortReason::INVALID_APDU_IN_THIS_STATE);
}
other => panic!("expected the caller to be aborted, got {other:?}"),
}
}
#[tokio::test]
async fn segment_ack_for_an_unknown_invoke_id_is_aborted() {
let (mut link, mut client) = PeerLink::idle(config()).await;
link.send(Apdu::SegmentAck(SegmentAck {
negative_ack: false,
sent_by_server: true,
invoke_id: 7,
sequence_number: 0,
actual_window_size: 1,
}))
.await;
match link.recv("unsolicited SegmentACK").await {
Apdu::Abort(abort) => {
assert_eq!(abort.invoke_id, 7);
assert!(!abort.sent_by_server);
assert_eq!(abort.abort_reason, AbortReason::INVALID_APDU_IN_THIS_STATE);
}
other => panic!("expected Abort, got {other:?}"),
}
client.stop().await.expect("client stops");
link.transport.stop().await.expect("peer transport stops");
}
#[tokio::test]
async fn exactly_256_segments_reassemble() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
feed_segments(
&mut link,
invoke_id,
ConfirmedServiceChoice::READ_PROPERTY,
256,
true,
)
.await;
let payload = link.finish().await.expect("request succeeds");
let expected: Vec<u8> = (0..=255).collect();
assert_eq!(payload.as_ref(), expected.as_slice());
}
#[tokio::test]
async fn segment_257_aborts_with_buffer_overflow() {
let mut link =
PeerLink::issuing(config(), ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = link.request_invoke_id().await;
feed_segments(
&mut link,
invoke_id,
ConfirmedServiceChoice::READ_PROPERTY,
256,
false,
)
.await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: false,
invoke_id,
sequence_number: Some(0),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(&[0xFF]),
}))
.await;
match link.recv("overflow abort").await {
Apdu::Abort(abort) => {
assert_eq!(abort.invoke_id, invoke_id);
assert!(
!abort.sent_by_server,
"Clause 5.4.4.4 requires 'server' = FALSE"
);
assert_eq!(abort.abort_reason, AbortReason::BUFFER_OVERFLOW);
}
other => panic!("expected Abort, got {other:?}"),
}
match link.finish().await {
Err(Error::Remote(RemoteError::Abort { reason })) => {
assert_eq!(reason, AbortReason::BUFFER_OVERFLOW);
}
other => panic!("expected a BUFFER_OVERFLOW abort, got {other:?}"),
}
}
#[tokio::test]
async fn non_rung_max_segments_rounds_down_in_header_and_bounds_reassembly() {
let mut config = config();
config.max_segments = Some(5);
let mut link =
PeerLink::issuing(config, ConfirmedServiceChoice::READ_PROPERTY, vec![0x01]).await;
let invoke_id = match link.recv("initial request").await {
Apdu::ConfirmedRequest(req) => {
assert_eq!(req.max_segments, Some(4));
req.invoke_id
}
other => panic!("expected ConfirmedRequest, got {other:?}"),
};
feed_segments(
&mut link,
invoke_id,
ConfirmedServiceChoice::READ_PROPERTY,
4,
false,
)
.await;
link.send(Apdu::ComplexAck(ComplexAck {
segmented: true,
more_follows: false,
invoke_id,
sequence_number: Some(4),
proposed_window_size: Some(1),
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_ack: Bytes::from_static(&[0xFF]),
}))
.await;
match link.recv("overflow abort").await {
Apdu::Abort(abort) => assert_eq!(abort.abort_reason, AbortReason::BUFFER_OVERFLOW),
other => panic!("expected Abort, got {other:?}"),
}
assert!(matches!(
link.finish().await,
Err(Error::Remote(RemoteError::Abort { .. }))
));
}
#[tokio::test]
async fn max_segments_above_the_encodable_rungs_advertises_no_bound() {
assert_eq!(apdu::advertised_max_segments(Some(100)), None);
assert_eq!(apdu::advertised_max_segments(None), None);
assert_eq!(apdu::advertised_max_segments(Some(255)), None);
assert_eq!(apdu::advertised_max_segments(Some(4)), Some(4));
assert_eq!(apdu::advertised_max_segments(Some(64)), Some(64));
}