Skip to main content

bacnet_ops/decode/
frame.rs

1//! One frame of any link the stack taps, read as far as its list row needs:
2//! the link message, the network layer and the APDU's header.
3//!
4//! The link layers each wrap the NPDU differently, so [`link`] finds the
5//! NPDU and what the link says around it; the network and application
6//! layers are the same everywhere and go through [`super::decode_npdu_layer`].
7//! [`summarize_npdu`] reads an NPDU given without its link.
8
9use 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
22/// A routed address as the capture shows it: a station in the target
23/// syntax, `2:0x05`, and a broadcast on a network (an empty MAC), which
24/// names no station, as `2:*`.
25fn routed(network: u16, mac: &[u8]) -> String {
26    routed_station(network, mac).unwrap_or_else(|| format!("{network}:*"))
27}
28
29/// The link a frame was captured on, which decides how its octets are read.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Framing {
32    /// A B/IP BVLL message, the UDP payload (Annex J).
33    Bip,
34    /// A B/IPv6 BVLL message, the UDP payload (Annex U).
35    Bip6,
36    /// A BVLC-SC message, the WebSocket payload (Annex AB). Only an SC
37    /// session (`sc-tls`) captures one.
38    Sc,
39    /// An MS/TP frame, preamble to CRC (Clause 9.3).
40    Mstp,
41}
42
43impl Framing {
44    /// The short name the capture list shows.
45    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/// What the capture list shows of a frame, read from its headers.
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct FrameSummary {
58    /// The link message: a BVLC function, an MS/TP frame type.
59    pub link: String,
60    /// A link or network broadcast.
61    pub broadcast: bool,
62    /// The link's own source and destination, where the frame names them
63    /// (MS/TP stations, B/IPv6 and BACnet/SC VMACs).
64    pub link_source: Option<String>,
65    /// See [`link_source`](Self::link_source).
66    pub link_destination: Option<String>,
67    /// SNET, when a router added one.
68    pub snet: Option<u16>,
69    /// DNET, for a routed or globally broadcast NPDU.
70    pub dnet: Option<u16>,
71    /// The network-layer message type, for a network message.
72    pub network_message: Option<String>,
73    /// The APDU type, such as `Confirmed-Request`.
74    pub pdu: Option<String>,
75    /// The APDU's invoke ID.
76    pub invoke_id: Option<u8>,
77    /// The service, without `-ACK` or `-Error`.
78    pub service: Option<String>,
79    /// What went wrong: an Error, Reject or Abort, a NAK, a frame that
80    /// doesn't decode.
81    pub problem: Option<String>,
82    /// The device instance an I-Am announces.
83    pub device: Option<u32>,
84    /// SNET and SADR as a routed target, `2:0x7F000001C335`.
85    pub routed_source: Option<String>,
86    /// DNET and DADR the same way; `2:*` for a broadcast on network 2.
87    pub routed_destination: Option<String>,
88}
89
90/// An enumeration's name as the TUI writes it: `READ_PROPERTY` and
91/// `EncapsulatedNpdu` become `read-property` and `encapsulated-npdu`.
92pub 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
108/// What the link layer says about a frame, and where its NPDU is.
109pub struct Link {
110    /// The link message in kebab case: a BVLC function, an MS/TP frame
111    /// type, or `malformed`.
112    pub name: String,
113    /// A link broadcast.
114    pub broadcast: bool,
115    /// The link's source, where the frame names one.
116    pub source: Option<String>,
117    /// The link's destination, where the frame names one.
118    pub destination: Option<String>,
119    /// A NAK, or why the header doesn't decode.
120    pub problem: Option<String>,
121    /// Whether the link header refused to decode, so `npdu` is unknown
122    /// rather than absent.
123    pub malformed: bool,
124    /// The NPDU, as a range of the frame's octets, or decoded on its own
125    /// for an extended MS/TP frame, whose data is COBS-encoded on the wire.
126    pub npdu: Option<Npdu>,
127}
128
129/// Where a frame's NPDU is.
130pub enum Npdu {
131    /// Inside the frame, as sent.
132    At(Range<usize>),
133    /// Encoded in `wire`; `data` is the decoded NPDU.
134    Encoded {
135        /// The encoded octets in the frame.
136        wire: Range<usize>,
137        /// The NPDU.
138        data: Vec<u8>,
139    },
140}
141
142/// Where `part`, a slice of `frame`, starts in it.
143fn offset_in(frame: &[u8], part: &[u8]) -> usize {
144    (part.as_ptr() as usize).saturating_sub(frame.as_ptr() as usize)
145}
146
147/// Read the link layer of `frame`.
148pub 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
280/// Summarise a frame for the capture list. Decodes the headers only; the
281/// service's own parameters are left for [`super::tree`].
282pub 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
301/// Summarise an NPDU on its own, with no link around it: the network
302/// layer and the APDU's header.
303pub 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
309/// What the network layer and the APDU's header add to a summary.
310fn 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;