use std::ops::Range;
use bacnet_transport::bip6::{decode_bvll6, format_vmac_text, Bvll6};
use bacnet_transport::bvll::{self, BvlcFunction, BvlcResultCode, Bvll};
use bacnet_transport::mstp::codec::{decode_frame, decode_frame_in_place, FrameType};
use bacnet_transport::sc_frame::{
decode_sc_bvlc_result, decode_sc_message, is_broadcast_vmac, ScBvlcResult, ScFunction,
};
use bytes::Bytes;
use super::decode_npdu_layer;
use crate::core::target::{hex, routed_station};
fn routed(network: u16, mac: &[u8]) -> String {
routed_station(network, mac).unwrap_or_else(|| format!("{network}:*"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Framing {
Bip,
Bip6,
#[cfg_attr(not(feature = "sc-tls"), allow(dead_code))]
Sc,
Mstp,
}
impl Framing {
pub fn label(self) -> &'static str {
match self {
Self::Bip => "BIP",
Self::Bip6 => "BIP6",
Self::Sc => "SC",
Self::Mstp => "MSTP",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FrameSummary {
pub link: String,
pub broadcast: bool,
pub link_source: Option<String>,
pub link_destination: Option<String>,
pub snet: Option<u16>,
pub dnet: Option<u16>,
pub network_message: Option<String>,
pub pdu: Option<String>,
pub invoke_id: Option<u8>,
pub service: Option<String>,
pub problem: Option<String>,
pub device: Option<u32>,
pub routed_source: Option<String>,
pub routed_destination: Option<String>,
}
pub fn kebab(name: impl std::fmt::Display) -> String {
let mut out = String::new();
let mut lower_before = false;
for ch in name.to_string().chars() {
if ch.is_ascii_uppercase() && lower_before {
out.push('-');
}
lower_before = ch.is_ascii_lowercase() || ch.is_ascii_digit();
out.push(match ch {
'_' => '-',
ch => ch.to_ascii_lowercase(),
});
}
out
}
pub(super) struct Link {
pub(super) name: String,
pub(super) broadcast: bool,
pub(super) source: Option<String>,
pub(super) destination: Option<String>,
pub(super) problem: Option<String>,
pub(super) npdu: Option<Npdu>,
}
pub(super) enum Npdu {
At(Range<usize>),
Encoded {
wire: Range<usize>,
data: Vec<u8>,
},
}
fn offset_in(frame: &[u8], part: &[u8]) -> usize {
(part.as_ptr() as usize).saturating_sub(frame.as_ptr() as usize)
}
pub(super) fn link(framing: Framing, frame: &[u8]) -> Link {
let mut link = Link {
name: String::new(),
broadcast: false,
source: None,
destination: None,
problem: None,
npdu: None,
};
let refused = match framing {
Framing::Bip => bip(frame, &mut link),
Framing::Bip6 => bip6(frame, &mut link),
Framing::Sc => sc(frame, &mut link),
Framing::Mstp => mstp(frame, &mut link),
};
if let Err(error) = refused {
if link.name.is_empty() {
link.name = "malformed".into();
}
link.problem = Some(format!("malformed: {error}"));
}
link
}
fn bip(frame: &[u8], link: &mut Link) -> Result<(), String> {
let message = bvll::decode_bvll(frame).map_err(|e| e.to_string())?;
link.name = kebab(message.function);
link.broadcast = matches!(
message.function,
BvlcFunction::ORIGINAL_BROADCAST_NPDU
| BvlcFunction::FORWARDED_NPDU
| BvlcFunction::DISTRIBUTE_BROADCAST_TO_NETWORK
);
match message.message().map_err(|e| e.to_string())? {
Bvll::BvlcResult(code) if code != BvlcResultCode::SUCCESSFUL_COMPLETION => {
link.problem = Some(format!("NAK {}", kebab(code)));
}
parsed => {
if let Some(npdu) = parsed.npdu() {
let start = 4 + offset_in(&message.payload, npdu);
link.npdu = Some(Npdu::At(start..start + npdu.len()));
}
}
}
Ok(())
}
fn bip6(frame: &[u8], link: &mut Link) -> Result<(), String> {
let message = decode_bvll6(frame).map_err(|e| e.to_string())?;
link.name = kebab(message.function());
link.source = Some(format_vmac_text(message.source_vmac()));
link.destination = message.destination_vmac().map(format_vmac_text);
link.broadcast = matches!(
message,
Bvll6::OriginalBroadcastNpdu { .. }
| Bvll6::ForwardedNpdu { .. }
| Bvll6::DistributeBroadcastToNetwork { .. }
);
if let Bvll6::BvlcResult { code, .. } = message {
if code.to_raw() != 0 {
link.problem = Some(format!("NAK {}", kebab(code)));
}
}
if let Some(npdu) = message.npdu() {
let start = offset_in(frame, npdu);
link.npdu = Some(Npdu::At(start..start + npdu.len()));
}
Ok(())
}
fn sc(frame: &[u8], link: &mut Link) -> Result<(), String> {
let message = decode_sc_message(frame).map_err(|e| e.to_string())?;
link.name = kebab(format!("{:?}", message.function));
link.source = message.originating_vmac.map(|v| hex(&v));
link.destination = message.destination_vmac.map(|v| hex(&v));
link.broadcast = message
.destination_vmac
.is_some_and(|vmac| is_broadcast_vmac(&vmac));
match message.function {
ScFunction::EncapsulatedNpdu => {
let start = frame.len() - message.payload.len();
link.npdu = Some(Npdu::At(start..frame.len()));
}
ScFunction::Result => {
if let Ok(ScBvlcResult::Nak { error_code, .. }) = decode_sc_bvlc_result(&message) {
link.problem = Some(format!("NAK {error_code}"));
}
}
_ => {}
}
Ok(())
}
fn mstp(frame: &[u8], link: &mut Link) -> Result<(), String> {
if frame.len() >= 5 {
let frame_type = FrameType::from_raw(frame[2]);
link.name = frame_type
.name()
.map_or_else(|| format!("type {}", frame_type.to_raw()), kebab);
link.destination = Some(format!("mstp:{}", frame[3]));
link.source = Some(format!("mstp:{}", frame[4]));
link.broadcast = frame[3] == 255;
}
let frame_type = FrameType::from_raw(*frame.get(2).ok_or("shorter than a header")?);
let carries_npdu = matches!(
frame_type,
FrameType::BACNET_DATA_EXPECTING_REPLY
| FrameType::BACNET_DATA_NOT_EXPECTING_REPLY
| FrameType::BACNET_EXTENDED_DATA_EXPECTING_REPLY
| FrameType::BACNET_EXTENDED_DATA_NOT_EXPECTING_REPLY
);
if frame_type.is_cobs() {
let mut copy = frame.to_vec();
let (decoded, _) = decode_frame_in_place(&mut copy).map_err(|e| e.to_string())?;
if carries_npdu {
link.npdu = Some(Npdu::Encoded {
wire: 8..frame.len().saturating_sub(5).max(8),
data: decoded.data.to_vec(),
});
}
return Ok(());
}
let (decoded, _) = decode_frame(frame).map_err(|e| e.to_string())?;
if carries_npdu && !decoded.data.is_empty() {
let start = offset_in(frame, decoded.data);
link.npdu = Some(Npdu::At(start..start + decoded.data.len()));
}
Ok(())
}
pub fn summarize_frame(framing: Framing, frame: &[u8]) -> FrameSummary {
let link = link(framing, frame);
let mut summary = FrameSummary {
link: link.name,
broadcast: link.broadcast,
link_source: link.source,
link_destination: link.destination,
problem: link.problem,
..FrameSummary::default()
};
let npdu = match link.npdu {
Some(Npdu::At(range)) => Bytes::copy_from_slice(&frame[range]),
Some(Npdu::Encoded { data, .. }) => Bytes::from(data),
None => return summary,
};
if let Ok(view) = bacnet_npdu::Npdu::decode(&npdu) {
summary.routed_source = view.source.map(|a| routed(a.network, a.mac));
summary.routed_destination = view.destination.map(|a| routed(a.network, a.mac));
}
match decode_npdu_layer(npdu) {
Ok(npdu) => {
summary.broadcast |= npdu.broadcast;
summary.snet = npdu.source_network;
summary.dnet = npdu.dest_network;
summary.network_message = npdu
.network_message_type
.map(|kind| kebab(bacnet_npdu::NetworkMessageType::from_raw(kind)));
if let Some(apdu) = npdu.apdu {
summary.pdu = Some(apdu.pdu_type);
summary.invoke_id = apdu.invoke_id;
summary.service = apdu.service.map(kebab);
summary.problem = apdu.problem.map(kebab);
summary.device = apdu.device;
}
}
Err(error) => summary.problem = Some(format!("malformed: {error}")),
}
summary
}
#[cfg(test)]
#[path = "frame_tests.rs"]
mod tests;