use bacnet_npdu::{Address, EncodeError, MAX_MAC_LEN, NetworkMessageType, NetworkPriority};
use bacnet_types::MacAddr;
use bacnet_types::error::{Error, RangeError};
use bytes::{Bytes, BytesMut};
mod decode_error;
pub use decode_error::NpduDecodeError;
pub use bacnet_npdu as view;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NpduAddress {
pub network: u16,
pub mac_address: MacAddr,
}
impl NpduAddress {
fn view(&self) -> Address<'_> {
Address {
network: self.network,
mac: &self.mac_address,
}
}
}
impl From<Address<'_>> for NpduAddress {
fn from(address: Address<'_>) -> Self {
Self {
network: address.network,
mac_address: MacAddr::from_slice(address.mac),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Npdu {
pub expecting_reply: bool,
pub priority: NetworkPriority,
pub destination: Option<NpduAddress>,
pub source: Option<NpduAddress>,
pub hop_count: u8,
pub message_type: Option<NetworkMessageType>,
pub vendor_id: Option<u16>,
pub payload: Bytes,
}
impl Npdu {
pub fn is_network_message(&self) -> bool {
self.message_type.is_some()
}
pub fn header_from(view: &view::Npdu<'_>) -> Self {
Self {
expecting_reply: view.expecting_reply,
priority: view.priority,
destination: view.destination.map(NpduAddress::from),
source: view.source.map(NpduAddress::from),
hop_count: view.hop_count,
message_type: view.message_type,
vendor_id: view.vendor_id,
payload: Bytes::new(),
}
}
fn view(&self) -> bacnet_npdu::Npdu<'_> {
bacnet_npdu::Npdu {
expecting_reply: self.expecting_reply,
priority: self.priority,
destination: self.destination.as_ref().map(NpduAddress::view),
source: self.source.as_ref().map(NpduAddress::view),
hop_count: self.hop_count,
message_type: self.message_type,
vendor_id: self.vendor_id,
payload: &self.payload,
payload_offset: 0,
}
}
}
impl Default for Npdu {
fn default() -> Self {
Self {
expecting_reply: false,
priority: NetworkPriority::Normal,
destination: None,
source: None,
hop_count: 255,
message_type: None,
vendor_id: None,
payload: Bytes::new(),
}
}
}
pub fn encode_npdu(buf: &mut BytesMut, npdu: &Npdu) -> Result<(), Error> {
let view = npdu.view();
let start = buf.len();
buf.resize(start + view.encoded_len(), 0);
match view.encode_into(&mut buf[start..]) {
Ok(_) => Ok(()),
Err(refused) => {
buf.truncate(start);
Err(encode_error(npdu, refused))
}
}
}
fn encode_error(npdu: &Npdu, refused: EncodeError) -> Error {
let destination = npdu.destination.as_ref();
let source = npdu.source.as_ref();
match refused {
EncodeError::ReservedNetwork if destination.is_some_and(|d| d.network == 0) => {
RangeError::new("NPDU DNET must not be 0")
}
EncodeError::ReservedNetwork => RangeError::new("NPDU SNET must be 1..65534, got {}")
.with_value(source.map_or(0, |s| s.network).into()),
EncodeError::EmptySource => RangeError::new("NPDU SLEN must not be 0"),
EncodeError::AddressTooLong => {
let (label, address) = match destination {
Some(d) if d.mac_address.len() > MAX_MAC_LEN => (
"NPDU destination address of {} octets exceeds the {}-octet limit",
d,
),
_ => (
"NPDU source address of {} octets exceeds the {}-octet limit",
source.expect("an over-long address is one of the two"),
),
};
RangeError::new(label).with_values(address.mac_address.len() as u64, MAX_MAC_LEN as u64)
}
_ => RangeError::new("NPDU encoding refused"),
}
.into()
}
pub fn decode_npdu(data: Bytes) -> Result<Npdu, NpduDecodeError> {
#[cfg(feature = "std")]
if let Some(&control) = data.get(1)
&& control & 0x50 != 0
{
tracing::warn!(
control_byte = control,
"NPDU control byte has reserved bits set (bits 4 or 6)"
);
}
let view = match bacnet_npdu::Npdu::decode(&data) {
Ok(view) => view,
Err(bacnet_npdu::DecodeError::AddressTooLong {
field,
length,
dnet,
source,
}) => {
return Err(NpduDecodeError::AddressTooLong {
field,
length,
dnet,
source: source.map(NpduAddress::from),
});
}
Err(bacnet_npdu::DecodeError::Malformed(fault)) => {
return Err(NpduDecodeError::Malformed(fault));
}
};
let mut npdu = Npdu::header_from(&view);
let offset = view.payload_offset;
let mut payload = data;
bytes::Buf::advance(&mut payload, offset);
npdu.payload = payload;
Ok(npdu)
}
#[cfg(test)]
mod address_bound_tests;
#[cfg(test)]
mod tests {
use super::*;
use bacnet_npdu::Malformed;
fn encode_to_vec(npdu: &Npdu) -> Vec<u8> {
let mut buf = BytesMut::with_capacity(64);
encode_npdu(&mut buf, npdu).unwrap();
buf.to_vec()
}
#[test]
fn minimal_local_apdu() {
let npdu = Npdu {
payload: Bytes::from_static(&[0x10, 0x08]), ..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded, vec![0x01, 0x00, 0x10, 0x08]);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn expecting_reply_flag() {
let npdu = Npdu {
expecting_reply: true,
payload: Bytes::from_static(&[0xAA]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1], 0x04); let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert!(decoded.expecting_reply);
}
#[test]
fn priority_encoding() {
for (prio, expected_bits) in [
(NetworkPriority::Normal, 0x00),
(NetworkPriority::Urgent, 0x01),
(NetworkPriority::CriticalEquipment, 0x02),
(NetworkPriority::LifeSafety, 0x03),
] {
let npdu = Npdu {
priority: prio,
payload: Bytes::new(),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x03, expected_bits);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.priority, prio);
}
}
#[test]
fn destination_only_round_trip() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
}),
hop_count: 254,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x20, 0x20);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn destination_broadcast() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::new(),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded.len(), 8);
assert_eq!(&encoded[2..4], &[0xFF, 0xFF]);
assert_eq!(encoded[4], 0);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn source_only_round_trip() {
let npdu = Npdu {
source: Some(NpduAddress {
network: 500,
mac_address: MacAddr::from_slice(&[0x01]),
}),
payload: Bytes::from_static(&[0x30, 0x01, 0x0C]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x08, 0x08);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn source_and_destination_round_trip() {
let npdu = Npdu {
expecting_reply: true,
destination: Some(NpduAddress {
network: 2000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
}),
source: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
}),
hop_count: 250,
payload: Bytes::from_static(&[0x00, 0x05, 0x01, 0x0C]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1], 0x2C);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn network_message_round_trip() {
let npdu = Npdu {
message_type: Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK),
payload: Bytes::from_static(&[0x03, 0xE8]), ..Default::default()
};
let encoded = encode_to_vec(&npdu);
assert_eq!(encoded[1] & 0x80, 0x80);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn proprietary_network_message_round_trip() {
let npdu = Npdu {
message_type: Some(NetworkMessageType::from_raw(0x80)), vendor_id: Some(999),
payload: Bytes::from_static(&[0xDE, 0xAD]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn wire_format_who_is_broadcast() {
let wire = [0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF, 0x10, 0x08];
let decoded = decode_npdu(Bytes::copy_from_slice(&wire)).unwrap();
assert!(!decoded.is_network_message());
assert!(!decoded.expecting_reply);
assert_eq!(decoded.priority, NetworkPriority::Normal);
assert_eq!(
decoded.destination,
Some(NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::new(),
})
);
assert!(decoded.source.is_none());
assert_eq!(decoded.hop_count, 255);
assert_eq!(decoded.payload, vec![0x10, 0x08]);
let reencoded = encode_to_vec(&decoded);
assert_eq!(reencoded, wire);
}
#[test]
fn decode_too_short() {
assert_eq!(
decode_npdu(Bytes::new()),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
assert_eq!(
decode_npdu(Bytes::from_static(&[0x01])),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn decode_wrong_version() {
assert_eq!(
decode_npdu(Bytes::from_static(&[0x02, 0x00])),
Err(NpduDecodeError::Malformed(Malformed::Version))
);
}
#[test]
fn decode_truncated_destination() {
assert_eq!(
decode_npdu(Bytes::from_static(&[0x01, 0x20, 0xFF])),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn decode_truncated_source() {
assert_eq!(
decode_npdu(Bytes::from_static(&[0x01, 0x08, 0x00])),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn npdu_network_zero() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0,
mac_address: MacAddr::from_slice(&[0x01]),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let mut buf = BytesMut::new();
let result = encode_npdu(&mut buf, &npdu);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("DNET"), "got: {err}");
}
#[test]
fn npdu_network_fffe() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0xFFFE,
mac_address: MacAddr::from_slice(&[0x01, 0x02]),
}),
hop_count: 200,
payload: Bytes::from_static(&[0xAA]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFE);
assert_eq!(decoded.hop_count, 200);
}
#[test]
fn npdu_hop_count_zero() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::new(),
}),
hop_count: 0,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.hop_count, 0);
}
#[test]
fn npdu_source_with_empty_mac() {
let npdu = Npdu {
source: Some(NpduAddress {
network: 500,
mac_address: MacAddr::new(),
}),
payload: Bytes::from_static(&[0xBB]),
..Default::default()
};
let mut buf = BytesMut::new();
let result = encode_npdu(&mut buf, &npdu);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("SLEN"), "got: {err}");
}
#[test]
fn npdu_destination_dlen_zero_broadcast_accepted() {
let npdu = Npdu {
destination: Some(NpduAddress {
network: 0xFFFF,
mac_address: MacAddr::new(),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFF);
assert!(decoded.destination.as_ref().unwrap().mac_address.is_empty());
}
#[test]
fn npdu_destination_truncated_mac() {
let data = [0x01, 0x20, 0x03, 0xE8, 0x06, 0x01, 0x02];
assert_eq!(
decode_npdu(Bytes::copy_from_slice(&data)),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn npdu_source_truncated_mac() {
let data = [0x01, 0x08, 0x01, 0xF4, 0x04, 0x01]; assert_eq!(
decode_npdu(Bytes::copy_from_slice(&data)),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn npdu_missing_hop_count() {
let data = [0x01, 0x20, 0xFF, 0xFF, 0x00];
assert_eq!(
decode_npdu(Bytes::copy_from_slice(&data)),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn npdu_network_message_truncated_type() {
let data = [0x01, 0x80]; assert_eq!(
decode_npdu(Bytes::copy_from_slice(&data)),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn npdu_proprietary_message_truncated_vendor() {
let data = [0x01, 0x80, 0x80]; assert_eq!(
decode_npdu(Bytes::copy_from_slice(&data)),
Err(NpduDecodeError::Malformed(Malformed::Truncated))
);
}
#[test]
fn npdu_all_flags_round_trip() {
let npdu = Npdu {
expecting_reply: true,
priority: NetworkPriority::LifeSafety,
destination: Some(NpduAddress {
network: 2000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
}),
source: Some(NpduAddress {
network: 1000,
mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
}),
hop_count: 127,
payload: Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert_eq!(decoded, npdu);
}
#[test]
fn npdu_empty_payload() {
let npdu = Npdu {
payload: Bytes::new(),
..Default::default()
};
let encoded = encode_to_vec(&npdu);
let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
assert!(decoded.payload.is_empty());
}
#[test]
fn reject_snet_zero() {
let npdu = Npdu {
source: Some(NpduAddress {
network: 0,
mac_address: MacAddr::from_slice(&[0x01]),
}),
payload: Bytes::from_static(&[0x10, 0x08]),
..Default::default()
};
let mut buf = BytesMut::new();
let result = encode_npdu(&mut buf, &npdu);
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("SNET"), "got: {err}");
}
#[test]
fn reserved_bits_warning_still_decodes() {
let mut data = vec![0x01, 0x40]; data.extend_from_slice(&[0x10, 0x08]);
let result = decode_npdu(Bytes::copy_from_slice(&data));
assert!(
result.is_ok(),
"reserved bits should not cause decode failure"
);
}
}