Skip to main content

bacnet_ops/decode/
tree.rs

1//! A frame decoded into a tree of fields, each with the octets it came
2//! from: the capture screen's decode tree, and the range its hex view
3//! highlights for the selected field.
4//!
5//! Only the frame the user selects is decoded this far. Offsets come from
6//! the codecs' own views where they give them (the BVLL and NPDU slices,
7//! the NPDU's `payload_offset`) and otherwise from the fixed layout of each
8//! header: Clause 9.3 for MS/TP, Annex J.2 and U.2 for the BVLLs, Annex
9//! AB.2 for BVLC-SC, Clause 6.2 for the NPCI and Clause 20.1 for the APDU
10//! headers. The common services' parameters are named field by field
11//! (`service`); any other's, and any a decoder refuses, are walked tag by
12//! tag (Clause 20.2).
13
14use std::ops::Range;
15
16use bacnet_encoding::apdu::{Apdu, decode_apdu};
17use bacnet_encoding::primitives::decode_application_value;
18use bacnet_encoding::tags::{MAX_CONTEXT_NESTING_DEPTH, TagClass, decode_tag};
19use bacnet_npdu::{NetworkPriority, Npdu as NpduView};
20use bacnet_types::primitives::PropertyValue;
21use bytes::Bytes;
22
23use super::frame::{Framing, Link, Npdu, kebab, link};
24
25#[path = "service.rs"]
26mod service;
27
28use service::Params;
29
30/// One decoded field.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Field {
33    /// What it is and holds, such as `Invoke ID: 7`.
34    pub label: String,
35    /// The octets of the frame it came from; empty when it has none of its
36    /// own.
37    pub range: Range<usize>,
38    /// Its parts.
39    pub children: Vec<Field>,
40    /// The label quotes a character string from the frame: text whoever
41    /// sent it chose, which a front end shows as data.
42    pub device_text: bool,
43}
44
45impl Field {
46    fn leaf(label: impl Into<String>, range: Range<usize>) -> Self {
47        Self {
48            label: label.into(),
49            range,
50            children: Vec::new(),
51            device_text: false,
52        }
53    }
54
55    fn node(label: impl Into<String>, range: Range<usize>, children: Vec<Self>) -> Self {
56        Self {
57            label: label.into(),
58            range,
59            children,
60            device_text: false,
61        }
62    }
63
64    /// Point this field and its parts at `range` instead: for an NPDU
65    /// decoded out of COBS-encoded data, whose octets aren't the wire's.
66    fn relocate(&mut self, range: &Range<usize>) {
67        self.range = range.clone();
68        for child in &mut self.children {
69            child.relocate(range);
70        }
71    }
72}
73
74fn hex(octets: &[u8]) -> String {
75    octets.iter().map(|b| format!("{b:02X}")).collect()
76}
77
78/// Decode `frame` into its fields, link layer first.
79pub fn decode_tree(framing: Framing, frame: &[u8]) -> Vec<Field> {
80    let link = link(framing, frame);
81    let npdu_start = match &link.npdu {
82        Some(Npdu::At(range)) => range.start,
83        Some(Npdu::Encoded { wire, .. }) => wire.start,
84        None => frame.len(),
85    };
86    let mut fields = vec![match framing {
87        Framing::Bip => bvll(frame, npdu_start, &link),
88        Framing::Bip6 => bvll6(frame, npdu_start, &link),
89        Framing::Sc => sc(frame, npdu_start, &link),
90        Framing::Mstp => mstp(frame, &link),
91    }];
92    match link.npdu {
93        Some(Npdu::At(range)) => fields.extend(npdu(&frame[range.clone()], range.start)),
94        Some(Npdu::Encoded { wire, data }) => {
95            let mut decoded = npdu(&data, 0);
96            for field in &mut decoded {
97                field.relocate(&wire);
98            }
99            fields.extend(decoded);
100        }
101        None => {}
102    }
103    fields
104}
105
106/// An NPDU given without its link, decoded into its fields: the NPCI,
107/// then a network message or the APDU. Ranges are offsets in `npdu`.
108pub fn decode_npdu_tree(npdu_octets: &[u8]) -> Vec<Field> {
109    npdu(npdu_octets, 0)
110}
111
112/// An APDU given on its own, decoded into its fields: the header, then
113/// the service parameters, named field by field for the common services
114/// and otherwise tag by tag. Ranges are offsets in `apdu`.
115pub fn decode_apdu_tree(apdu_octets: &[u8]) -> Field {
116    apdu(apdu_octets, 0)
117}
118
119/// The link header's node, with the problem the link reported last.
120fn header(name: String, range: Range<usize>, mut kids: Vec<Field>, link: &Link) -> Field {
121    if let Some(problem) = &link.problem {
122        kids.push(Field::leaf(problem.clone(), range.clone()));
123    }
124    Field::node(name, range, kids)
125}
126
127fn bvll(frame: &[u8], npdu_start: usize, link: &Link) -> Field {
128    let mut kids = Vec::new();
129    if frame.len() >= 4 {
130        kids.push(Field::leaf(
131            format!("Type: 0x{:02X} (B/IP)", frame[0]),
132            0..1,
133        ));
134        kids.push(Field::leaf(
135            format!("Function: {} (0x{:02X})", link.name, frame[1]),
136            1..2,
137        ));
138        let length = u16::from_be_bytes([frame[2], frame[3]]);
139        kids.push(Field::leaf(format!("Length: {length}"), 2..4));
140        let rest = &frame[4..npdu_start.max(4)];
141        match (frame[1], rest) {
142            (0x04, [a, b, c, d, hi, lo, ..]) => kids.push(Field::leaf(
143                format!(
144                    "Original source: {a}.{b}.{c}.{d}:{}",
145                    u16::from_be_bytes([*hi, *lo])
146                ),
147                4..10,
148            )),
149            (0x00, [hi, lo]) => {
150                let code = u16::from_be_bytes([*hi, *lo]);
151                let name = bacnet_transport::bvll::BvlcResultCode::from_raw(code);
152                kids.push(Field::leaf(
153                    format!("Result code: {} (0x{code:04X})", kebab(name)),
154                    4..6,
155                ));
156                // The code says it all.
157                return Field::node(format!("BVLL: {}", link.name), 0..npdu_start, kids);
158            }
159            (0x05, [hi, lo]) => kids.push(Field::leaf(
160                format!("Time-to-live: {} s", u16::from_be_bytes([*hi, *lo])),
161                4..6,
162            )),
163            (_, []) => {}
164            (_, rest) => kids.push(Field::leaf(
165                format!("Payload: {} octets", rest.len()),
166                4..4 + rest.len(),
167            )),
168        }
169    }
170    header(format!("BVLL: {}", link.name), 0..npdu_start, kids, link)
171}
172
173fn bvll6(frame: &[u8], npdu_start: usize, link: &Link) -> Field {
174    let mut kids = Vec::new();
175    if frame.len() >= 4 {
176        kids.push(Field::leaf(
177            format!("Type: 0x{:02X} (B/IPv6)", frame[0]),
178            0..1,
179        ));
180        kids.push(Field::leaf(
181            format!("Function: {} (0x{:02X})", link.name, frame[1]),
182            1..2,
183        ));
184        let length = u16::from_be_bytes([frame[2], frame[3]]);
185        kids.push(Field::leaf(format!("Length: {length}"), 2..4));
186    }
187    if let (Some(source), true) = (&link.source, frame.len() >= 7) {
188        kids.push(Field::leaf(format!("Source VMAC: {source}"), 4..7));
189    }
190    if let (Some(destination), true) = (&link.destination, frame.len() >= 10) {
191        kids.push(Field::leaf(
192            format!("Destination VMAC: {destination}"),
193            7..10,
194        ));
195    }
196    let used = kids.last().map_or(4, |field| field.range.end);
197    if npdu_start > used {
198        kids.push(Field::leaf(
199            format!("Function data: {} octets", npdu_start - used),
200            used..npdu_start,
201        ));
202    }
203    header(format!("BVLL6: {}", link.name), 0..npdu_start, kids, link)
204}
205
206fn sc(frame: &[u8], npdu_start: usize, link: &Link) -> Field {
207    let mut kids = Vec::new();
208    if frame.len() >= 4 {
209        kids.push(Field::leaf(
210            format!("Function: {} (0x{:02X})", link.name, frame[0]),
211            0..1,
212        ));
213        let control = frame[1];
214        let flags: Vec<&str> = [
215            (0x08, "originating VMAC"),
216            (0x04, "destination VMAC"),
217            (0x02, "destination options"),
218            (0x01, "data options"),
219        ]
220        .iter()
221        .filter(|(bit, _)| control & bit != 0)
222        .map(|(_, name)| *name)
223        .collect();
224        kids.push(Field::leaf(
225            format!("Control: 0x{control:02X} ({})", flags.join(", ")),
226            1..2,
227        ));
228        let id = u16::from_be_bytes([frame[2], frame[3]]);
229        kids.push(Field::leaf(format!("Message ID: {id}"), 2..4));
230        let mut at = 4;
231        for (bit, name, vmac) in [
232            (0x08, "Originating VMAC", &link.source),
233            (0x04, "Destination VMAC", &link.destination),
234        ] {
235            if let (true, Some(vmac)) = (control & bit != 0, vmac) {
236                kids.push(Field::leaf(format!("{name}: {vmac}"), at..at + 6));
237                at += 6;
238            }
239        }
240        let payload = if link.npdu.is_some() {
241            npdu_start
242        } else {
243            frame.len()
244        };
245        if control & 0x03 != 0 && link.npdu.is_some() && npdu_start > at {
246            kids.push(Field::leaf("Header options", at..npdu_start));
247        } else if payload > at {
248            kids.push(Field::leaf(
249                format!("Options and payload: {} octets", payload - at),
250                at..payload,
251            ));
252        }
253    }
254    header(format!("BVLC-SC: {}", link.name), 0..npdu_start, kids, link)
255}
256
257fn mstp(frame: &[u8], link: &Link) -> Field {
258    let mut kids = Vec::new();
259    if frame.len() >= 8 {
260        let length = usize::from(u16::from_be_bytes([frame[5], frame[6]]));
261        kids.push(Field::leaf("Preamble: 55 FF", 0..2));
262        kids.push(Field::leaf(
263            format!("Frame type: {} ({})", link.name, frame[2]),
264            2..3,
265        ));
266        kids.push(Field::leaf(format!("Destination: {}", frame[3]), 3..4));
267        kids.push(Field::leaf(format!("Source: {}", frame[4]), 4..5));
268        kids.push(Field::leaf(format!("Length: {length}"), 5..7));
269        kids.push(Field::leaf(format!("Header CRC: {:02X}", frame[7]), 7..8));
270        let tail = if frame[2] >= 32 && frame[2] < 128 {
271            5
272        } else {
273            2
274        };
275        if length > 0 && frame.len() >= 8 + tail {
276            let end = frame.len() - tail;
277            kids.push(Field::leaf(format!("Data: {} octets", end - 8), 8..end));
278            let crc = if tail == 5 {
279                "Encoded CRC-32K"
280            } else {
281                "Data CRC"
282            };
283            kids.push(Field::leaf(
284                format!("{crc}: {}", hex(&frame[end..])),
285                end..frame.len(),
286            ));
287        }
288    }
289    header(format!("MS/TP: {}", link.name), 0..frame.len(), kids, link)
290}
291
292/// The NPCI and what follows it: a network message, or the APDU.
293fn npdu(npdu: &[u8], base: usize) -> Vec<Field> {
294    let at = |range: Range<usize>| base + range.start..base + range.end;
295    let view = match NpduView::decode(npdu) {
296        Ok(view) => view,
297        Err(error) => {
298            return vec![Field::leaf(
299                format!("NPDU: malformed ({error})"),
300                at(0..npdu.len()),
301            )];
302        }
303    };
304    let control = npdu.get(1).copied().unwrap_or_default();
305    let mut flags = Vec::new();
306    for (bit, name) in [
307        (0x80, "network message"),
308        (0x20, "DNET"),
309        (0x08, "SNET"),
310        (0x04, "expecting reply"),
311    ] {
312        if control & bit != 0 {
313            flags.push(name.to_string());
314        }
315    }
316    let priority = match view.priority {
317        NetworkPriority::Normal => "normal",
318        NetworkPriority::Urgent => "urgent",
319        NetworkPriority::CriticalEquipment => "critical-equipment",
320        NetworkPriority::LifeSafety => "life-safety",
321    };
322    flags.push(format!("priority {priority}"));
323    let mut kids = vec![
324        Field::leaf(format!("Version: {}", npdu[0]), at(0..1)),
325        Field::leaf(
326            format!("Control: 0x{control:02X} ({})", flags.join(", ")),
327            at(1..2),
328        ),
329    ];
330    let mut pos = 2;
331    for (name, address) in [("D", view.destination), ("S", view.source)] {
332        if let Some(address) = address {
333            let len = address.mac.len();
334            kids.push(Field::leaf(
335                format!("{name}NET: {}", address.network),
336                at(pos..pos + 2),
337            ));
338            kids.push(Field::leaf(
339                format!("{name}LEN: {len}"),
340                at(pos + 2..pos + 3),
341            ));
342            if len > 0 {
343                kids.push(Field::leaf(
344                    format!("{name}ADR: {}", hex(address.mac)),
345                    at(pos + 3..pos + 3 + len),
346                ));
347            }
348            pos += 3 + len;
349        }
350    }
351    if view.destination.is_some() {
352        kids.push(Field::leaf(
353            format!("Hop count: {}", view.hop_count),
354            at(pos..pos + 1),
355        ));
356        pos += 1;
357    }
358    if let Some(kind) = view.message_type {
359        kids.push(Field::leaf(
360            format!("Message type: {}", kebab(kind)),
361            at(pos..pos + 1),
362        ));
363        pos += 1;
364        if let Some(vendor) = view.vendor_id {
365            kids.push(Field::leaf(
366                format!("Vendor ID: {vendor}"),
367                at(pos..pos + 2),
368            ));
369        }
370    }
371    let payload = view.payload_offset;
372    let mut fields = vec![Field::node("NPDU", at(0..payload), kids)];
373    let rest = at(payload..npdu.len());
374    match view.message_type {
375        Some(kind) if !view.payload.is_empty() => fields.push(Field::leaf(
376            format!("{}: {} octets", kebab(kind), view.payload.len()),
377            rest,
378        )),
379        Some(_) => {}
380        None if view.payload.is_empty() => {}
381        None => fields.push(apdu(view.payload, rest.start)),
382    }
383    fields
384}
385
386const PDU_NAMES: [&str; 8] = [
387    "Confirmed-Request",
388    "Unconfirmed-Request",
389    "Simple-ACK",
390    "Complex-ACK",
391    "Segment-ACK",
392    "Error",
393    "Reject",
394    "Abort",
395];
396
397/// The APDU header, field by field, then its parameters: named for the
398/// common services, as tags otherwise.
399fn apdu(apdu: &[u8], base: usize) -> Field {
400    let at = |range: Range<usize>| base + range.start..base + range.end;
401    let whole = at(0..apdu.len());
402    let decoded = match decode_apdu(Bytes::copy_from_slice(apdu)) {
403        Ok(decoded) => decoded,
404        Err(error) => return Field::leaf(format!("APDU: malformed ({error})"), whole),
405    };
406    let first = apdu[0];
407    let kind = PDU_NAMES[usize::from(first >> 4)];
408    let mut kids = vec![Field::leaf(
409        format!("PDU type: {kind} (flags 0x{:X})", first & 0x0F),
410        at(0..1),
411    )];
412    let mut leaf = |label: String, range: Range<usize>| kids.push(Field::leaf(label, at(range)));
413    let segmented = first & 0x08 != 0;
414    let (service, params, extra, named) = match &decoded {
415        Apdu::ConfirmedRequest(request) => {
416            let segments = request
417                .max_segments
418                .map_or_else(|| "unspecified".to_string(), |n| n.to_string());
419            leaf(
420                format!(
421                    "Max segments: {segments}, max APDU: {}",
422                    request.max_apdu_length
423                ),
424                1..2,
425            );
426            leaf(format!("Invoke ID: {}", request.invoke_id), 2..3);
427            let mut pos = 3;
428            if segmented {
429                leaf(format!("Sequence number: {}", apdu[3]), 3..4);
430                leaf(format!("Proposed window size: {}", apdu[4]), 4..5);
431                pos = 5;
432            }
433            leaf(
434                format!("Service: {}", kebab(request.service_choice)),
435                pos..pos + 1,
436            );
437            let named = Params::Request(request.service_choice);
438            (kebab(request.service_choice), pos + 1, None, Some(named))
439        }
440        Apdu::UnconfirmedRequest(request) => {
441            leaf(format!("Service: {}", kebab(request.service_choice)), 1..2);
442            let named = Params::Unconfirmed(request.service_choice);
443            (kebab(request.service_choice), 2, None, Some(named))
444        }
445        Apdu::SimpleAck(ack) => {
446            leaf(format!("Invoke ID: {}", ack.invoke_id), 1..2);
447            leaf(format!("Service: {}", kebab(ack.service_choice)), 2..3);
448            (kebab(ack.service_choice), 3, None, None)
449        }
450        Apdu::ComplexAck(ack) => {
451            leaf(format!("Invoke ID: {}", ack.invoke_id), 1..2);
452            let mut pos = 2;
453            if segmented {
454                leaf(format!("Sequence number: {}", apdu[2]), 2..3);
455                leaf(format!("Proposed window size: {}", apdu[3]), 3..4);
456                pos = 4;
457            }
458            leaf(
459                format!("Service: {}", kebab(ack.service_choice)),
460                pos..pos + 1,
461            );
462            let named = Params::Ack(ack.service_choice);
463            (kebab(ack.service_choice), pos + 1, None, Some(named))
464        }
465        Apdu::SegmentAck(ack) => {
466            leaf(format!("Invoke ID: {}", ack.invoke_id), 1..2);
467            leaf(format!("Sequence number: {}", ack.sequence_number), 2..3);
468            leaf(
469                format!("Actual window size: {}", ack.actual_window_size),
470                3..4,
471            );
472            (String::new(), 4, None, None)
473        }
474        Apdu::Error(error) => {
475            leaf(format!("Invoke ID: {}", error.invoke_id), 1..2);
476            leaf(format!("Service: {}", kebab(error.service_choice)), 2..3);
477            let problem = format!(
478                "Error: {}:{}",
479                kebab(error.error_class),
480                kebab(error.error_code)
481            );
482            (kebab(error.service_choice), 3, Some(problem), None)
483        }
484        Apdu::Reject(reject) => {
485            leaf(format!("Invoke ID: {}", reject.invoke_id), 1..2);
486            leaf(
487                format!("Reject reason: {}", kebab(reject.reject_reason)),
488                2..3,
489            );
490            (String::new(), 3, None, None)
491        }
492        Apdu::Abort(abort) => {
493            leaf(format!("Invoke ID: {}", abort.invoke_id), 1..2);
494            leaf(format!("Abort reason: {}", kebab(abort.abort_reason)), 2..3);
495            (String::new(), 3, None, None)
496        }
497    };
498    if params < apdu.len() && service.is_empty() && extra.is_none() {
499        // A PDU with no parameters: whatever follows its header.
500        kids.push(Field::leaf(
501            format!("Trailing octets: {}", apdu.len() - params),
502            at(params..apdu.len()),
503        ));
504    } else if params < apdu.len() {
505        let label = extra.unwrap_or_else(|| format!("{service} parameters"));
506        // A segment holds only part of the parameters; its sequence
507        // number says whether it is the first.
508        let segment = match &decoded {
509            Apdu::ConfirmedRequest(request) => request.sequence_number,
510            Apdu::ComplexAck(ack) => ack.sequence_number,
511            _ => None,
512        }
513        .filter(|_| segmented);
514        let fields = service::parameters(named, segment, apdu, base, params);
515        kids.push(Field::node(label, at(params..apdu.len()), fields));
516    }
517    Field::node(format!("APDU: {kind}"), whole, kids)
518}
519
520const APPLICATION_TAGS: [&str; 13] = [
521    "Null",
522    "Boolean",
523    "Unsigned",
524    "Signed",
525    "Real",
526    "Double",
527    "Octet string",
528    "Character string",
529    "Bit string",
530    "Enumerated",
531    "Date",
532    "Time",
533    "Object identifier",
534];
535
536/// An application value as the screen writes it: enumerations and object
537/// types in kebab-case, an object as `analog-input 1`.
538fn value_text(value: &PropertyValue) -> String {
539    match value {
540        PropertyValue::Enumerated(n) => n.to_string(),
541        PropertyValue::ObjectIdentifier(object) => {
542            format!(
543                "{} {}",
544                kebab(object.object_type()),
545                object.instance_number()
546            )
547        }
548        // TODO(#1888): the TUI's float cap applies here once it lands.
549        value => crate::format::format_property_value(value),
550    }
551}
552
553/// The tagged values in `data[start..end]`, opening and closing tags as
554/// nodes. Stops at the first tag it can't read, with a leaf saying so.
555/// A closing tag with no opening one ends the walk early; the octets from
556/// it on are one leaf, so every octet still has a field.
557fn tags(data: &[u8], base: usize, start: usize, end: usize, depth: usize) -> Vec<Field> {
558    let mut fields = walk(data, base, start, end, depth).0;
559    let covered = fields
560        .last()
561        .map_or(start, |field| field.range.end.saturating_sub(base));
562    if covered < end {
563        fields.push(Field::leaf(
564            format!("Trailing octets: {}", end - covered),
565            base + covered..base + end,
566        ));
567    }
568    fields
569}
570
571/// [`tags`], and where the walk stopped: just past the closing tag that
572/// ended this level, or `end`. Every tag header's length comes from
573/// `decode_tag`, since tag numbers from 15 up take a second octet.
574fn walk(data: &[u8], base: usize, start: usize, end: usize, depth: usize) -> (Vec<Field>, usize) {
575    let mut fields = Vec::new();
576    let mut pos = start;
577    while pos < end {
578        let (tag, content) = match decode_tag(&data[..end], pos) {
579            Ok(read) if depth < MAX_CONTEXT_NESTING_DEPTH => read,
580            Ok(_) => {
581                fields.push(Field::leaf("Nested too deeply", base + pos..base + end));
582                return (fields, end);
583            }
584            Err(error) => {
585                fields.push(Field::leaf(
586                    format!("Malformed tag: {error}"),
587                    base + pos..base + end,
588                ));
589                return (fields, end);
590            }
591        };
592        if tag.is_closing {
593            return (fields, content);
594        }
595        if tag.is_opening {
596            let (inner, after) = walk(data, base, content, end, depth + 1);
597            fields.push(Field::node(
598                format!("[{}] opening", tag.number),
599                base + pos..base + after,
600                inner,
601            ));
602            pos = after;
603            continue;
604        }
605        // An application Boolean holds its value in the header and has no
606        // content octets; the value decoder knows where each one ends.
607        let decoded = (tag.class == TagClass::Application)
608            .then(|| decode_application_value(&data[..end], pos));
609        let after = match &decoded {
610            Some(Ok((_, after))) => *after,
611            _ => content.saturating_add(tag.length as usize),
612        }
613        .min(end)
614        .max(content);
615        let device_text = matches!(&decoded, Some(Ok((PropertyValue::CharacterString(_), _))));
616        let label = match decoded {
617            Some(Ok((value, _))) => format!(
618                "{}: {}",
619                APPLICATION_TAGS
620                    .get(usize::from(tag.number))
621                    .unwrap_or(&"Application"),
622                value_text(&value)
623            ),
624            Some(Err(error)) => format!("Application tag {}: {error}", tag.number),
625            None => {
626                let octets = &data[content..after];
627                let number = (octets.len() <= 4)
628                    .then(|| octets.iter().fold(0u32, |n, b| (n << 8) | u32::from(*b)));
629                match number {
630                    Some(n) if !octets.is_empty() => {
631                        format!("[{}] {} ({n})", tag.number, hex(octets))
632                    }
633                    _ => format!("[{}] {}", tag.number, hex(octets)),
634                }
635            }
636        };
637        let mut field = Field::leaf(label, base + pos..base + after);
638        field.device_text = device_text;
639        fields.push(field);
640        pos = after;
641    }
642    (fields, end)
643}
644
645#[cfg(test)]
646#[path = "tree_tests.rs"]
647mod tests;