use crate::constructed::tagged::*;
use bacnet_types::enums::ObjectType;
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
const W: &str = "Thing";
fn decoding<T: std::fmt::Debug>(result: Result<T, Error>) -> (usize, String) {
match result {
Err(Error::Decoding {
offset, message, ..
}) => (offset, message),
other => panic!("expected a decoding error, got {other:?}"),
}
}
fn short<T: std::fmt::Debug>(result: Result<T, Error>) -> (usize, usize) {
match result {
Err(Error::BufferTooShort { need, have }) => (need, have),
other => panic!("expected a short buffer, got {other:?}"),
}
}
#[test]
fn peeks_match_only_their_own_tag_and_form() {
let data = [0x09, 0x01, 0x0E, 0x0F, 0x21, 0x05];
assert!(next_is_context(&data, 0, 0).unwrap());
assert!(!next_is_context(&data, 0, 1).unwrap());
assert!(!next_is_context(&data, 2, 0).unwrap());
assert!(!next_is_context(&data, 4, 2).unwrap());
assert!(next_is_opening(&data, 2, 0).unwrap());
assert!(!next_is_opening(&data, 0, 0).unwrap());
assert!(!next_is_opening(&data, 2, 1).unwrap());
assert!(next_is_closing(&data, 3, 0).unwrap());
assert!(!next_is_closing(&data, 2, 0).unwrap());
}
#[test]
fn optional_peeks_are_false_at_the_end_but_a_frame_must_close() {
for offset in [0, 1, 7] {
let data = &[0x09][..offset.min(1)];
assert!(!next_is_context(data, offset, 0).unwrap());
assert!(!next_is_opening(data, offset, 0).unwrap());
}
let (offset, _) = decoding(next_is_closing(&[0x0E], 1, 0));
assert_eq!(offset, 1);
let (offset, _) = decoding(next_is_context(&[0x0D], 0, 0));
assert_eq!(offset, 1);
}
#[test]
fn frames_refuse_a_wrong_number_or_form_at_the_tag() {
let data = [0xAA, 0x0E, 0x09, 0x01, 0x0F];
assert_eq!(expect_opening(&data, 1, 0, W).unwrap(), 2);
assert_eq!(expect_closing(&data, 4, 0, W).unwrap(), 5);
for (bytes, expected) in [
(&[0xAA, 0x1E][..], "Thing: expected opening tag [0]"),
(&[0xAA, 0x09, 0x01][..], "Thing: expected opening tag [0]"),
(&[0xAA, 0x0F][..], "Thing: expected opening tag [0]"),
] {
assert_eq!(
decoding(expect_opening(bytes, 1, 0, W)),
(1, expected.to_string())
);
}
assert_eq!(
decoding(expect_closing(&data, 1, 0, W)),
(1, "Thing: expected closing tag [0]".to_string())
);
assert_eq!(
decoding(expect_closing(&[0xAA, 0x1F], 1, 0, W)),
(1, "Thing: expected closing tag [0]".to_string())
);
}
#[test]
fn a_constructed_field_yields_its_balanced_body() {
let data = [0x2E, 0x0E, 0x21, 0x05, 0x0F, 0x09, 0x01, 0x2F, 0xAA];
let (body, end) = decode_ctx_constructed(&data, 0, 2, W).unwrap();
assert_eq!(body, &data[1..7]);
assert_eq!(end, 8);
assert_eq!(decoding(decode_ctx_constructed(&data, 0, 3, W)).0, 0);
let (_, message) = decoding(decode_ctx_constructed(&data[..7], 0, 2, W));
assert!(message.contains("missing closing tag 2"), "{message}");
}
#[test]
fn a_primitive_refuses_another_number_class_or_form() {
for (bytes, number) in [
(&[0x19, 0x01][..], 0), (&[0x21, 0x01][..], 2), (&[0x0E, 0x0F][..], 0), (&[0xF9, 0x10, 0x01][..], 0), ] {
let mut data = vec![0xAA];
data.extend_from_slice(bytes);
assert_eq!(
decoding(decode_ctx_primitive(&data, 1, number, W)),
(1, format!("Thing: expected context tag [{number}]"))
);
}
assert_eq!(
decode_ctx_primitive(&[0xF9, 0x10, 0x07], 0, 16, W).unwrap(),
(&[0x07][..], 3)
);
}
#[test]
fn contents_past_the_end_are_a_short_buffer() {
let data = [0x0C, 0x00, 0x00];
assert_eq!(short(decode_ctx_primitive(&data, 0, 0, W)), (5, 3));
assert_eq!(short(decode_ctx_unsigned::<u32>(&data, 0, 0, W)), (5, 3));
assert_eq!(short(decode_ctx_object_id(&data, 0, 0, W)), (5, 3));
assert_eq!(short(decode_ctx_real(&data, 0, 0, W)), (5, 3));
assert_eq!(short(decode_ctx_boolean(&[0x09], 0, 0, W)), (2, 1));
assert_eq!(short(contents(&data, 1, 4)), (5, 3));
assert_eq!(short(contents(&data, usize::MAX - 1, 10)), (usize::MAX, 3));
assert_eq!(decoding(decode_ctx_primitive(&[0x0D], 0, 0, W)).0, 1);
}
#[test]
fn fixed_size_primitives_refuse_any_other_length() {
for (bytes, expected) in [
(
&[0x0B, 1, 2, 3][..],
"Thing: [0] REAL has 3 contents octets, expected 4",
),
(
&[0x0D, 5, 1, 2, 3, 4, 5][..],
"Thing: [0] REAL has 5 contents octets, expected 4",
),
] {
assert_eq!(
decoding(decode_ctx_real(bytes, 0, 0, W)),
(0, expected.into())
);
}
for (bytes, expected) in [
(
&[0x08][..],
"Thing: [0] BOOLEAN has 0 contents octets, expected 1",
),
(
&[0x0A, 0, 1][..],
"Thing: [0] BOOLEAN has 2 contents octets, expected 1",
),
] {
assert_eq!(
decoding(decode_ctx_boolean(bytes, 0, 0, W)),
(0, expected.into())
);
}
assert_eq!(
decoding(decode_ctx_object_id(&[0x1B, 0, 0, 1], 0, 1, W)),
(
0,
"Thing: [1] object identifier has 3 contents octets, expected 4".into()
)
);
assert_eq!(
decoding(decode_ctx_real(&[0x1C, 0, 0, 0, 0], 0, 0, W)),
(0, "Thing: expected context tag [0] REAL".into())
);
}
#[test]
fn fixed_size_primitives_decode_their_values() {
assert_eq!(
decode_ctx_real(&[0x2C, 0x3F, 0x80, 0, 0], 0, 2, W).unwrap(),
(1.0, 5)
);
assert_eq!(decode_ctx_boolean(&[0x09, 0], 0, 0, W).unwrap(), (false, 2));
assert_eq!(decode_ctx_boolean(&[0x09, 1], 0, 0, W).unwrap(), (true, 2));
let device = ObjectIdentifier::new(ObjectType::DEVICE, 5).unwrap();
assert_eq!(
decode_ctx_object_id(&[0x3C, 0x02, 0, 0, 5], 0, 3, W).unwrap(),
(device, 5)
);
}
#[test]
fn a_boolean_holds_only_zero_or_one() {
assert_eq!(
decoding(decode_ctx_boolean(&[0xAA, 0x19, 0x02], 1, 1, W)),
(2, "Thing: [1] BOOLEAN contents must be 0 or 1".into())
);
}
#[test]
fn unsigned_fields_narrow_to_their_type() {
assert_eq!(
decode_ctx_unsigned::<u8>(&[0x09, 0xFF], 0, 0, W).unwrap(),
(255, 2)
);
assert_eq!(
decoding(decode_ctx_unsigned::<u8>(&[0x0A, 0x01, 0x00], 0, 0, W)),
(0, "Thing: [0] value 256 exceeds u8".into())
);
assert_eq!(
decoding(decode_ctx_unsigned::<u16>(
&[0x1B, 0x01, 0x00, 0x00],
0,
1,
W
)),
(0, "Thing: [1] value 65536 exceeds u16".into())
);
assert_eq!(
decoding(decode_ctx_unsigned::<u32>(
&[0x2D, 5, 1, 0, 0, 0, 0],
0,
2,
W
)),
(0, "Thing: [2] value 4294967296 exceeds u32".into())
);
let max = [0x0D, 8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
assert_eq!(
decode_ctx_unsigned::<u64>(&max, 0, 0, W).unwrap(),
(u64::MAX, 10)
);
assert_eq!(
decode_ctx_unsigned::<u8>(&[0x0A, 0, 7], 0, 0, W).unwrap(),
(7, 3)
);
decoding(decode_ctx_unsigned::<u64>(&[0x08], 0, 0, W));
decoding(decode_ctx_unsigned::<u64>(
&[0x0D, 9, 0, 0, 0, 0, 0, 0, 0, 0, 1],
0,
0,
W,
));
}
#[test]
fn canonical_unsigned_fields_refuse_padding_and_bad_lengths() {
assert_eq!(
decode_ctx_canonical_unsigned::<u8>(&[0x09, 0], 0, 0, W).unwrap(),
(0, 2)
);
assert_eq!(
decoding(decode_ctx_canonical_unsigned::<u8>(
&[0xAA, 0x0A, 0, 7],
1,
0,
W
)),
(
1,
"Thing: must use the shortest Unsigned/Enumerated encoding".into()
)
);
assert_eq!(
decoding(decode_ctx_canonical_unsigned::<u64>(&[0xAA, 0x08], 1, 0, W)),
(
1,
"Thing: has 0 contents octets, expected one to eight".into()
)
);
let nine = [0x0D, 9, 1, 0, 0, 0, 0, 0, 0, 0, 0];
assert_eq!(
decoding(decode_ctx_canonical_unsigned::<u64>(&nine, 0, 0, W)),
(
0,
"Thing: has 9 contents octets, expected one to eight".into()
)
);
assert_eq!(
decoding(decode_ctx_canonical_unsigned::<u8>(&[0x0A, 1, 0], 0, 0, W)),
(0, "Thing: [0] value 256 exceeds u8".into())
);
assert_eq!(
decode_canonical_unsigned(&[0x12, 0x34], 0, W).unwrap(),
0x1234
);
}
#[test]
fn an_optional_field_is_read_only_under_its_own_tag() {
let data = [0x09, 0x05, 0x19, 0x06];
let read = |offset, tag| decode_optional_ctx(&data, offset, tag, W, decode_ctx_unsigned::<u8>);
assert_eq!(read(0, 0).unwrap(), (Some(5), 2));
assert_eq!(read(0, 1).unwrap(), (None, 0));
assert_eq!(read(2, 1).unwrap(), (Some(6), 4));
assert_eq!(read(4, 1).unwrap(), (None, 4));
let (offset, message) = decoding(decode_optional_ctx(
&[0x0A, 1, 0],
0,
0,
W,
decode_ctx_unsigned::<u8>,
));
assert_eq!(
(offset, message.as_str()),
(0, "Thing: [0] value 256 exceeds u8")
);
assert_eq!(
short(decode_optional_ctx(
&[0x0C, 1],
0,
0,
W,
decode_ctx_object_id
)),
(5, 2)
);
}
#[test]
fn an_optional_primitive_yields_its_contents_or_nothing() {
fn read(data: &[u8], tag: u8) -> Result<(Option<&[u8]>, usize), Error> {
decode_optional_ctx(data, 0, tag, W, decode_ctx_primitive)
}
assert_eq!(read(&[0x09, 42], 0).unwrap(), (Some(&[42][..]), 2));
assert_eq!(read(&[0x19, 42], 0).unwrap(), (None, 0));
assert_eq!(read(&[], 0).unwrap(), (None, 0));
assert_eq!(short(read(&[0x0C, 0x01, 0x02], 0)), (5, 3));
}
#[test]
fn an_application_peek_matches_only_its_own_type() {
let data = [0x21, 0x05, 0x29, 0x05, 0xA4, 0x7E, 0x0A, 0x03, 0xFF];
assert!(next_is_application(&data, 0, 2).unwrap());
assert!(!next_is_application(&data, 0, 3).unwrap());
assert!(!next_is_application(&data, 2, 2).unwrap());
assert!(next_is_application(&data, 4, 10).unwrap());
assert!(!next_is_application(&data, data.len(), 10).unwrap());
assert!(matches!(
next_is_application(&[0x26, 0x01], 0, 2),
Err(Error::Decoding { .. })
));
}
#[test]
fn an_input_must_end_where_its_value_does() {
let data = [0x09, 0x01, 0xAA, 0xBB];
assert!(expect_end(&data, 4, 4, W).is_ok());
assert_eq!(
decoding(expect_end(&data, 2, 2, W)),
(2, "Thing: 2 trailing byte(s)".into())
);
assert_eq!(decoding(expect_end(&data[2..], 1, 40, W)).0, 40);
}
#[test]
fn bit_and_octet_strings_and_character_strings_read_their_contents() {
assert_eq!(
decode_ctx_bit_string(&[0x0A, 0x05, 0xE0], 0, 0, W).unwrap(),
((5, vec![0xE0]), 3)
);
assert_eq!(
decoding(decode_ctx_bit_string(&[0x19, 0x00], 0, 0, W)),
(0, "Thing: expected context tag [0] BIT STRING".into())
);
decoding(decode_ctx_bit_string(&[0x09, 0x08], 0, 0, W));
assert_eq!(
decode_ctx_octet_string(&[0x2A, 0xDE, 0xAD], 0, 2, W).unwrap(),
(vec![0xDE, 0xAD], 3)
);
assert_eq!(
decoding(decode_ctx_octet_string(&[0x0A, 0, 0], 0, 2, W)),
(0, "Thing: expected context tag [2] OCTET STRING".into())
);
assert_eq!(
decode_ctx_character_string(&[0x5B, 0x00, b'o', b'k'], 0, 5, W).unwrap(),
("ok".to_string(), 4)
);
}
#[test]
fn application_items_refuse_another_tag() {
assert_eq!(
decode_app_unsigned::<u64>(&[0x21, 0x07], 0, W).unwrap(),
(7, 2)
);
assert_eq!(
decoding(decode_app_unsigned::<u64>(&[0x09, 0x07], 0, W)),
(0, "Thing: expected application-tagged Unsigned".into())
);
assert_eq!(
decode_app_enumerated::<u32>(&[0x91, 0x02], 0, W).unwrap(),
(2, 2)
);
assert_eq!(
decoding(decode_app_enumerated::<u32>(
&[0x95, 5, 1, 0, 0, 0, 0],
0,
W
)),
(2, "Thing: ENUMERATED exceeds u32".into())
);
assert_eq!(
decode_app_bit_string(&[0x82, 0x07, 0x80], 0, W).unwrap(),
((7, vec![0x80]), 3)
);
assert_eq!(
decoding(decode_app_character_string(&[0x21, 0x00], 0, W)),
(
0,
"Thing: expected application-tagged CharacterString".into()
)
);
assert_eq!(
short(decode_app_unsigned::<u64>(&[0x22, 0x07], 0, W)),
(3, 2)
);
}
#[test]
fn application_primitives_read_any_type_and_name_the_expected_one() {
use crate::tags::app_tag;
let data = [0x31, 0xFF, 0x62, 0xAB, 0xCD, 0xA4, 0x7E, 0x0A, 0x03, 0x05];
assert_eq!(
decode_app_primitive(&data, 0, app_tag::SIGNED, W).unwrap(),
(&[0xFF][..], 2)
);
assert_eq!(
decode_app_primitive(&data, 2, app_tag::OCTET_STRING, W).unwrap(),
(&[0xAB, 0xCD][..], 5)
);
assert_eq!(
decode_app_primitive(&data, 5, app_tag::DATE, W).unwrap(),
(&[0x7E, 0x0A, 0x03, 0x05][..], 10)
);
for (number, kind) in [
(app_tag::SIGNED, "INTEGER"),
(app_tag::OCTET_STRING, "OCTET STRING"),
(app_tag::DATE, "Date"),
(app_tag::TIME, "Time"),
(app_tag::OBJECT_IDENTIFIER, "BACnetObjectIdentifier"),
(app_tag::DOUBLE, "Double"),
(13, "value of a reserved type"),
] {
assert_eq!(
decoding(decode_app_primitive(&[0x21, 0x00], 0, number, W)),
(0, format!("Thing: expected application-tagged {kind}"))
);
}
assert_eq!(
decoding(decode_app_primitive(&[0x39, 0x01], 0, app_tag::SIGNED, W)),
(0, "Thing: expected application-tagged INTEGER".into())
);
assert_eq!(
short(decode_app_primitive(
&[0x63, 0xAB],
0,
app_tag::OCTET_STRING,
W
)),
(4, 2)
);
}
#[test]
fn an_application_boolean_has_no_contents_to_read() {
use crate::tags::app_tag;
for data in [&[0x11, 0x21, 0x05][..], &[0x10], &[0x21, 0x05]] {
assert_eq!(
decoding(decode_app_primitive(data, 0, app_tag::BOOLEAN, W)),
(
0,
"Thing: an application-tagged BOOLEAN has no contents to read".into()
),
"{data:02X?}"
);
}
}
#[test]
fn fixed_size_contents_check_their_length_first() {
assert_eq!(
decode_ctx_fixed(&[0x3C, 0x0C, 0x00, 0x00, 0x00], 0, 3, 4, "Time", W).unwrap(),
(&[0x0C, 0x00, 0x00, 0x00][..], 5)
);
assert_eq!(
decoding(decode_ctx_fixed(
&[0x3D, 0x05, 0x0C, 0x00, 0x00],
0,
3,
4,
"Time",
W
)),
(
0,
"Thing: [3] Time has 5 contents octets, expected 4".into()
)
);
assert_eq!(
short(decode_ctx_fixed(
&[0x3C, 0x0C, 0x00, 0x00],
0,
3,
4,
"Time",
W
)),
(5, 4)
);
assert_eq!(
decoding(decode_ctx_fixed(&[0x2C, 0, 0, 0, 0], 0, 3, 4, "Time", W)),
(0, "Thing: expected context tag [3] Time".into())
);
assert_eq!(
decoding(decode_ctx_real(&[0x5D, 0x05, 0x42, 0x90, 0x00], 0, 5, W)),
(
0,
"Thing: [5] REAL has 5 contents octets, expected 4".into()
)
);
assert_eq!(
short(decode_ctx_real(&[0x5C, 0x42, 0x90, 0x00], 0, 5, W)),
(5, 4)
);
}
#[test]
fn application_unsigned_values_narrow_to_their_width() {
assert_eq!(
decode_app_unsigned::<u8>(&[0x22, 0x00, 0xFF], 0, W).unwrap(),
(255, 3)
);
assert_eq!(
decoding(decode_app_unsigned::<u8>(&[0xAA, 0x22, 0x01, 0x00], 1, W)),
(2, "Thing: Unsigned exceeds u8".into())
);
assert_eq!(
decoding(decode_app_unsigned::<u16>(&[0x23, 0x01, 0x00, 0x00], 0, W)),
(1, "Thing: Unsigned exceeds u16".into())
);
assert_eq!(
decode_app_unsigned::<u32>(&[0x24, 0xFF, 0xFF, 0xFF, 0xFF], 0, W).unwrap(),
(u32::MAX, 5)
);
assert_eq!(
decoding(decode_app_unsigned::<u32>(&[0x25, 5, 1, 0, 0, 0, 0], 0, W)),
(2, "Thing: Unsigned exceeds u32".into())
);
}
#[test]
fn application_enumerated_items_narrow_and_canonical_ones_refuse_padding() {
assert_eq!(
decode_app_enumerated::<u16>(&[0x92, 0xFF, 0xFF], 0, W).unwrap(),
(0xFFFF, 3)
);
assert_eq!(
decoding(decode_app_enumerated::<u16>(&[0x93, 1, 0, 0], 0, W)),
(1, "Thing: ENUMERATED exceeds u16".into())
);
assert_eq!(
decode_app_enumerated::<u16>(&[0x92, 0, 7], 0, W).unwrap(),
(7, 3)
);
assert_eq!(
decode_app_canonical_enumerated::<u16>(&[0x91, 7], 0, W).unwrap(),
(7, 2)
);
assert_eq!(
decoding(decode_app_canonical_enumerated::<u16>(
&[0xAA, 0x92, 0, 7],
1,
W
)),
(
1,
"Thing: must use the shortest Unsigned/Enumerated encoding".into()
)
);
assert_eq!(
decoding(decode_app_canonical_enumerated::<u16>(
&[0x93, 1, 0, 0],
0,
W
)),
(1, "Thing: ENUMERATED exceeds u16".into())
);
assert_eq!(
decoding(decode_app_canonical_enumerated::<u16>(&[0x21, 7], 0, W)),
(0, "Thing: expected application-tagged ENUMERATED".into())
);
assert_eq!(
short(decode_app_enumerated::<u16>(&[0x92, 0], 0, W)),
(3, 2)
);
assert_eq!(
short(decode_app_canonical_enumerated::<u16>(&[0x92, 0], 0, W)),
(3, 2)
);
}
#[test]
fn fixed_size_application_items_check_their_length_first() {
let device_1 = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
assert_eq!(
decode_app_object_id(&[0xC4, 0x02, 0x00, 0x00, 0x01], 0, W).unwrap(),
(device_1, 5)
);
assert_eq!(
decoding(decode_app_object_id(&[0xC3, 0x02, 0x00, 0x00], 0, W)),
(
0,
"Thing: BACnetObjectIdentifier has 3 contents octets, expected 4".into()
)
);
assert_eq!(
decoding(decode_app_object_id(&[0xC5, 0x05, 0x02, 0x00], 0, W)),
(
0,
"Thing: BACnetObjectIdentifier has 5 contents octets, expected 4".into()
)
);
assert_eq!(
short(decode_app_object_id(&[0xC4, 0x02, 0x00], 0, W)),
(5, 3)
);
for wrong in [
[0x0C, 0x02, 0x00, 0x00, 0x01],
[0x24, 0x02, 0x00, 0x00, 0x01],
] {
assert_eq!(
decoding(decode_app_object_id(&wrong, 0, W)),
(
0,
"Thing: expected application-tagged BACnetObjectIdentifier".into()
)
);
}
assert_eq!(
decode_app_fixed(&[0xA4, 0x7E, 0x0A, 0x03, 0xFF], 0, 10, 4, W).unwrap(),
(&[0x7E, 0x0A, 0x03, 0xFF][..], 5)
);
assert_eq!(
decoding(decode_app_fixed(&[0xA3, 0x7E, 0x0A, 0x03], 0, 10, 4, W)),
(0, "Thing: Date has 3 contents octets, expected 4".into())
);
assert_eq!(
decoding(decode_app_fixed(&[0x11], 0, 1, 1, W)),
(
0,
"Thing: an application-tagged BOOLEAN has no contents to read".into()
)
);
}