use super::*;
use crate::database::ObjectDatabase;
use crate::property_metadata::PropertyWriteCapability;
use bacnet_encoding::constructed::{decode_host_n_port, encode_host_n_port};
use bacnet_types::bip_port::{BipPortMode, ForeignDeviceChanges, ForeignRegistration};
use bacnet_types::constructed::{BACnetHostAddress, BACnetHostNPort};
use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier as P};
use bacnet_types::error::RemoteError;
use bytes::BytesMut;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::{Arc, Mutex};
const REFUSED: [u8; 4] = [10, 0, 0, 99];
struct Registration {
bbmd: Mutex<BACnetHostNPort>,
ttl: Mutex<u16>,
checked: Mutex<Vec<ForeignDeviceChanges>>,
fail: Mutex<bool>,
}
impl Registration {
fn new() -> Arc<Self> {
Arc::new(Self {
bbmd: Mutex::new(host([10, 0, 0, 1])),
ttl: Mutex::new(300),
checked: Mutex::new(Vec::new()),
fail: Mutex::new(false),
})
}
}
fn refusal() -> Error {
Error::protocol(ErrorClass::PROPERTY, ErrorCode::VALUE_OUT_OF_RANGE)
}
fn is(error: &Error, expected: ErrorCode) -> bool {
matches!(error, Error::Remote(RemoteError::Error { class, code })
if *class == ErrorClass::PROPERTY && *code == expected)
}
impl ForeignRegistration for Registration {
fn bbmd(&self) -> BACnetHostNPort {
self.bbmd.lock().unwrap().clone()
}
fn subscription_lifetime(&self) -> u16 {
*self.ttl.lock().unwrap()
}
fn check(&self, changes: &ForeignDeviceChanges) -> Result<(), Error> {
self.checked.lock().unwrap().push(changes.clone());
let refused = changes
.bbmd
.as_ref()
.is_some_and(|bbmd| bbmd.host == BACnetHostAddress::Ip(IpAddr::V4(REFUSED.into())));
if refused || changes.subscription_lifetime == Some(0) {
return Err(refusal());
}
Ok(())
}
fn activate(&self, changes: &ForeignDeviceChanges) -> Result<(), Error> {
if *self.fail.lock().unwrap() {
return Err(refusal());
}
self.check(changes)?;
if let Some(bbmd) = &changes.bbmd {
*self.bbmd.lock().unwrap() = bbmd.clone();
}
if let Some(ttl) = changes.subscription_lifetime {
*self.ttl.lock().unwrap() = ttl;
}
Ok(())
}
fn can_change(&self) -> bool {
true
}
}
fn oid() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::NETWORK_PORT, 1).unwrap()
}
fn host(ip: [u8; 4]) -> BACnetHostNPort {
BACnetHostNPort {
host: BACnetHostAddress::Ip(IpAddr::V4(Ipv4Addr::from(ip))),
port: 0xBAC0,
}
}
fn foreign_port(registration: &Arc<Registration>, activation: bool) -> (ObjectDatabase, Arc<()>) {
let mut db = ObjectDatabase::new();
let config = BipPortConfig {
ip_address: [127, 0, 0, 1],
..Default::default()
};
db.add(Box::new(
NetworkPortObject::new_bip(1, "NP", config).unwrap(),
))
.unwrap();
let (_, lease) = db
.reserve_bip_port_internal(oid(), [127, 0, 0, 1], 47808)
.unwrap();
let registration: Arc<dyn ForeignRegistration> = registration.clone();
let mode = BipPortMode::Foreign { registration };
db.publish_bip_port_internal(oid(), [127, 0, 0, 1], 47808, 1476, mode)
.unwrap();
db.set_network_port_activation_internal(oid(), activation)
.unwrap();
(db, lease)
}
fn read(db: &ObjectDatabase, property: P) -> PropertyValue {
db.get(&oid())
.unwrap()
.read_property(property, None)
.unwrap()
}
fn bbmd_read(db: &ObjectDatabase) -> BACnetHostNPort {
let PropertyValue::ApplicationData(bytes) = read(db, P::FD_BBMD_ADDRESS) else {
panic!("an encoded BACnetHostNPort");
};
decode_host_n_port(&bytes, 0).unwrap().0
}
fn write(db: &mut ObjectDatabase, property: P, value: PropertyValue) -> Result<(), Error> {
db.get_mut(&oid())
.unwrap()
.write_property(property, None, value, None)
}
fn written(address: &BACnetHostNPort) -> PropertyValue {
let mut bytes = BytesMut::new();
encode_host_n_port(&mut bytes, address).unwrap();
PropertyValue::ApplicationData(bytes.to_vec())
}
fn pending(db: &ObjectDatabase) -> bool {
read(db, P::CHANGES_PENDING) == PropertyValue::Boolean(true)
}
#[test]
fn written_values_read_back_while_the_registration_keeps_the_old_ones() {
let registration = Registration::new();
let (mut db, _lease) = foreign_port(®istration, true);
assert!(!pending(&db));
write(&mut db, P::FD_BBMD_ADDRESS, written(&host([10, 0, 1, 1]))).unwrap();
write(
&mut db,
P::FD_SUBSCRIPTION_LIFETIME,
PropertyValue::Unsigned(120),
)
.unwrap();
assert!(pending(&db));
assert_eq!(bbmd_read(&db), host([10, 0, 1, 1]));
assert_eq!(
read(&db, P::FD_SUBSCRIPTION_LIFETIME),
PropertyValue::Unsigned(120)
);
assert_eq!(registration.bbmd(), host([10, 0, 0, 1]));
assert_eq!(registration.subscription_lifetime(), 300);
let last = registration
.checked
.lock()
.unwrap()
.last()
.cloned()
.unwrap();
assert_eq!(last.bbmd, Some(host([10, 0, 1, 1])));
assert_eq!(last.subscription_lifetime, Some(120));
assert_eq!(db.activate_network_port_changes().unwrap(), 1);
assert!(!pending(&db));
assert_eq!(registration.bbmd(), host([10, 0, 1, 1]));
assert_eq!(registration.subscription_lifetime(), 120);
assert_eq!(bbmd_read(&db), host([10, 0, 1, 1]));
}
#[test]
fn a_refused_value_changes_neither_the_property_nor_the_registration() {
let registration = Registration::new();
let (mut db, _lease) = foreign_port(®istration, true);
let error = write(&mut db, P::FD_BBMD_ADDRESS, written(&host(REFUSED))).unwrap_err();
assert!(is(&error, ErrorCode::VALUE_OUT_OF_RANGE), "{error:?}");
let error = write(
&mut db,
P::FD_SUBSCRIPTION_LIFETIME,
PropertyValue::Unsigned(0),
)
.unwrap_err();
assert!(is(&error, ErrorCode::VALUE_OUT_OF_RANGE), "{error:?}");
let error = write(
&mut db,
P::FD_SUBSCRIPTION_LIFETIME,
PropertyValue::Unsigned(65_536),
)
.unwrap_err();
assert!(is(&error, ErrorCode::VALUE_OUT_OF_RANGE), "{error:?}");
let error = write(
&mut db,
P::FD_SUBSCRIPTION_LIFETIME,
PropertyValue::Boolean(true),
)
.unwrap_err();
assert!(is(&error, ErrorCode::INVALID_DATA_TYPE), "{error:?}");
let error = write(
&mut db,
P::FD_BBMD_ADDRESS,
PropertyValue::ApplicationData(vec![0x0E, 0x1C]),
)
.unwrap_err();
assert!(is(&error, ErrorCode::INVALID_DATA_ENCODING), "{error:?}");
let error = db
.get_mut(&oid())
.unwrap()
.write_property(
P::FD_SUBSCRIPTION_LIFETIME,
Some(1),
PropertyValue::Unsigned(60),
None,
)
.unwrap_err();
assert!(is(&error, ErrorCode::PROPERTY_IS_NOT_AN_ARRAY), "{error:?}");
assert!(!pending(&db));
assert_eq!(bbmd_read(&db), host([10, 0, 0, 1]));
assert_eq!(
read(&db, P::FD_SUBSCRIPTION_LIFETIME),
PropertyValue::Unsigned(300)
);
assert_eq!(registration.bbmd(), host([10, 0, 0, 1]));
assert_eq!(registration.subscription_lifetime(), 300);
}
#[test]
fn a_refused_activation_keeps_the_values_pending() {
let registration = Registration::new();
let (mut db, _lease) = foreign_port(®istration, true);
write(
&mut db,
P::FD_SUBSCRIPTION_LIFETIME,
PropertyValue::Unsigned(90),
)
.unwrap();
db.check_network_port_changes().unwrap();
*registration.fail.lock().unwrap() = true;
assert!(db.activate_network_port_changes().is_err());
assert!(pending(&db));
assert_eq!(
read(&db, P::FD_SUBSCRIPTION_LIFETIME),
PropertyValue::Unsigned(90)
);
assert_eq!(registration.subscription_lifetime(), 300);
}
#[test]
fn without_an_activation_owner_the_rows_are_read_only() {
let registration = Registration::new();
let (mut db, _lease) = foreign_port(®istration, false);
for (property, value) in [
(P::FD_BBMD_ADDRESS, written(&host([10, 0, 1, 1]))),
(P::FD_SUBSCRIPTION_LIFETIME, PropertyValue::Unsigned(120)),
] {
let error = write(&mut db, property, value).unwrap_err();
assert!(is(&error, ErrorCode::WRITE_ACCESS_DENIED), "{error:?}");
}
assert!(!pending(&db));
assert!(registration.checked.lock().unwrap().is_empty());
let writable = |db: &ObjectDatabase| {
db.get(&oid())
.unwrap()
.property_metadata()
.iter()
.filter(|row| row.write_capability == PropertyWriteCapability::Always)
.map(|row| row.property_identifier)
.collect::<Vec<_>>()
};
assert!(!writable(&db).contains(&P::FD_BBMD_ADDRESS));
db.set_network_port_activation_internal(oid(), true)
.unwrap();
let rows = writable(&db);
assert!(rows.contains(&P::FD_BBMD_ADDRESS) && rows.contains(&P::FD_SUBSCRIPTION_LIFETIME));
}
#[test]
fn a_fixed_registration_refuses_every_written_value() {
let mut db = ObjectDatabase::new();
db.add(Box::new(
NetworkPortObject::new_bip(
1,
"NP",
BipPortConfig {
ip_address: [127, 0, 0, 1],
..Default::default()
},
)
.unwrap(),
))
.unwrap();
let (_, _lease) = db
.reserve_bip_port_internal(oid(), [127, 0, 0, 1], 47808)
.unwrap();
let mode = BipPortMode::fixed_foreign(host([10, 0, 0, 1]), 300);
db.publish_bip_port_internal(oid(), [127, 0, 0, 1], 47808, 1476, mode)
.unwrap();
db.set_network_port_activation_internal(oid(), true)
.unwrap();
let error = write(
&mut db,
P::FD_SUBSCRIPTION_LIFETIME,
PropertyValue::Unsigned(60),
)
.unwrap_err();
assert!(is(&error, ErrorCode::WRITE_ACCESS_DENIED), "{error:?}");
assert!(!pending(&db));
}