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