use super::*;
use crate::server::ReadRangeBudget;
use bacnet_types::enums::EngineeringUnits;
use bacnet_types::error::RemoteError;
fn page(
db: &ObjectDatabase,
oid: ObjectIdentifier,
range: Option<RangeSpec>,
cap: usize,
bytes: usize,
) -> Result<ReadRangeAck, ReadRangeFailure> {
let request = ReadRangeRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::LOG_BUFFER,
property_array_index: None,
range,
};
let mut input = BytesMut::new();
request.encode(&mut input).unwrap();
let mut output = BytesMut::from(&b"sentinel"[..]);
let result = handle_read_range_budgeted(
db,
&input,
&mut output,
ReadRangeBudget {
max_returned_items: cap,
max_service_ack_bytes: bytes,
},
);
if result.is_err() {
assert_eq!(&output[..], b"sentinel");
}
result?;
assert!(output.len() - 8 <= bytes);
Ok(ReadRangeAck::decode(&output[8..]).unwrap())
}
#[test]
fn directional_pages_preserve_sparse_wrapped_resident_identity() {
let items = unsigned_items(&[10, 20, 30, 40, 50]);
let ids = vec![
identity(u64::from(u32::MAX), 1),
identity(1, 2),
identity(255, 3),
identity(65536, 4),
identity(9, 5),
];
let (db, oid) = list_db(PropertyIdentifier::LOG_BUFFER, items.clone(), Some(ids));
for cap in [1, 2, 5, 256] {
for kind in 0..3 {
for backwards in [false, true] {
let count = if backwards { -4 } else { 4 };
let range = match kind {
0 => RangeSpec::ByPosition {
reference_index: if backwards { 4 } else { 2 },
count,
},
1 => RangeSpec::BySequenceNumber {
reference_seq: if backwards { 65536 } else { 1 },
count,
},
_ => RangeSpec::ByTime {
reference_time: (DATE, time(if backwards { 5 } else { 1 })),
count,
},
};
let n = cap.min(4);
let expected = if backwards { 4 - n..4 } else { 1..1 + n };
let first =
(kind != 0).then(|| [u64::from(u32::MAX), 1, 255, 65536, 9][expected.start]);
let ack = page(&db, oid, Some(range), cap, 16384).unwrap();
assert_ack(
&ack,
&items[expected.clone()],
(expected.start == 0, expected.end == 5, n < 4),
first,
);
}
}
let n = cap.min(5);
assert_ack(
&page(&db, oid, None, cap, 16384).unwrap(),
&items[..n],
(true, n == 5, n < 5),
None,
);
}
}
#[test]
fn every_page_keeps_the_rules_a_client_checks() {
let items = unsigned_items(&[10, 20, 30, 40, 50]);
let ids = vec![
identity(u64::from(u32::MAX), 1),
identity(1, 2),
identity(255, 3),
identity(65536, 4),
identity(9, 5),
];
let sequences = [u64::from(u32::MAX), 1, 255, 65536, 9, 7];
let (db, oid) = list_db(PropertyIdentifier::LOG_BUFFER, items, Some(ids));
let mut ranges = vec![None];
for count in [1, 2, 5, 9, -1, -2, -5, -9] {
for reference in 0..=6u8 {
ranges.push(Some(RangeSpec::ByPosition {
reference_index: u64::from(reference),
count,
}));
ranges.push(Some(RangeSpec::ByTime {
reference_time: (DATE, time(reference)),
count,
}));
}
for reference_seq in sequences {
ranges.push(Some(RangeSpec::BySequenceNumber {
reference_seq,
count,
}));
}
}
for range in ranges {
for cap in [1, 2, 3, 256] {
let request = ReadRangeRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::LOG_BUFFER,
property_array_index: None,
range: range.clone(),
};
let ack = page(&db, oid, range.clone(), cap, 16384).unwrap();
assert_eq!(
ack.violations(&request),
[],
"{range:?} cap {cap}: {:?}",
ack.result_flags
);
}
}
}
#[test]
fn count_width_255_256_and_default_exact_limit() {
for total in [256, 257] {
let items = vec![PropertyValue::Unsigned(1); total];
let (db, oid) = list_db(PropertyIdentifier::LOG_BUFFER, items.clone(), None);
let unlimited = call(&db, oid, PropertyIdentifier::LOG_BUFFER, None).unwrap();
for n in [1, 255, 256] {
let ack = page(&db, oid, None, n, 16384).unwrap();
let mut encoded = BytesMut::new();
ack.encode(&mut encoded);
assert_eq!(encoded.len(), 12 + if n == 256 { 3 } else { 2 } + n * 2);
assert_ack(
&page(&db, oid, None, usize::MAX, encoded.len()).unwrap(),
&items[..n],
(true, n == total, n < total),
None,
);
if n > 1 {
assert_eq!(
page(&db, oid, None, usize::MAX, encoded.len() - 1)
.unwrap()
.item_count,
(n - 1) as u32
);
}
}
let parity = page(&db, oid, None, usize::MAX, usize::MAX).unwrap();
let mut actual = BytesMut::new();
let mut expected = BytesMut::new();
parity.encode(&mut actual);
unlimited.encode(&mut expected);
assert_eq!(actual, expected);
}
}
#[test]
fn missing_references_empty_lists_and_validation_precede_tiny_budget() {
let (db, oid) = list_db(
PropertyIdentifier::LOG_BUFFER,
unsigned_items(&[1]),
Some(vec![identity(1, 1)]),
);
for range in [
RangeSpec::ByPosition {
reference_index: 2,
count: -1,
},
RangeSpec::BySequenceNumber {
reference_seq: 2,
count: 1,
},
RangeSpec::ByTime {
reference_time: (DATE, time(0)),
count: -1,
},
] {
assert_ack(
&page(&db, oid, Some(range.clone()), 1, 14).unwrap(),
&[],
(false, false, false),
None,
);
assert!(matches!(
page(&db, oid, Some(range), 1, 13),
Err(ReadRangeFailure::Bytes)
));
}
let (empty, empty_oid) = list_db(PropertyIdentifier::LOG_BUFFER, vec![], None);
assert_ack(
&page(&empty, empty_oid, None, 1, 14).unwrap(),
&[],
(false, false, false),
None,
);
for ids in [None, Some(vec![])] {
let (bad, bad_oid) = list_db(PropertyIdentifier::LOG_BUFFER, unsigned_items(&[1]), ids);
for range in [
RangeSpec::BySequenceNumber {
reference_seq: 1,
count: 1,
},
RangeSpec::ByTime {
reference_time: (DATE, time(0)),
count: 1,
},
] {
assert!(matches!(
page(&bad, bad_oid, Some(range), 1, 1),
Err(ReadRangeFailure::Service(Error::Remote(
RemoteError::Error { .. }
)))
));
}
}
for signed_count in [
vec![0x31, 0],
vec![0x33, 0, 0x80, 0],
vec![0x33, 0xff, 0x7f, 0xff],
] {
let mut request = BytesMut::new();
bacnet_encoding::primitives::encode_ctx_object_id(&mut request, 0, &oid);
request.extend_from_slice(&[0x19, 131, 0x3e, 0x21, 1]);
request.extend_from_slice(&signed_count);
request.extend_from_slice(&[0x3f]);
assert!(matches!(
handle_read_range_budgeted(
&db,
&request,
&mut BytesMut::new(),
ReadRangeBudget {
max_returned_items: 1,
max_service_ack_bytes: 1
}
),
Err(ReadRangeFailure::Service(Error::Remote(
RemoteError::Reject { .. }
)))
));
}
assert!(matches!(
page(&empty, oid, None, 1, 1),
Err(ReadRangeFailure::Bytes)
));
let missing_oid = ObjectIdentifier::new(ObjectType::TREND_LOG, 92).unwrap();
assert!(
matches!(page(&db, missing_oid, None, 1, 1), Err(ReadRangeFailure::Service(Error::Remote(RemoteError::Error { code, .. })))
if code == ErrorCode::UNKNOWN_OBJECT)
);
}
#[test]
fn read_range_error_precedence_matches_legacy_before_pagination() {
let (db, oid) = list_db(PropertyIdentifier::LOG_BUFFER, unsigned_items(&[1]), None);
for suffix in [
vec![0x19, 131, 0x29, 0],
vec![0x19, 131, 0x29, 1],
vec![0x19, 8], vec![0x19, 105], vec![0x19, 80], vec![0x19, 85], vec![
0x19, 131, 0x7e, 0xa4, 126, 8, 31, 1, 0xb4, 24, 0, 0, 0, 0x31, 1, 0x7f,
],
] {
let mut request = BytesMut::new();
bacnet_encoding::primitives::encode_ctx_object_id(&mut request, 0, &oid);
request.extend_from_slice(&suffix);
let legacy = handle_read_range(&db, &request, &mut BytesMut::new()).unwrap_err();
let mut output = BytesMut::from(&b"old"[..]);
let ReadRangeFailure::Service(error) = handle_read_range_budgeted(
&db,
&request,
&mut output,
ReadRangeBudget {
max_returned_items: 1,
max_service_ack_bytes: 1,
},
)
.unwrap_err() else {
panic!("budget hid existing error")
};
assert_eq!(error.to_string(), legacy.to_string());
assert_eq!(&output[..], b"old");
}
let mut scalar_db = ObjectDatabase::new();
let scalar = bacnet_objects::analog::AnalogValueObject::new(
1,
"scalar",
EngineeringUnits::DEGREES_CELSIUS,
)
.unwrap();
let scalar_oid = scalar.object_identifier();
scalar_db.add(Box::new(scalar)).unwrap();
let mut request = BytesMut::new();
ReadRangeRequest {
object_identifier: scalar_oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
range: None,
}
.encode(&mut request)
.unwrap();
assert!(
matches!(handle_read_range_budgeted(&scalar_db, &request, &mut BytesMut::new(),
ReadRangeBudget { max_returned_items:1, max_service_ack_bytes:1 }),
Err(ReadRangeFailure::Service(Error::Remote(RemoteError::Error { code, .. }))) if code == ErrorCode::PROPERTY_IS_NOT_A_LIST)
);
}
#[test]
fn byte_overflow_never_skips_or_loses_direction_anchor() {
let items = vec![
PropertyValue::Unsigned(1),
PropertyValue::OctetString(vec![0; 100]),
PropertyValue::Unsigned(3),
];
let (db, oid) = list_db(PropertyIdentifier::LOG_BUFFER, items.clone(), None);
for backwards in [false, true] {
let range = Some(RangeSpec::ByPosition {
reference_index: if backwards { 3 } else { 1 },
count: if backwards { -3 } else { 3 },
});
let ack = page(&db, oid, range, 10, 20).unwrap();
assert_ack(
&ack,
&items[if backwards { 2..3 } else { 0..1 }],
(!backwards, backwards, true),
None,
);
let oversized = Some(RangeSpec::ByPosition {
reference_index: 2,
count: if backwards { -2 } else { 2 },
});
assert!(matches!(
page(&db, oid, oversized, 10, 20),
Err(ReadRangeFailure::Bytes)
));
}
}
#[tokio::test]
async fn read_range_client_pages_all_matches_forward_and_backward() {
use crate::server::BACnetServer;
use bacnet_client::client::BACnetClient;
use std::net::Ipv4Addr;
let items = unsigned_items(&[10, 20, 30, 40, 50]);
let (db, oid) = list_db(PropertyIdentifier::LOG_BUFFER, items.clone(), None);
let mut server = BACnetServer::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.database(db)
.read_range_budget(ReadRangeBudget {
max_returned_items: 2,
..Default::default()
})
.build()
.await
.unwrap();
let mut client = BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.build()
.await
.unwrap();
for backwards in [false, true] {
let mut remaining = 5;
let mut pieces = Vec::new();
while remaining > 0 {
let reference_index = if backwards { remaining } else { 6 - remaining };
let ack = client
.read_range(
server.local_mac(),
oid,
PropertyIdentifier::LOG_BUFFER,
None,
Some(RangeSpec::ByPosition {
reference_index,
count: if backwards {
-(remaining as i32)
} else {
remaining as i32
},
}),
)
.await
.unwrap();
assert!(ack.item_count > 0 && ack.item_count <= 2);
remaining -= u64::from(ack.item_count);
assert_eq!(ack.result_flags.2, remaining > 0);
pieces.push(ack.item_data);
}
if backwards {
pieces.reverse();
}
assert_eq!(pieces.concat(), encoded_items(&items));
}
client.stop().await.unwrap();
server.stop().await.unwrap();
}