1use std::ops::Range;
10
11use crate::target::{hex, routed_station};
12use bacnet_transport::bip6::{Bvll6, decode_bvll6, format_vmac_text};
13use bacnet_transport::bvll::{self, BvlcFunction, BvlcResultCode, Bvll};
14use bacnet_transport::mstp::codec::{FrameType, decode_frame, decode_frame_in_place};
15use bacnet_transport::sc_frame::{
16 ScBvlcResult, ScFunction, decode_sc_bvlc_result, decode_sc_message, is_broadcast_vmac,
17};
18use bytes::Bytes;
19
20use super::decode_npdu_layer;
21
22fn routed(network: u16, mac: &[u8]) -> String {
26 routed_station(network, mac).unwrap_or_else(|| format!("{network}:*"))
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Framing {
32 Bip,
34 Bip6,
36 Sc,
39 Mstp,
41}
42
43impl Framing {
44 pub fn label(self) -> &'static str {
46 match self {
47 Self::Bip => "BIP",
48 Self::Bip6 => "BIP6",
49 Self::Sc => "SC",
50 Self::Mstp => "MSTP",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct FrameSummary {
58 pub link: String,
60 pub broadcast: bool,
62 pub link_source: Option<String>,
65 pub link_destination: Option<String>,
67 pub snet: Option<u16>,
69 pub dnet: Option<u16>,
71 pub network_message: Option<String>,
73 pub pdu: Option<String>,
75 pub invoke_id: Option<u8>,
77 pub service: Option<String>,
79 pub problem: Option<String>,
82 pub device: Option<u32>,
84 pub routed_source: Option<String>,
86 pub routed_destination: Option<String>,
88}
89
90pub fn kebab(name: impl std::fmt::Display) -> String {
93 let mut out = String::new();
94 let mut lower_before = false;
95 for ch in name.to_string().chars() {
96 if ch.is_ascii_uppercase() && lower_before {
97 out.push('-');
98 }
99 lower_before = ch.is_ascii_lowercase() || ch.is_ascii_digit();
100 out.push(match ch {
101 '_' => '-',
102 ch => ch.to_ascii_lowercase(),
103 });
104 }
105 out
106}
107
108pub struct Link {
110 pub name: String,
113 pub broadcast: bool,
115 pub source: Option<String>,
117 pub destination: Option<String>,
119 pub problem: Option<String>,
121 pub malformed: bool,
124 pub npdu: Option<Npdu>,
127}
128
129pub enum Npdu {
131 At(Range<usize>),
133 Encoded {
135 wire: Range<usize>,
137 data: Vec<u8>,
139 },
140}
141
142fn offset_in(frame: &[u8], part: &[u8]) -> usize {
144 (part.as_ptr() as usize).saturating_sub(frame.as_ptr() as usize)
145}
146
147pub fn link(framing: Framing, frame: &[u8]) -> Link {
149 let mut link = Link {
150 name: String::new(),
151 broadcast: false,
152 source: None,
153 destination: None,
154 problem: None,
155 malformed: false,
156 npdu: None,
157 };
158 let refused = match framing {
159 Framing::Bip => bip(frame, &mut link),
160 Framing::Bip6 => bip6(frame, &mut link),
161 Framing::Sc => sc(frame, &mut link),
162 Framing::Mstp => mstp(frame, &mut link),
163 };
164 if let Err(error) = refused {
165 if link.name.is_empty() {
166 link.name = "malformed".into();
167 }
168 link.problem = Some(format!("malformed: {error}"));
169 link.malformed = true;
170 }
171 link
172}
173
174fn bip(frame: &[u8], link: &mut Link) -> Result<(), String> {
175 let message = bvll::decode_bvll(frame).map_err(|e| e.to_string())?;
176 link.name = kebab(message.function);
177 link.broadcast = matches!(
178 message.function,
179 BvlcFunction::ORIGINAL_BROADCAST_NPDU
180 | BvlcFunction::FORWARDED_NPDU
181 | BvlcFunction::DISTRIBUTE_BROADCAST_TO_NETWORK
182 );
183 match message.message().map_err(|e| e.to_string())? {
184 Bvll::BvlcResult(code) if code != BvlcResultCode::SUCCESSFUL_COMPLETION => {
185 link.problem = Some(format!("NAK {}", kebab(code)));
186 }
187 parsed => {
188 if let Some(npdu) = parsed.npdu() {
189 let start = 4 + offset_in(&message.payload, npdu);
190 link.npdu = Some(Npdu::At(start..start + npdu.len()));
191 }
192 }
193 }
194 Ok(())
195}
196
197fn bip6(frame: &[u8], link: &mut Link) -> Result<(), String> {
198 let message = decode_bvll6(frame).map_err(|e| e.to_string())?;
199 link.name = kebab(message.function());
200 link.source = Some(format_vmac_text(message.source_vmac()));
201 link.destination = message.destination_vmac().map(format_vmac_text);
202 link.broadcast = matches!(
203 message,
204 Bvll6::OriginalBroadcastNpdu { .. }
205 | Bvll6::ForwardedNpdu { .. }
206 | Bvll6::DistributeBroadcastToNetwork { .. }
207 );
208 if let Bvll6::BvlcResult { code, .. } = message
209 && code.to_raw() != 0
210 {
211 link.problem = Some(format!("NAK {}", kebab(code)));
212 }
213 if let Some(npdu) = message.npdu() {
214 let start = offset_in(frame, npdu);
215 link.npdu = Some(Npdu::At(start..start + npdu.len()));
216 }
217 Ok(())
218}
219
220fn sc(frame: &[u8], link: &mut Link) -> Result<(), String> {
221 let message = decode_sc_message(frame).map_err(|e| e.to_string())?;
222 link.name = kebab(format!("{:?}", message.function));
223 link.source = message.originating_vmac.map(|v| hex(&v));
224 link.destination = message.destination_vmac.map(|v| hex(&v));
225 link.broadcast = message
226 .destination_vmac
227 .is_some_and(|vmac| is_broadcast_vmac(&vmac));
228 match message.function {
229 ScFunction::EncapsulatedNpdu => {
230 let start = frame.len() - message.payload.len();
231 link.npdu = Some(Npdu::At(start..frame.len()));
232 }
233 ScFunction::Result => {
234 if let Ok(ScBvlcResult::Nak { error_code, .. }) = decode_sc_bvlc_result(&message) {
235 link.problem = Some(format!("NAK {error_code}"));
236 }
237 }
238 _ => {}
239 }
240 Ok(())
241}
242
243fn mstp(frame: &[u8], link: &mut Link) -> Result<(), String> {
244 if frame.len() >= 5 {
245 let frame_type = FrameType::from_raw(frame[2]);
246 link.name = frame_type
247 .name()
248 .map_or_else(|| format!("type {}", frame_type.to_raw()), kebab);
249 link.destination = Some(format!("mstp:{}", frame[3]));
250 link.source = Some(format!("mstp:{}", frame[4]));
251 link.broadcast = frame[3] == 255;
252 }
253 let frame_type = FrameType::from_raw(*frame.get(2).ok_or("shorter than a header")?);
254 let carries_npdu = matches!(
255 frame_type,
256 FrameType::BACNET_DATA_EXPECTING_REPLY
257 | FrameType::BACNET_DATA_NOT_EXPECTING_REPLY
258 | FrameType::BACNET_EXTENDED_DATA_EXPECTING_REPLY
259 | FrameType::BACNET_EXTENDED_DATA_NOT_EXPECTING_REPLY
260 );
261 if frame_type.is_cobs() {
262 let mut copy = frame.to_vec();
263 let (decoded, _) = decode_frame_in_place(&mut copy).map_err(|e| e.to_string())?;
264 if carries_npdu {
265 link.npdu = Some(Npdu::Encoded {
266 wire: 8..frame.len().saturating_sub(5).max(8),
267 data: decoded.data.to_vec(),
268 });
269 }
270 return Ok(());
271 }
272 let (decoded, _) = decode_frame(frame).map_err(|e| e.to_string())?;
273 if carries_npdu && !decoded.data.is_empty() {
274 let start = offset_in(frame, decoded.data);
275 link.npdu = Some(Npdu::At(start..start + decoded.data.len()));
276 }
277 Ok(())
278}
279
280pub fn summarize_frame(framing: Framing, frame: &[u8]) -> FrameSummary {
283 let link = link(framing, frame);
284 let mut summary = FrameSummary {
285 link: link.name,
286 broadcast: link.broadcast,
287 link_source: link.source,
288 link_destination: link.destination,
289 problem: link.problem,
290 ..FrameSummary::default()
291 };
292 let npdu = match link.npdu {
293 Some(Npdu::At(range)) => Bytes::copy_from_slice(&frame[range]),
294 Some(Npdu::Encoded { data, .. }) => Bytes::from(data),
295 None => return summary,
296 };
297 network_layer(&mut summary, npdu);
298 summary
299}
300
301pub fn summarize_npdu(npdu: &[u8]) -> FrameSummary {
304 let mut summary = FrameSummary::default();
305 network_layer(&mut summary, Bytes::copy_from_slice(npdu));
306 summary
307}
308
309fn network_layer(summary: &mut FrameSummary, npdu: Bytes) {
311 if let Ok(view) = bacnet_npdu::Npdu::decode(&npdu) {
312 summary.routed_source = view.source.map(|a| routed(a.network, a.mac));
313 summary.routed_destination = view.destination.map(|a| routed(a.network, a.mac));
314 }
315 match decode_npdu_layer(npdu) {
316 Ok(npdu) => {
317 summary.broadcast |= npdu.broadcast;
318 summary.snet = npdu.source_network;
319 summary.dnet = npdu.dest_network;
320 summary.network_message = npdu
321 .network_message_type
322 .map(|kind| kebab(bacnet_npdu::NetworkMessageType::from_raw(kind)));
323 if let Some(apdu) = npdu.apdu {
324 summary.pdu = Some(apdu.pdu_type);
325 summary.invoke_id = apdu.invoke_id;
326 summary.service = apdu.service.map(kebab);
327 summary.problem = apdu.problem.map(kebab);
328 summary.device = apdu.device;
329 }
330 }
331 Err(error) => summary.problem = Some(format!("malformed: {error}")),
332 }
333}
334
335#[cfg(test)]
336#[path = "frame_tests.rs"]
337mod tests;