Skip to main content

bgpkit_parser/parser/iters/
diagnostic.rs

1//! Record-level diagnostic iterator for malformed MRT data investigation.
2//!
3//! The iterator yields one event per record with the original raw bytes
4//! always attached, so byte-level inspection (see
5//! [`crate::parser::mrt::dissect`]) is possible for every record, not only
6//! the anomalous ones. `Record` events carry the RFC 7606 validation
7//! findings collected during parsing (empty when the record is clean);
8//! `ParseError` events carry a best-effort partial dissection tree showing
9//! how far the structure could be walked before the failure.
10//!
11//! For full Wireshark-style field trees on every record, upgrade the
12//! iterator with [`DiagnosticIterator::with_dissection`].
13
14use crate::error::{BgpValidationWarning, ParserError};
15use crate::models::*;
16use crate::parser::mrt::dissect::{dissect_mrt_bytes, dissect_mrt_record};
17use crate::parser::mrt::mrt_record::{chunk_mrt_record_with_context, raw_record_uses_zebra_compat};
18use crate::parser::mrt::RawMrtRecord;
19use crate::parser::BgpkitParser;
20use std::collections::HashMap;
21use std::io::Read;
22
23/// A record-level parsing outcome for malformed-data investigation.
24#[derive(Debug)]
25#[non_exhaustive]
26pub enum DiagnosticEvent {
27    /// A parsed record, with its raw bytes and any recoverable validation
28    /// findings. An empty `warnings` vector means the record parsed clean.
29    Record {
30        /// The parsed MRT record.
31        record: MrtRecord,
32        /// The original MRT record bytes and header.
33        raw: RawMrtRecord,
34        /// RFC 7606 validation findings in record traversal order.
35        warnings: Vec<BgpValidationWarning>,
36    },
37    /// A record that could not be fully parsed.
38    ParseError {
39        /// The parsing failure.
40        error: ParserError,
41        /// The MRT common header when framing completed successfully.
42        common_header: Option<CommonHeader>,
43        /// All bytes consumed for this record, when available.
44        raw_bytes: Option<Vec<u8>>,
45        /// Best-effort partial dissection tree: the fields that could be
46        /// walked before the failure, showing where parsing stopped.
47        partial: Option<DissectionNode>,
48    },
49}
50
51/// Iterator over record-level parsing diagnostics.
52pub struct DiagnosticIterator<R> {
53    parser: BgpkitParser<R>,
54    terminated: bool,
55}
56
57impl<R> DiagnosticIterator<R> {
58    pub(crate) fn new(parser: BgpkitParser<R>) -> Self {
59        Self {
60            parser,
61            terminated: false,
62        }
63    }
64
65    /// Upgrade to an iterator that attaches a full dissection tree and
66    /// byte spans to every event.
67    ///
68    /// This is the byte-inspection mode used by field-level tooling: each
69    /// `Record` event gains a [`DissectionNode`] tree over the whole record
70    /// (common header, BGP4MP subheader, embedded BGP message) and its
71    /// warnings become [`SpannedWarning`]s anchored to the bytes they
72    /// concern. Building the tree costs an extra walk per record, so it is
73    /// opt-in and never paid by the lean iterator.
74    pub fn with_dissection(self) -> DissectingDiagnosticIterator<R> {
75        DissectingDiagnosticIterator { inner: self }
76    }
77}
78
79impl<R: Read> Iterator for DiagnosticIterator<R> {
80    type Item = DiagnosticEvent;
81
82    fn next(&mut self) -> Option<Self::Item> {
83        // Text dumps have no MRT record boundary or raw MRT representation.
84        if self.terminated || self.parser.text_dump_iter.is_some() {
85            return None;
86        }
87
88        let raw_record = match chunk_mrt_record_with_context(&mut self.parser.reader) {
89            Ok(raw_record) => raw_record,
90            Err(error) if matches!(error.error, ParserError::EofExpected) => {
91                self.terminated = true;
92                return None;
93            }
94            Err(error) => {
95                // A framing error may leave the reader between record boundaries. Do not
96                // attempt to reinterpret the remaining bytes as another MRT header unless
97                // a rejected header's record was consumed whole.
98                self.terminated = !error.record_consumed;
99                return Some(DiagnosticEvent::ParseError {
100                    error: error.error,
101                    common_header: error.common_header,
102                    partial: error.bytes.as_deref().map(dissect_mrt_bytes),
103                    raw_bytes: error.bytes,
104                });
105            }
106        };
107
108        let used_zebra_compat = raw_record_uses_zebra_compat(&raw_record);
109        let record = match raw_record.clone().parse() {
110            Ok(record) => record,
111            Err(error) => {
112                let raw_bytes = Some(raw_record.raw_bytes().to_vec());
113                return Some(DiagnosticEvent::ParseError {
114                    error,
115                    common_header: Some(raw_record.common_header),
116                    partial: raw_bytes.as_deref().map(dissect_mrt_bytes),
117                    raw_bytes,
118                });
119            }
120        };
121        if used_zebra_compat {
122            self.parser.warn_zebra_compat_once();
123        }
124
125        let warnings = record_validation_warnings(&record);
126        Some(DiagnosticEvent::Record {
127            record,
128            raw: raw_record,
129            warnings,
130        })
131    }
132}
133
134/// A record-level diagnostic with a full dissection tree attached.
135#[derive(Debug)]
136// The tree-carrying Record variant is intentionally the largest and the most
137// common outcome; boxing its fields would add indirection to every consumer.
138#[allow(clippy::large_enum_variant)]
139#[non_exhaustive]
140pub enum DissectedDiagnosticEvent {
141    /// A parsed record with its dissection tree and spanned warnings.
142    Record {
143        /// The parsed MRT record.
144        record: MrtRecord,
145        /// The original MRT record bytes and header.
146        raw: RawMrtRecord,
147        /// Validation findings anchored to the byte ranges they concern.
148        warnings: Vec<SpannedWarning>,
149        /// Wireshark-style field tree over the whole record.
150        tree: DissectionNode,
151    },
152    /// A record that could not be fully parsed; `partial` shows how far the
153    /// structure could be walked.
154    ParseError {
155        /// The parsing failure.
156        error: ParserError,
157        /// The MRT common header when framing completed successfully.
158        common_header: Option<CommonHeader>,
159        /// All bytes consumed for this record, when available.
160        raw_bytes: Option<Vec<u8>>,
161        /// Best-effort partial dissection tree.
162        partial: Option<DissectionNode>,
163    },
164}
165
166/// Iterator produced by [`DiagnosticIterator::with_dissection`].
167pub struct DissectingDiagnosticIterator<R> {
168    inner: DiagnosticIterator<R>,
169}
170
171impl<R: Read> Iterator for DissectingDiagnosticIterator<R> {
172    type Item = DissectedDiagnosticEvent;
173
174    fn next(&mut self) -> Option<Self::Item> {
175        self.inner.next().map(|event| match event {
176            DiagnosticEvent::Record {
177                record,
178                raw,
179                warnings,
180            } => {
181                let tree = dissect_mrt_record(&raw);
182                let warnings = span_record_warnings(&warnings, &tree);
183                DissectedDiagnosticEvent::Record {
184                    record,
185                    raw,
186                    warnings,
187                    tree,
188                }
189            }
190            DiagnosticEvent::ParseError {
191                error,
192                common_header,
193                raw_bytes,
194                partial,
195            } => DissectedDiagnosticEvent::ParseError {
196                error,
197                common_header,
198                raw_bytes,
199                partial,
200            },
201        })
202    }
203}
204
205/// Collect the RFC 7606 validation findings of an MRT record.
206///
207/// This is the taxonomy walk the [`DiagnosticIterator`] applies per record;
208/// it is public so custom investigation pipelines can reuse it.
209pub fn record_validation_warnings(record: &MrtRecord) -> Vec<BgpValidationWarning> {
210    match &record.message {
211        MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(message)) => update_warnings(&message.bgp_message),
212        MrtMessage::LegacyBgp(LegacyBgp::Message(message)) => update_warnings(&message.bgp_message),
213        MrtMessage::TableDumpMessage(message) => message.attributes.validation_warnings().to_vec(),
214        MrtMessage::TableDumpMessageBatch(messages) => messages
215            .iter()
216            .flat_map(|message| message.attributes.validation_warnings().iter().cloned())
217            .collect(),
218        MrtMessage::TableDumpV2Message(TableDumpV2Message::RibAfi(rib)) => rib
219            .rib_entries
220            .iter()
221            .flat_map(|entry| entry.attributes.validation_warnings().iter().cloned())
222            .collect(),
223        _ => Vec::new(),
224    }
225}
226
227fn update_warnings(message: &BgpMessage) -> Vec<BgpValidationWarning> {
228    match message {
229        BgpMessage::Update(update) => update.attributes.validation_warnings().to_vec(),
230        BgpMessage::RouteRefresh(refresh) => refresh.validation_warnings(),
231        _ => Vec::new(),
232    }
233}
234
235/// Anchor validation warnings to the byte ranges they concern.
236///
237/// The parser does not track offsets on its hot path, so spans are
238/// correlated here against the dissection tree: attribute-keyed warnings
239/// point at the matching `bgp.attr.{code}` node (the Nth occurrence for
240/// duplicate attributes), NLRI warnings at the NLRI section, and everything
241/// else falls back to the enclosing section or the whole record.
242pub fn span_record_warnings(
243    warnings: &[BgpValidationWarning],
244    tree: &DissectionNode,
245) -> Vec<SpannedWarning> {
246    // Most records are clean: don't walk the tree for them at all.
247    if warnings.is_empty() {
248        return Vec::new();
249    }
250    // Index the tree once: a record can carry as many warnings as it has
251    // attributes, and walking the whole tree per warning is quadratic.
252    let index = SpanIndex::build(tree);
253    let mut duplicate_counts: HashMap<u8, usize> = HashMap::new();
254    warnings
255        .iter()
256        .map(|warning| SpannedWarning {
257            span: locate_warning_span(warning, &index, &mut duplicate_counts)
258                .unwrap_or_else(|| tree.span()),
259            warning: warning.clone(),
260        })
261        .collect()
262}
263
264/// Spans of every node in a dissection tree, keyed by field name, in tree
265/// (pre-order) order: `nth(field, 0)` is the node `DissectionNode::find`
266/// would return, and `nth(field, n)` the `n`th match of `find_all`.
267struct SpanIndex<'a> {
268    spans: HashMap<&'a str, Vec<Span>>,
269}
270
271impl<'a> SpanIndex<'a> {
272    fn build(tree: &'a DissectionNode) -> Self {
273        let mut spans = HashMap::new();
274        Self::walk(tree, &mut spans);
275        Self { spans }
276    }
277
278    fn walk(node: &'a DissectionNode, spans: &mut HashMap<&'a str, Vec<Span>>) {
279        spans
280            .entry(node.field.as_str())
281            .or_default()
282            .push(node.span());
283        for child in &node.children {
284            Self::walk(child, spans);
285        }
286    }
287
288    fn nth(&self, field: &str, occurrence: usize) -> Option<Span> {
289        self.spans.get(field)?.get(occurrence).copied()
290    }
291}
292
293fn locate_warning_span(
294    warning: &BgpValidationWarning,
295    index: &SpanIndex<'_>,
296    duplicate_counts: &mut HashMap<u8, usize>,
297) -> Option<Span> {
298    use BgpValidationWarning as W;
299
300    let attr_span = |code: u8, occurrence: usize| -> Option<Span> {
301        index.nth(&format!("bgp.attr.{code}"), occurrence)
302    };
303    // Anchor to the attribute occurrence the warning is about. The parser
304    // emits warnings while observing headers in wire order and reports
305    // `DuplicateAttribute` before any other finding for that duplicate
306    // header, so the number of duplicates seen so far is the occurrence
307    // every attribute-keyed warning of that type belongs to.
308    let current_attr_span = |code: u8| -> Option<Span> {
309        let occurrence = duplicate_counts.get(&code).copied().unwrap_or(0);
310        attr_span(code, occurrence)
311    };
312    let section_span = |field: &str| -> Option<Span> { index.nth(field, 0) };
313
314    match warning {
315        W::AttributeFlagsError { attr_type, .. }
316        | W::AttributeLengthError { attr_type, .. }
317        | W::OptionalAttributeError { attr_type, .. }
318        | W::PartialAttributeError { attr_type, .. } => current_attr_span(u8::from(*attr_type))
319            .or_else(|| section_span("bgp.update.path_attributes")),
320        W::DuplicateAttribute { attr_type } => {
321            let code = u8::from(*attr_type);
322            let count = duplicate_counts.entry(code).or_insert(0);
323            *count += 1;
324            attr_span(code, *count).or_else(|| section_span("bgp.update.path_attributes"))
325        }
326        W::InvalidOriginAttribute { .. } => current_attr_span(1),
327        W::InvalidNextHopAttribute { .. } => current_attr_span(3),
328        W::MalformedAsPath { .. } => current_attr_span(2),
329        W::UnrecognizedWellKnownAttribute { attr_type_code } => current_attr_span(*attr_type_code),
330        W::MissingWellKnownAttribute { .. } | W::MalformedAttributeList { .. } => {
331            section_span("bgp.update.path_attributes")
332        }
333        W::InvalidNetworkField { .. } => {
334            section_span("bgp.update.nlri").or_else(|| section_span("bgp.update.withdrawn_routes"))
335        }
336        W::MalformedNlri { nlri_type, .. } => match *nlri_type {
337            "withdrawn" => section_span("bgp.update.withdrawn_routes"),
338            "announced" => section_span("bgp.update.nlri"),
339            "mp_reach" => current_attr_span(14),
340            "mp_unreach" => current_attr_span(15),
341            _ => None,
342        },
343        W::UnknownRouteRefreshSubtype { .. } | W::InvalidRouteRefreshLength { .. } => {
344            section_span("bgp.route_refresh")
345        }
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use std::io::Cursor;
353    use std::net::IpAddr;
354    use std::str::FromStr;
355
356    fn update_record(attributes: Attributes) -> MrtRecord {
357        MrtRecord {
358            common_header: CommonHeader {
359                timestamp: 1_700_000_000,
360                microsecond_timestamp: None,
361                entry_type: EntryType::BGP4MP,
362                entry_subtype: Bgp4MpType::MessageAs4 as u16,
363                length: 0,
364            },
365            message: MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(Bgp4MpMessage {
366                msg_type: Bgp4MpType::MessageAs4,
367                peer_asn: Asn::new_32bit(64496),
368                local_asn: Asn::new_32bit(64497),
369                interface_index: 0,
370                peer_ip: IpAddr::from_str("192.0.2.1").unwrap(),
371                local_ip: IpAddr::from_str("192.0.2.2").unwrap(),
372                bgp_message: BgpMessage::Update(BgpUpdateMessage {
373                    withdrawn_prefixes: vec![],
374                    attributes,
375                    announced_prefixes: vec![NetworkPrefix::from_str("203.0.113.0/24").unwrap()],
376                }),
377            })),
378        }
379    }
380
381    fn valid_attributes() -> Attributes {
382        let mut attributes = Attributes::default();
383        attributes.add_attr(AttributeValue::Origin(Origin::IGP).into());
384        attributes.add_attr(AttributeValue::AsPath(AsPath::from_sequence([64500])).into());
385        attributes
386            .add_attr(AttributeValue::NextHop(IpAddr::from_str("192.0.2.254").unwrap()).into());
387        attributes
388    }
389
390    fn update_wire_body(withdrawn: &[u8], attributes: &[u8], announced: &[u8]) -> Vec<u8> {
391        let mut body = Vec::new();
392        body.extend_from_slice(&(withdrawn.len() as u16).to_be_bytes());
393        body.extend_from_slice(withdrawn);
394        body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
395        body.extend_from_slice(attributes);
396        body.extend_from_slice(announced);
397        body
398    }
399
400    fn bgp4mp_update_wire(withdrawn: &[u8], attributes: &[u8], announced: &[u8]) -> Vec<u8> {
401        let update_body = update_wire_body(withdrawn, attributes, announced);
402        bgp4mp_message_wire(BgpMessageType::UPDATE, &update_body)
403    }
404
405    fn bgp4mp_route_refresh_wire(subtype: u8, orf_data: &[u8]) -> Vec<u8> {
406        let mut refresh_body = vec![0x00, 0x01, subtype, 0x01];
407        refresh_body.extend_from_slice(orf_data);
408        bgp4mp_message_wire(BgpMessageType::ROUTE_REFRESH, &refresh_body)
409    }
410
411    fn bgp4mp_message_wire(msg_type: BgpMessageType, msg_body: &[u8]) -> Vec<u8> {
412        let mut bgp_message = vec![0xff; 16];
413        bgp_message.extend_from_slice(&((19 + msg_body.len()) as u16).to_be_bytes());
414        bgp_message.push(msg_type as u8);
415        bgp_message.extend_from_slice(msg_body);
416
417        let mut body = Vec::new();
418        body.extend_from_slice(&64496u32.to_be_bytes());
419        body.extend_from_slice(&64497u32.to_be_bytes());
420        body.extend_from_slice(&0u16.to_be_bytes());
421        body.extend_from_slice(&1u16.to_be_bytes());
422        body.extend_from_slice(&[192, 0, 2, 1]);
423        body.extend_from_slice(&[192, 0, 2, 2]);
424        body.extend_from_slice(&bgp_message);
425
426        let header = CommonHeader {
427            timestamp: 1_700_000_000,
428            microsecond_timestamp: None,
429            entry_type: EntryType::BGP4MP,
430            entry_subtype: Bgp4MpType::MessageAs4 as u16,
431            length: body.len() as u32,
432        };
433        let mut wire = header.encode().to_vec();
434        wire.extend_from_slice(&body);
435        wire
436    }
437
438    fn valid_update_attributes_wire() -> Vec<u8> {
439        vec![
440            0x40, 0x01, 0x01, 0x00, // ORIGIN = IGP
441            0x40, 0x02, 0x00, // AS_PATH = empty
442            0x40, 0x03, 0x04, 192, 0, 2, 254, // NEXT_HOP
443        ]
444    }
445
446    fn assert_wire_validation(
447        wire: Vec<u8>,
448        expected_warning: impl Fn(&BgpValidationWarning) -> bool,
449    ) {
450        let mut iter = BgpkitParser::from_reader(Cursor::new(wire.clone())).into_diagnostic_iter();
451        match iter.next().unwrap() {
452            DiagnosticEvent::Record { warnings, raw, .. } => {
453                assert!(
454                    warnings.iter().any(expected_warning),
455                    "expected validation warning, got {warnings:?}"
456                );
457                assert_eq!(raw.raw_bytes().as_ref(), wire.as_slice());
458            }
459            event => panic!("expected record event with warnings, got {event:?}"),
460        }
461        assert!(iter.next().is_none());
462    }
463
464    #[test]
465    fn diagnostic_iterator_flags_unknown_route_refresh_subtype() {
466        // RFC 7313 Section 5: subtype other than 0-2 is ignored on the wire
467        let wire = bgp4mp_route_refresh_wire(3, &[]);
468        assert_wire_validation(wire, |warning| {
469            matches!(
470                warning,
471                BgpValidationWarning::UnknownRouteRefreshSubtype { subtype: 3 }
472            )
473        });
474    }
475
476    #[test]
477    fn diagnostic_iterator_flags_borr_with_trailing_data() {
478        // RFC 7313 Section 5: a BoRR/EoRR body must be exactly 4 bytes
479        let wire = bgp4mp_route_refresh_wire(1, &[0xDE, 0xAD]);
480        assert_wire_validation(wire, |warning| {
481            matches!(
482                warning,
483                BgpValidationWarning::InvalidRouteRefreshLength {
484                    subtype: 1,
485                    length: 6
486                }
487            )
488        });
489    }
490
491    #[test]
492    fn diagnostic_iterator_passes_normal_route_refresh_with_orf_data_clean() {
493        // A normal (subtype 0) refresh may carry ORF data (RFC 5291)
494        let wire = bgp4mp_route_refresh_wire(0, &[0x01, 0x80, 0x00, 0x00]);
495        let mut iter = BgpkitParser::from_reader(Cursor::new(wire)).into_diagnostic_iter();
496        match iter.next().unwrap() {
497            DiagnosticEvent::Record {
498                record, warnings, ..
499            } => {
500                assert!(warnings.is_empty());
501                assert!(matches!(
502                    record.message,
503                    MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(Bgp4MpMessage {
504                        bgp_message: BgpMessage::RouteRefresh(_),
505                        ..
506                    }))
507                ));
508            }
509            event => panic!("expected clean record event, got {event:?}"),
510        }
511        assert!(iter.next().is_none());
512    }
513
514    #[test]
515    fn diagnostic_iterator_yields_clean_record() {
516        let wire = update_record(valid_attributes()).encode().unwrap().to_vec();
517        let mut iter = BgpkitParser::from_reader(Cursor::new(wire)).into_diagnostic_iter();
518
519        match iter.next().unwrap() {
520            DiagnosticEvent::Record {
521                record, warnings, ..
522            } => {
523                assert!(warnings.is_empty());
524                assert!(matches!(record.message, MrtMessage::Bgp4Mp(_)));
525            }
526            event => panic!("expected clean record event, got {event:?}"),
527        }
528        assert!(iter.next().is_none());
529    }
530
531    #[test]
532    fn diagnostic_iterator_yields_validation_with_original_bytes() {
533        let wire = update_record(Attributes::default())
534            .encode()
535            .unwrap()
536            .to_vec();
537        let mut iter = BgpkitParser::from_reader(Cursor::new(wire.clone())).into_diagnostic_iter();
538
539        match iter.next().unwrap() {
540            DiagnosticEvent::Record {
541                record,
542                warnings,
543                raw,
544            } => {
545                assert!(matches!(record.message, MrtMessage::Bgp4Mp(_)));
546                assert!(warnings.iter().any(|warning| {
547                    matches!(
548                        warning,
549                        BgpValidationWarning::MissingWellKnownAttribute {
550                            attr_type: AttrType::ORIGIN
551                        }
552                    )
553                }));
554                assert_eq!(raw.raw_bytes().as_ref(), wire.as_slice());
555            }
556            event => panic!("expected record event with warnings, got {event:?}"),
557        }
558    }
559
560    #[test]
561    fn diagnostic_iterator_preserves_body_error_and_continues() {
562        let header = CommonHeader {
563            timestamp: 2,
564            microsecond_timestamp: None,
565            entry_type: EntryType::TABLE_DUMP,
566            entry_subtype: 0,
567            length: 4,
568        };
569        let mut input = header.encode().to_vec();
570        input.extend_from_slice(&[0xff; 4]);
571        let invalid_record = input.clone();
572        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
573
574        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
575        match iter.next().unwrap() {
576            DiagnosticEvent::ParseError {
577                common_header,
578                raw_bytes,
579                ..
580            } => {
581                assert_eq!(common_header, Some(header));
582                assert_eq!(raw_bytes.as_deref(), Some(invalid_record.as_slice()));
583            }
584            event => panic!("expected parse error event, got {event:?}"),
585        }
586        assert!(matches!(iter.next(), Some(DiagnosticEvent::Record { .. })));
587        assert!(iter.next().is_none());
588    }
589
590    #[test]
591    fn diagnostic_iterator_preserves_partial_body_context() {
592        let header = CommonHeader {
593            timestamp: 3,
594            microsecond_timestamp: None,
595            entry_type: EntryType::BGP4MP,
596            entry_subtype: 0,
597            length: 4,
598        };
599        let mut input = header.encode().to_vec();
600        input.extend_from_slice(&[0, 1]);
601
602        match BgpkitParser::from_reader(Cursor::new(input.clone()))
603            .into_diagnostic_iter()
604            .next()
605            .unwrap()
606        {
607            DiagnosticEvent::ParseError {
608                common_header,
609                raw_bytes,
610                ..
611            } => {
612                assert_eq!(common_header, Some(header));
613                assert_eq!(raw_bytes.as_deref(), Some(input.as_slice()));
614            }
615            event => panic!("expected parse error event, got {event:?}"),
616        }
617    }
618
619    #[test]
620    fn diagnostic_iterator_parse_error_carries_partial_tree() {
621        // A BGP4MP record whose UPDATE body is truncated: the partial tree
622        // must still walk the header, subheader, and BGP message header.
623        let mut bgp = vec![0xFF; 16];
624        bgp.extend_from_slice(&21u16.to_be_bytes()); // claims 2 body bytes
625        bgp.push(2);
626        bgp.extend_from_slice(&[0x00]); // only 1 of 2 withdrawn-length bytes
627
628        let mut body = Vec::new();
629        body.extend_from_slice(&64496u32.to_be_bytes());
630        body.extend_from_slice(&64497u32.to_be_bytes());
631        body.extend_from_slice(&0u16.to_be_bytes());
632        body.extend_from_slice(&1u16.to_be_bytes());
633        body.extend_from_slice(&[192, 0, 2, 1]);
634        body.extend_from_slice(&[192, 0, 2, 2]);
635        body.extend_from_slice(&bgp);
636
637        let mut wire = CommonHeader {
638            timestamp: 1,
639            microsecond_timestamp: None,
640            entry_type: EntryType::BGP4MP,
641            entry_subtype: Bgp4MpType::MessageAs4 as u16,
642            length: body.len() as u32,
643        }
644        .encode()
645        .to_vec();
646        wire.extend_from_slice(&body);
647
648        match BgpkitParser::from_reader(Cursor::new(wire))
649            .into_diagnostic_iter()
650            .next()
651            .unwrap()
652        {
653            DiagnosticEvent::ParseError { partial, .. } => {
654                let partial = partial.expect("partial tree on parse error");
655                assert!(partial.find("mrt.header.type").is_some());
656                assert!(partial.find("mrt.bgp4mp.peer_asn").is_some());
657                assert!(partial.find("bgp.header.marker").is_some());
658                assert!(partial.find("bgp.update.path_attributes").is_none());
659            }
660            event => panic!("expected parse error event, got {event:?}"),
661        }
662    }
663
664    #[test]
665    fn diagnostic_iterator_terminates_after_framing_error() {
666        let header = CommonHeader {
667            timestamp: 3,
668            microsecond_timestamp: None,
669            entry_type: EntryType::BGP4MP,
670            entry_subtype: Bgp4MpType::MessageAs4 as u16,
671            length: 16 * 1024 * 1024 + 1,
672        };
673        let mut input = header.encode().to_vec();
674        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
675
676        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
677        match iter.next().unwrap() {
678            DiagnosticEvent::ParseError {
679                common_header,
680                raw_bytes,
681                ..
682            } => {
683                assert_eq!(common_header, Some(header));
684                assert_eq!(raw_bytes.as_deref(), Some(header.encode().as_ref()));
685            }
686            event => panic!("expected parse error event, got {event:?}"),
687        }
688        assert!(iter.next().is_none());
689    }
690
691    #[test]
692    fn diagnostic_iterator_continues_after_unknown_entry_type() {
693        // An unknown entry type is skipped whole, so the reader is still on a
694        // record boundary and the following record must be reported.
695        let mut unknown = vec![
696            0, 0, 0, 1, // timestamp
697            0x77, 0x77, // unknown entry type
698            0, 0, // subtype
699            0, 0, 0, 20, // length
700        ];
701        unknown.extend_from_slice(&[0, 0, 0, 1, 0, 5, 0, 7, 0, 0, 0, 32]);
702        unknown.extend_from_slice(&[0xaa; 8]);
703        let mut input = unknown.clone();
704        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
705
706        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
707        match iter.next().unwrap() {
708            DiagnosticEvent::ParseError {
709                error,
710                common_header,
711                raw_bytes,
712                ..
713            } => {
714                assert!(matches!(error, ParserError::ParseError(_)));
715                assert_eq!(common_header, None);
716                assert_eq!(raw_bytes.as_deref(), Some(unknown.as_slice()));
717            }
718            event => panic!("expected parse error event, got {event:?}"),
719        }
720        assert!(matches!(iter.next(), Some(DiagnosticEvent::Record { .. })));
721        assert!(iter.next().is_none());
722    }
723
724    #[test]
725    fn diagnostic_iterator_terminates_after_truncated_unknown_entry_type() {
726        // The declared body outruns the stream, so no boundary is known and the
727        // iterator must not poll again.
728        let input = vec![
729            0, 0, 0, 1, // timestamp
730            0x77, 0x77, // unknown entry type
731            0, 0, // subtype
732            0, 0, 0, 8, // length, but only 4 body bytes follow
733            1, 2, 3, 4,
734        ];
735
736        let mut iter = BgpkitParser::from_reader(Cursor::new(input.clone())).into_diagnostic_iter();
737        match iter.next().unwrap() {
738            DiagnosticEvent::ParseError { raw_bytes, .. } => {
739                assert_eq!(raw_bytes.as_deref(), Some(input.as_slice()));
740            }
741            event => panic!("expected parse error event, got {event:?}"),
742        }
743        assert!(iter.next().is_none());
744    }
745
746    #[test]
747    fn diagnostic_iterator_preserves_invalid_header_bytes() {
748        let invalid_header = vec![
749            0, 0, 0, 1, // timestamp
750            0xff, 0xff, // unknown entry type
751            0, 0, // subtype
752            0, 0, 0, 0, // length
753        ];
754
755        match BgpkitParser::from_reader(Cursor::new(invalid_header.clone()))
756            .into_diagnostic_iter()
757            .next()
758            .unwrap()
759        {
760            DiagnosticEvent::ParseError {
761                common_header,
762                raw_bytes,
763                partial,
764                ..
765            } => {
766                assert_eq!(common_header, None);
767                assert_eq!(raw_bytes.as_deref(), Some(invalid_header.as_slice()));
768                // The bytes still frame a 12-byte header, so the partial tree
769                // walks the header fields but nothing beyond them.
770                let partial = partial.unwrap();
771                assert!(partial.find("mrt.header.type").is_some());
772                assert!(partial.find("mrt.body").is_none());
773            }
774            event => panic!("expected parse error event, got {event:?}"),
775        }
776    }
777
778    #[test]
779    fn diagnostic_iterator_ignores_filters() {
780        let wire = update_record(valid_attributes()).encode().unwrap().to_vec();
781        let parser = BgpkitParser::from_reader(Cursor::new(wire))
782            .add_filter("prefix", "198.51.100.0/24")
783            .unwrap();
784
785        assert!(matches!(
786            parser.into_diagnostic_iter().next(),
787            Some(DiagnosticEvent::Record { .. })
788        ));
789    }
790
791    #[test]
792    fn diagnostic_iterator_reports_wire_level_nlri_warnings_and_continues() {
793        let malformed_nlri = [0xc8, 0x01];
794        let invalid_wire = bgp4mp_update_wire(
795            &malformed_nlri,
796            &valid_update_attributes_wire(),
797            &malformed_nlri,
798        );
799        let mut input = invalid_wire.clone();
800        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
801
802        let mut iter = BgpkitParser::from_reader(Cursor::new(input)).into_diagnostic_iter();
803        match iter.next().unwrap() {
804            DiagnosticEvent::Record { warnings, raw, .. } => {
805                assert!(warnings.iter().any(|warning| {
806                    matches!(
807                        warning,
808                        BgpValidationWarning::MalformedNlri {
809                            nlri_type: "withdrawn",
810                            raw_bytes,
811                            ..
812                        } if raw_bytes == &malformed_nlri
813                    )
814                }));
815                assert!(warnings.iter().any(|warning| {
816                    matches!(
817                        warning,
818                        BgpValidationWarning::MalformedNlri {
819                            nlri_type: "announced",
820                            raw_bytes,
821                            ..
822                        } if raw_bytes == &malformed_nlri
823                    )
824                }));
825                assert_eq!(raw.raw_bytes().as_ref(), invalid_wire.as_slice());
826            }
827            event => panic!("expected record event with warnings, got {event:?}"),
828        }
829        assert!(matches!(iter.next(), Some(DiagnosticEvent::Record { .. })));
830        assert!(iter.next().is_none());
831    }
832
833    #[test]
834    fn diagnostic_iterator_reports_wire_level_attribute_warnings() {
835        let announced = [0x18, 10, 0, 0];
836        let mut flags_and_duplicate = valid_update_attributes_wire();
837        flags_and_duplicate[0] = 0x80;
838        flags_and_duplicate.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
839        assert_wire_validation(
840            bgp4mp_update_wire(&[], &flags_and_duplicate, &announced),
841            |warning| {
842                matches!(
843                    warning,
844                    BgpValidationWarning::AttributeFlagsError {
845                        attr_type: AttrType::ORIGIN,
846                        ..
847                    }
848                )
849            },
850        );
851        assert_wire_validation(
852            bgp4mp_update_wire(&[], &flags_and_duplicate, &announced),
853            |warning| {
854                matches!(
855                    warning,
856                    BgpValidationWarning::DuplicateAttribute {
857                        attr_type: AttrType::ORIGIN
858                    }
859                )
860            },
861        );
862
863        let mut invalid_origin = valid_update_attributes_wire();
864        invalid_origin[3] = 3;
865        assert_wire_validation(
866            bgp4mp_update_wire(&[], &invalid_origin, &announced),
867            |warning| matches!(warning, BgpValidationWarning::MalformedAttributeList { .. }),
868        );
869
870        let malformed_as_path = vec![
871            0x40, 0x01, 0x01, 0x00, // ORIGIN = IGP
872            0x40, 0x02, 0x02, 0x02, 0x01, // AS_PATH segment lacks its ASN
873            0x40, 0x03, 0x04, 192, 0, 2, 254, // NEXT_HOP
874        ];
875        assert_wire_validation(
876            bgp4mp_update_wire(&[], &malformed_as_path, &announced),
877            |warning| matches!(warning, BgpValidationWarning::MalformedAttributeList { .. }),
878        );
879
880        let invalid_next_hop = vec![
881            0x40, 0x01, 0x01, 0x00, // ORIGIN = IGP
882            0x40, 0x02, 0x00, // AS_PATH = empty
883            0x40, 0x03, 0x03, 192, 0, 2, // NEXT_HOP has the wrong length
884        ];
885        assert_wire_validation(
886            bgp4mp_update_wire(&[], &invalid_next_hop, &announced),
887            |warning| {
888                matches!(
889                    warning,
890                    BgpValidationWarning::AttributeLengthError {
891                        attr_type: AttrType::NEXT_HOP,
892                        ..
893                    }
894                )
895            },
896        );
897    }
898
899    #[test]
900    fn diagnostic_iterator_collects_table_dump_batch_and_rib_warnings() {
901        let table_dump_messages = vec![
902            TableDumpMessage {
903                view_number: 0,
904                sequence_number: 1,
905                prefix: NetworkPrefix::from_str("192.0.2.0/24").unwrap(),
906                status: 1,
907                originated_time: 1,
908                peer_ip: IpAddr::from_str("192.0.2.1").unwrap(),
909                peer_asn: Asn::new_16bit(64512),
910                attributes: Attributes::default(),
911            },
912            TableDumpMessage {
913                view_number: 0,
914                sequence_number: 1,
915                prefix: NetworkPrefix::from_str("198.51.100.0/24").unwrap(),
916                status: 1,
917                originated_time: 2,
918                peer_ip: IpAddr::from_str("192.0.2.2").unwrap(),
919                peer_asn: Asn::new_16bit(64513),
920                attributes: Attributes::default(),
921            },
922        ];
923        let table_dump_record = MrtRecord {
924            common_header: CommonHeader {
925                timestamp: 1,
926                microsecond_timestamp: None,
927                entry_type: EntryType::TABLE_DUMP,
928                entry_subtype: 1,
929                length: 0,
930            },
931            message: MrtMessage::TableDumpMessageBatch(table_dump_messages),
932        };
933        let table_dump_wire = table_dump_record.encode().unwrap().to_vec();
934        match BgpkitParser::from_reader(Cursor::new(table_dump_wire.clone()))
935            .into_diagnostic_iter()
936            .next()
937            .unwrap()
938        {
939            DiagnosticEvent::Record {
940                record, warnings, ..
941            } => {
942                assert!(matches!(
943                    record.message,
944                    MrtMessage::TableDumpMessageBatch(_)
945                ));
946                assert_eq!(warnings.len(), 6);
947            }
948            event => panic!("expected record event, got {event:?}"),
949        }
950
951        let rib_record = MrtRecord {
952            common_header: CommonHeader {
953                timestamp: 2,
954                microsecond_timestamp: None,
955                entry_type: EntryType::TABLE_DUMP_V2,
956                entry_subtype: TableDumpV2Type::RibIpv4Unicast as u16,
957                length: 0,
958            },
959            message: MrtMessage::TableDumpV2Message(TableDumpV2Message::RibAfi(RibAfiEntries {
960                rib_type: TableDumpV2Type::RibIpv4Unicast,
961                sequence_number: 1,
962                prefix: NetworkPrefix::from_str("203.0.113.0/24").unwrap(),
963                rib_entries: vec![RibEntry {
964                    peer_index: 0,
965                    originated_time: 1,
966                    path_id: None,
967                    attributes: Attributes::default(),
968                }],
969            })),
970        };
971        let rib_wire = rib_record.encode().unwrap().to_vec();
972        match BgpkitParser::from_reader(Cursor::new(rib_wire))
973            .into_diagnostic_iter()
974            .next()
975            .unwrap()
976        {
977            DiagnosticEvent::Record { warnings, .. } => {
978                assert_eq!(warnings.len(), 3);
979            }
980            event => panic!("expected record event, got {event:?}"),
981        }
982    }
983
984    #[test]
985    fn with_dissection_attaches_tree_and_spans() {
986        // Attribute flags error on ORIGIN: the spanned warning must point at
987        // the first ORIGIN attribute node's byte range.
988        let announced = [0x18, 10, 0, 0];
989        let mut bad_flags = valid_update_attributes_wire();
990        bad_flags[0] = 0x80;
991        let wire = bgp4mp_update_wire(&[], &bad_flags, &announced);
992
993        let mut iter = BgpkitParser::from_reader(Cursor::new(wire))
994            .into_diagnostic_iter()
995            .with_dissection();
996        match iter.next().unwrap() {
997            DissectedDiagnosticEvent::Record { warnings, tree, .. } => {
998                // MRT header, BGP4MP subheader, and BGP layers all present
999                assert!(tree.find("mrt.header.type").is_some());
1000                assert!(tree.find("mrt.bgp4mp.peer_asn").is_some());
1001                assert!(tree.find("bgp.header.type").is_some());
1002                assert!(tree.find("bgp.attr.1").is_some());
1003
1004                let origin = tree.find("bgp.attr.1").unwrap();
1005                assert_eq!(warnings.len(), 1);
1006                assert!(matches!(
1007                    warnings[0].warning,
1008                    BgpValidationWarning::AttributeFlagsError { .. }
1009                ));
1010                assert_eq!(warnings[0].span, origin.span());
1011            }
1012            event => panic!("expected dissected record event, got {event:?}"),
1013        }
1014    }
1015
1016    #[test]
1017    fn with_dissection_anchors_duplicate_to_second_occurrence() {
1018        // Two ORIGIN attributes: the duplicate warning and its companion
1019        // flags warning both belong to the SECOND occurrence.
1020        let mut attrs = Vec::new();
1021        attrs.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]); // first, clean
1022        attrs.extend_from_slice(&[0x80, 0x01, 0x01, 0x00]); // duplicate, bad flags
1023        let announced = [0x18, 10, 0, 0];
1024        let wire = bgp4mp_update_wire(&[], &attrs, &announced);
1025
1026        let mut iter = BgpkitParser::from_reader(Cursor::new(wire))
1027            .into_diagnostic_iter()
1028            .with_dissection();
1029        match iter.next().unwrap() {
1030            DissectedDiagnosticEvent::Record { warnings, tree, .. } => {
1031                let mut origins = Vec::new();
1032                tree.find_all("bgp.attr.1", &mut origins);
1033                assert_eq!(origins.len(), 2, "two ORIGIN attributes on the wire");
1034
1035                let duplicate = warnings
1036                    .iter()
1037                    .find(|w| matches!(w.warning, BgpValidationWarning::DuplicateAttribute { .. }))
1038                    .expect("duplicate warning");
1039                // NTH-occurrence anchoring: the duplicate points at #2
1040                assert_eq!(duplicate.span, origins[1].span());
1041
1042                // The flags error accompanying the duplicate header must
1043                // also anchor to the second occurrence, not the first.
1044                let flags = warnings
1045                    .iter()
1046                    .find(|w| matches!(w.warning, BgpValidationWarning::AttributeFlagsError { .. }))
1047                    .expect("flags warning");
1048                assert_eq!(flags.span, origins[1].span());
1049            }
1050            event => panic!("expected dissected record event, got {event:?}"),
1051        }
1052    }
1053
1054    #[test]
1055    fn with_dissection_passes_through_parse_error_partial() {
1056        // A truncated record still yields a ParseError carrying the partial
1057        // tree through the dissecting iterator.
1058        let header = CommonHeader {
1059            timestamp: 3,
1060            microsecond_timestamp: None,
1061            entry_type: EntryType::TABLE_DUMP,
1062            entry_subtype: 0,
1063            length: 4,
1064        };
1065        let mut input = header.encode().to_vec();
1066        input.extend_from_slice(&[0xff; 4]);
1067        input.extend_from_slice(&update_record(valid_attributes()).encode().unwrap());
1068
1069        let mut iter = BgpkitParser::from_reader(Cursor::new(input))
1070            .into_diagnostic_iter()
1071            .with_dissection();
1072        match iter.next().unwrap() {
1073            DissectedDiagnosticEvent::ParseError { partial, .. } => {
1074                assert!(partial.is_some());
1075            }
1076            event => panic!("expected dissected parse error event, got {event:?}"),
1077        }
1078        assert!(matches!(
1079            iter.next(),
1080            Some(DissectedDiagnosticEvent::Record { .. })
1081        ));
1082    }
1083
1084    #[test]
1085    fn record_validation_warnings_reachable_from_crate_root() {
1086        // Regression: the helper must be re-exported, not just declared pub
1087        // in a private module. It reports the findings stored on the parsed
1088        // attributes, so attach one directly.
1089        let record = update_record(valid_attributes());
1090        assert!(crate::record_validation_warnings(&record).is_empty());
1091
1092        let mut attributes = Attributes::default();
1093        attributes.add_validation_warning(BgpValidationWarning::MissingWellKnownAttribute {
1094            attr_type: AttrType::ORIGIN,
1095        });
1096        let flagged = update_record(attributes);
1097        assert_eq!(crate::record_validation_warnings(&flagged).len(), 1);
1098    }
1099
1100    #[test]
1101    fn with_dissection_spans_nlri_warning() {
1102        let malformed_nlri = [0xc8, 0x01];
1103        let mut body = Vec::new();
1104        body.extend_from_slice(&(malformed_nlri.len() as u16).to_be_bytes());
1105        body.extend_from_slice(&malformed_nlri);
1106        body.extend_from_slice(&(valid_update_attributes_wire().len() as u16).to_be_bytes());
1107        body.extend_from_slice(&valid_update_attributes_wire());
1108        let wire = bgp4mp_message_wire(BgpMessageType::UPDATE, &body);
1109
1110        let mut iter = BgpkitParser::from_reader(Cursor::new(wire))
1111            .into_diagnostic_iter()
1112            .with_dissection();
1113        match iter.next().unwrap() {
1114            DissectedDiagnosticEvent::Record { warnings, tree, .. } => {
1115                let withdrawn = tree.find("bgp.update.withdrawn_routes").unwrap();
1116                let warning = warnings
1117                    .iter()
1118                    .find(|w| {
1119                        matches!(
1120                            w.warning,
1121                            BgpValidationWarning::MalformedNlri {
1122                                nlri_type: "withdrawn",
1123                                ..
1124                            }
1125                        )
1126                    })
1127                    .expect("withdrawn NLRI warning");
1128                assert_eq!(warning.span, withdrawn.span());
1129            }
1130            event => panic!("expected dissected record event, got {event:?}"),
1131        }
1132    }
1133}