use super::*;
use crate::server::cov_wire_test_support::Harness;
use crate::server::test_transport::TestTransport;
use bacnet_types::error::DecodingKind;
const AV_1: [u8; 5] = [0x0C, 0x00, 0x80, 0x00, 0x01];
fn answers(apdu: &Apdu, invoke_id: u8) -> bool {
match apdu {
Apdu::SimpleAck(ack) => ack.invoke_id == invoke_id,
Apdu::ComplexAck(ack) => ack.invoke_id == invoke_id,
Apdu::Error(error) => error.invoke_id == invoke_id,
Apdu::Reject(reject) => reject.invoke_id == invoke_id,
Apdu::Abort(abort) => abort.invoke_id == invoke_id,
_ => false,
}
}
pub(super) async fn answer_to(
h: &mut Harness,
service: ConfirmedServiceChoice,
body: &[u8],
) -> Apdu {
h.request(service, BytesMut::from(body)).await;
let invoke_id = h.invoke_id;
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let found = {
let mut frames = h.frames.lock().unwrap();
let at = frames.iter().position(|apdu| answers(apdu, invoke_id));
at.map(|at| frames.remove(at))
};
match found {
Some(apdu) => return apdu,
None => tokio::time::sleep(Duration::from_millis(1)).await,
}
}
})
.await
.expect("an answer to the request")
}
pub(super) async fn reject_for(
h: &mut Harness,
service: ConfirmedServiceChoice,
body: &[u8],
reason: RejectReason,
) {
reject_case(h, service, "", body, reason).await;
}
pub(super) async fn reject_case(
h: &mut Harness,
service: ConfirmedServiceChoice,
what: &str,
body: &[u8],
reason: RejectReason,
) {
match answer_to(h, service, body).await {
Apdu::Reject(reject) => {
assert_eq!(
reject.reject_reason, reason,
"{service:?} {what} {body:02X?}"
);
}
other => panic!("{service:?} {what} {body:02X?} drew {other:?}"),
}
}
async fn cut_short(h: &mut Harness, service: ConfirmedServiceChoice, body: &[u8]) {
reject_for(h, service, body, RejectReason::MISSING_REQUIRED_PARAMETER).await;
}
#[tokio::test(start_paused = true)]
async fn read_property_multiple_cut_short_is_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let service = ConfirmedServiceChoice::READ_PROPERTY_MULTIPLE;
let reference_cut = [&AV_1[..], &[0x1E, 0x0A, 0x55]].concat();
for body in [&AV_1[..4], &reference_cut] {
cut_short(&mut h, service, body).await;
}
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn subscribe_cov_property_cut_short_is_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let body = [0x09, 0x01, 0x1C, 0x00, 0x80, 0x00, 0x01, 0x4E, 0x0A, 0x55];
cut_short(
&mut h,
ConfirmedServiceChoice::SUBSCRIBE_COV_PROPERTY,
&body,
)
.await;
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn subscribe_cov_property_multiple_cut_short_is_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let body = [
&[0x09, 0x01, 0x19, 0x00, 0x4E][..],
&AV_1,
&[0x1E, 0x0E, 0x0A, 0x55],
]
.concat();
let service = ConfirmedServiceChoice::SUBSCRIBE_COV_PROPERTY_MULTIPLE;
cut_short(&mut h, service, &body).await;
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn confirmed_audit_notification_cut_short_or_misshapen_is_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let service = ConfirmedServiceChoice::CONFIRMED_AUDIT_NOTIFICATION;
let device = [0x2E, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x2F];
let three_octets = [&[0x0E][..], &device, &[0x3B, 0x00, 0x80, 0x00, 0x0F]].concat();
let cut = [&[0x0E][..], &device, &[0x3C, 0x00, 0x80]].concat();
reject_for(
&mut h,
service,
&three_octets,
RejectReason::INVALID_DATA_ENCODING,
)
.await;
cut_short(&mut h, service, &cut).await;
h.server.stop().await.unwrap();
}
#[test]
fn request_syntax_faults_draw_the_reject_naming_them_for_every_service() {
let reply = |service, error: &Error| {
BACnetServer::<TestTransport>::error_apdu_from_error(7, service, error)
};
for raw in 0..=u8::MAX {
let service = ConfirmedServiceChoice::from_raw(raw);
for (error, reason) in [
(
Error::decoding(3, "malformed"),
RejectReason::INVALID_DATA_ENCODING,
),
(
Error::out_of_range(3, "wide"),
RejectReason::PARAMETER_OUT_OF_RANGE,
),
(Error::overflow(3, "many"), RejectReason::BUFFER_OVERFLOW),
(
Error::decoding_kind(DecodingKind::Unsupported, 3, "DBCS"),
RejectReason::OTHER,
),
(Error::invalid_tag(3, "tag"), RejectReason::INVALID_TAG),
(
Error::missing(3, "missing"),
RejectReason::MISSING_REQUIRED_PARAMETER,
),
(Error::trailing(3, "more"), RejectReason::TOO_MANY_ARGUMENTS),
(
Error::buffer_too_short(9, 4),
RejectReason::MISSING_REQUIRED_PARAMETER,
),
] {
let what = format!("{service:?} {error}");
assert!(matches!(reply(service, &error), Apdu::Error(_)), "{what}");
assert_eq!(
reply(service, &error.into_request_reject()),
Apdu::Reject(RejectPdu {
invoke_id: 7,
reject_reason: reason,
}),
"{what}"
);
}
}
let refusal = Error::OutOfRange("too large".into()).into_request_reject();
let service = ConfirmedServiceChoice::READ_PROPERTY;
assert!(matches!(reply(service, &refusal), Apdu::Error(_)));
}
const PROCESS_CUT: [u8; 2] = [0x0A, 0x01];
#[tokio::test(start_paused = true)]
async fn service_parameters_cut_short_are_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let property_cut = [&AV_1[..], &[0x1A, 0x55]].concat();
let object_cut = [0x0C, 0x00, 0x80];
let cases: [(ConfirmedServiceChoice, &[u8]); 10] = [
(ConfirmedServiceChoice::READ_PROPERTY, &property_cut),
(ConfirmedServiceChoice::WRITE_PROPERTY, &property_cut),
(ConfirmedServiceChoice::READ_RANGE, &object_cut),
(ConfirmedServiceChoice::SUBSCRIBE_COV, &PROCESS_CUT),
(ConfirmedServiceChoice::SUBSCRIBE_COV_PROPERTY, &PROCESS_CUT),
(
ConfirmedServiceChoice::SUBSCRIBE_COV_PROPERTY_MULTIPLE,
&PROCESS_CUT,
),
(ConfirmedServiceChoice::ACKNOWLEDGE_ALARM, &PROCESS_CUT),
(ConfirmedServiceChoice::GET_EVENT_INFORMATION, &object_cut),
(ConfirmedServiceChoice::LIFE_SAFETY_OPERATION, &PROCESS_CUT),
(ConfirmedServiceChoice::AUDIT_LOG_QUERY, &object_cut),
];
for (service, body) in cases {
cut_short(&mut h, service, body).await;
}
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn inline_members_cut_short_are_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let object_cut = [0x0C, 0x00, 0x80];
let cases: [(ConfirmedServiceChoice, &str, &[u8]); 9] = [
(
ConfirmedServiceChoice::READ_PROPERTY,
"[0] object",
&object_cut,
),
(
ConfirmedServiceChoice::WRITE_PROPERTY,
"[0] object",
&object_cut,
),
(
ConfirmedServiceChoice::DEVICE_COMMUNICATION_CONTROL,
"[1] enable-disable",
&[0x09, 0x05, 0x1A, 0x00],
),
(
ConfirmedServiceChoice::REINITIALIZE_DEVICE,
"[0] state",
&[0x0A, 0x00],
),
(
ConfirmedServiceChoice::CONFIRMED_TEXT_MESSAGE,
"[0] source device",
&[0x0C, 0x02, 0x00],
),
(
ConfirmedServiceChoice::GET_ENROLLMENT_SUMMARY,
"[0] acknowledgment filter",
&[0x0A, 0x00],
),
(
ConfirmedServiceChoice::DELETE_OBJECT,
"object",
&[0xC4, 0x00, 0x80],
),
(
ConfirmedServiceChoice::ATOMIC_READ_FILE,
"file",
&[0xC4, 0x02, 0x80],
),
(
ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
"file",
&[0xC4, 0x02, 0x80],
),
];
for (service, member, body) in cases {
match answer_to(&mut h, service, body).await {
Apdu::Reject(reject) => assert_eq!(
reject.reject_reason,
RejectReason::MISSING_REQUIRED_PARAMETER,
"{service:?} {member}"
),
other => panic!("{service:?} {member} drew {other:?}"),
}
}
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn members_cut_short_inside_a_frame_are_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
let file = [0xC4, 0x02, 0x80, 0x00, 0x01];
let elements_cut = [&AV_1[..], &[0x19, 0x55, 0x3E, 0x22, 0x01]].concat();
let cases: [(ConfirmedServiceChoice, &str, Vec<u8>); 9] = [
(
ConfirmedServiceChoice::WRITE_PROPERTY,
"a REAL in the [3] value",
[&AV_1[..], &[0x19, 0x55, 0x3E, 0x44, 0x42, 0x90]].concat(),
),
(
ConfirmedServiceChoice::READ_RANGE,
"the reference index in [3] by-position",
[&AV_1[..], &[0x19, 0x55, 0x3E, 0x22, 0x01]].concat(),
),
(
ConfirmedServiceChoice::ATOMIC_READ_FILE,
"the octet count in [0] stream access",
[&file[..], &[0x0E, 0x31, 0x00, 0x22, 0x01]].concat(),
),
(
ConfirmedServiceChoice::ATOMIC_WRITE_FILE,
"the file data in [0] stream access",
[&file[..], &[0x0E, 0x31, 0x00, 0x63, 0x01, 0x02]].concat(),
),
(
ConfirmedServiceChoice::GET_ENROLLMENT_SUMMARY,
"the recipient device in the [1] enrollment filter",
vec![0x09, 0x00, 0x1E, 0x0E, 0x0C, 0x02, 0x00],
),
(
ConfirmedServiceChoice::AUDIT_LOG_QUERY,
"the target device in [1] by-target",
vec![0x0C, 0x0F, 0x40, 0x00, 0x01, 0x1E, 0x0E, 0x0C, 0x02, 0x00],
),
(
ConfirmedServiceChoice::CREATE_OBJECT,
"an initial value's property identifier in [1]",
vec![0x0E, 0x09, 0x02, 0x0F, 0x1E, 0x0A, 0x00],
),
(
ConfirmedServiceChoice::ADD_LIST_ELEMENT,
"an element in [3]",
elements_cut.clone(),
),
(
ConfirmedServiceChoice::REMOVE_LIST_ELEMENT,
"an element in [3]",
elements_cut,
),
];
for (service, member, body) in cases {
match answer_to(&mut h, service, &body).await {
Apdu::Reject(reject) => assert_eq!(
reject.reject_reason,
RejectReason::MISSING_REQUIRED_PARAMETER,
"{service:?} {member}"
),
other => panic!("{service:?} {member} drew {other:?}"),
}
}
h.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn create_object_and_list_requests_cut_short_are_rejected() {
let mut h = Harness::start(ServerConfig::default()).await;
cut_short(
&mut h,
ConfirmedServiceChoice::CREATE_OBJECT,
&[0x0E, 0x0A, 0x02],
)
.await;
let property_cut = [&AV_1[..], &[0x1A, 0x55]].concat();
for service in [
ConfirmedServiceChoice::ADD_LIST_ELEMENT,
ConfirmedServiceChoice::REMOVE_LIST_ELEMENT,
] {
cut_short(&mut h, service, &property_cut).await;
}
h.server.stop().await.unwrap();
}