bgpkit_parser/models/dissection.rs
1//! Byte-level dissection types for Wireshark-style field inspection.
2//!
3//! A [`DissectionNode`] tree annotates every field of a BGP or MRT message
4//! with its byte range (`offset`/`length`) so a frontend can highlight the
5//! bytes behind any protocol field, and vice versa. Dissection is produced by
6//! a separate best-effort pass ([`crate::parser::bgp::dissect`],
7//! [`crate::parser::mrt::dissect`]) and is never on the default parsing hot
8//! path.
9
10use crate::error::BgpValidationWarning;
11
12/// Byte range within a dissected buffer: `[offset, offset + length)`.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
16pub struct Span {
17 pub offset: u32,
18 pub length: u32,
19}
20
21impl Span {
22 pub const fn new(offset: u32, length: u32) -> Self {
23 Span { offset, length }
24 }
25}
26
27/// One field of a dissected message.
28///
29/// `field` is a stable machine-readable identifier using dotted paths, e.g.
30/// `bgp.header.marker`, `bgp.update.path_attributes`, or `bgp.attr.32` (the
31/// attribute type code suffix identifies which path attribute the node
32/// covers). `label` is a human-readable rendering that frontends can display
33/// directly. Offsets are relative to the start of the dissected buffer: for a
34/// bare BGP message the message itself, and for an MRT record the whole
35/// record (common header + message body).
36///
37/// Best-effort contract: when the input is truncated or malformed, the tree
38/// simply stops at the last field that could be walked; a dissector never
39/// fails.
40#[derive(Debug, Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
43pub struct DissectionNode {
44 pub field: String,
45 pub label: String,
46 pub offset: u32,
47 pub length: u32,
48 pub children: Vec<DissectionNode>,
49}
50
51impl DissectionNode {
52 pub fn new(
53 field: impl Into<String>,
54 label: impl Into<String>,
55 offset: u32,
56 length: u32,
57 ) -> Self {
58 DissectionNode {
59 field: field.into(),
60 label: label.into(),
61 offset,
62 length,
63 children: Vec::new(),
64 }
65 }
66
67 /// Byte range covered by this node.
68 pub const fn span(&self) -> Span {
69 Span {
70 offset: self.offset,
71 length: self.length,
72 }
73 }
74
75 /// Depth-first search for the first node with an exact `field` match.
76 pub fn find(&self, field: &str) -> Option<&DissectionNode> {
77 if self.field == field {
78 return Some(self);
79 }
80 self.children.iter().find_map(|child| child.find(field))
81 }
82
83 /// Collect all nodes with an exact `field` match, in tree order.
84 pub fn find_all<'a>(&'a self, field: &str, out: &mut Vec<&'a DissectionNode>) {
85 if self.field == field {
86 out.push(self);
87 }
88 for child in &self.children {
89 child.find_all(field, out);
90 }
91 }
92}
93
94/// A validation warning anchored to the byte range it concerns.
95///
96/// Produced by correlating RFC 7606 warnings with a [`DissectionNode`] tree;
97/// the span points at the attribute or NLRI section the warning is about, so
98/// a frontend can highlight the offending bytes.
99#[derive(Debug, Clone, PartialEq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize))]
101#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
102pub struct SpannedWarning {
103 pub span: Span,
104 pub warning: BgpValidationWarning,
105}