Skip to main content

bgpkit_parser/parser/iters/
update.rs

1/*!
2Update message iterator implementation.
3
4This module provides iterators that yield BGP announcement data from MRT files,
5supporting BGP4MP and deprecated MRT Type 5 UPDATE messages and RIB dump entries.
6
7## Overview
8
9The iterators in this module provide a middle ground between `MrtRecord` and `BgpElem`:
10- More focused than `MrtRecord` as they only yield BGP announcements
11- More efficient than `BgpElem` as they avoid duplicating attributes for each prefix
12
13## Message Types
14
15### BGP4MP Updates (from UPDATES files)
16- One message contains multiple prefixes sharing the SAME attributes
17- Efficient when you need to process updates without per-prefix attribute cloning
18
19### RIB Entries (from RIB dump files)
20- One record contains ONE prefix with multiple RIB entries (one per peer)
21- Each peer has its own attributes for the same prefix
22
23## Usage
24
25```no_run
26use bgpkit_parser::BgpkitParser;
27
28let parser = BgpkitParser::new("updates.mrt").unwrap();
29for announcement in parser.into_update_iter() {
30    match announcement {
31        bgpkit_parser::MrtUpdate::Bgp4MpUpdate(update) => {
32            println!("BGP UPDATE from peer {}", update.peer_ip);
33        }
34        bgpkit_parser::MrtUpdate::LegacyBgpUpdate(update) => {
35            println!("Legacy BGP UPDATE from peer {}", update.peer_ip);
36        }
37        bgpkit_parser::MrtUpdate::TableDumpV2Entry(entry) => {
38            println!("RIB entry for prefix {}", entry.prefix);
39        }
40        bgpkit_parser::MrtUpdate::TableDumpMessage(msg) => {
41            println!("Legacy table dump for prefix {}", msg.prefix);
42        }
43    }
44}
45```
46*/
47use crate::error::ParserError;
48use crate::models::*;
49use crate::parser::iters::write_mrt_core_dump;
50use crate::parser::BgpkitParser;
51use crate::Elementor;
52use log::{error, warn};
53use std::io::Read;
54use std::net::IpAddr;
55
56/// A BGP4MP UPDATE message with associated metadata.
57///
58/// This struct wraps a `BgpUpdateMessage` with the peer information and timestamp
59/// from the MRT record. It's more efficient than `BgpElem` when a single UPDATE
60/// contains multiple prefixes, as the attributes are not duplicated.
61#[derive(Debug, Clone, PartialEq)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct Bgp4MpUpdate {
64    /// The timestamp of the MRT record in floating-point format (seconds since epoch).
65    pub timestamp: f64,
66    /// The IP address of the BGP peer that sent this update.
67    pub peer_ip: IpAddr,
68    /// The ASN of the BGP peer that sent this update.
69    pub peer_asn: Asn,
70    /// The BGP UPDATE message containing announcements, withdrawals, and attributes.
71    pub message: BgpUpdateMessage,
72}
73
74/// A deprecated MRT Type 5 BGP_UPDATE message with endpoint metadata.
75///
76/// The wire format is defined in [RFC 6396, Appendix B.2.1.2]: peer AS,
77/// peer IPv4 address, local AS, local IPv4 address, followed by the BGP UPDATE
78/// contents without the BGP message header.
79///
80/// [RFC 6396, Appendix B.2.1.2]: https://www.rfc-editor.org/rfc/rfc6396.html#appendix-B.2.1.2
81#[derive(Debug, Clone, PartialEq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub struct LegacyBgpUpdate {
84    pub timestamp: f64,
85    pub peer_ip: IpAddr,
86    pub peer_asn: Asn,
87    pub local_ip: IpAddr,
88    pub local_asn: Asn,
89    pub message: BgpUpdateMessage,
90}
91
92/// A TableDumpV2 RIB entry with associated metadata.
93///
94/// This struct represents a single prefix with all its RIB entries from different peers.
95/// Each RIB entry contains the peer information and attributes for that prefix.
96#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct TableDumpV2Entry {
99    /// The timestamp from the MRT header.
100    pub timestamp: f64,
101    /// The RIB subtype (IPv4 Unicast, IPv6 Unicast, etc.)
102    pub rib_type: TableDumpV2Type,
103    /// The sequence number of this RIB entry.
104    pub sequence_number: u32,
105    /// The network prefix for this RIB entry.
106    pub prefix: NetworkPrefix,
107    /// The RIB entries for this prefix, one per peer.
108    /// Each entry contains peer_index, originated_time, and attributes.
109    pub rib_entries: Vec<RibEntry>,
110}
111
112/// Unified enum representing BGP announcements from different MRT message types.
113///
114/// This enum provides a common interface for processing BGP data from:
115/// - BGP4MP UPDATE messages (real-time updates)
116/// - TableDumpV2 RIB entries (routing table snapshots)
117/// - Legacy TableDump messages
118#[derive(Debug, Clone, PartialEq)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub enum MrtUpdate {
121    /// A BGP4MP UPDATE message from an UPDATES file.
122    Bgp4MpUpdate(Bgp4MpUpdate),
123    /// A deprecated MRT Type 5 UPDATE message.
124    LegacyBgpUpdate(LegacyBgpUpdate),
125    /// A TableDumpV2 RIB entry from a RIB dump file.
126    TableDumpV2Entry(TableDumpV2Entry),
127    /// A legacy TableDump (v1) message.
128    TableDumpMessage(TableDumpMessage),
129}
130
131impl MrtUpdate {
132    /// Returns the timestamp of this update/entry.
133    pub fn timestamp(&self) -> f64 {
134        match self {
135            MrtUpdate::Bgp4MpUpdate(u) => u.timestamp,
136            MrtUpdate::LegacyBgpUpdate(u) => u.timestamp,
137            MrtUpdate::TableDumpV2Entry(e) => e.timestamp,
138            MrtUpdate::TableDumpMessage(m) => m.originated_time as f64,
139        }
140    }
141}
142
143/// Iterator over BGP announcements from MRT data.
144///
145/// This iterator yields `MrtUpdate` items from both UPDATES files (BGP4MP messages)
146/// and RIB dump files (TableDump/TableDumpV2 messages).
147///
148/// Unlike `ElemIterator`, this iterator does not expand messages into individual
149/// `BgpElem`s, making it more efficient for use cases that need to process
150/// the raw message structures.
151pub struct UpdateIterator<R> {
152    parser: BgpkitParser<R>,
153    elementor: Elementor,
154    pending_table_dump: Vec<TableDumpMessage>,
155}
156
157impl<R> UpdateIterator<R> {
158    pub(crate) fn new(parser: BgpkitParser<R>) -> Self {
159        UpdateIterator {
160            parser,
161            elementor: Elementor::new(),
162            pending_table_dump: Vec::new(),
163        }
164    }
165}
166
167impl<R: Read> Iterator for UpdateIterator<R> {
168    type Item = MrtUpdate;
169
170    fn next(&mut self) -> Option<MrtUpdate> {
171        // Text-dump parsers have no MRT-record representation; short-circuit
172        // instead of spinning forever on Unsupported errors from next_record().
173        if self.parser.text_dump_iter.is_some() {
174            return None;
175        }
176        loop {
177            if let Some(message) = self.pending_table_dump.pop() {
178                return Some(MrtUpdate::TableDumpMessage(message));
179            }
180            let record = match self.parser.next_record() {
181                Ok(record) => record,
182                Err(e) => match e.error {
183                    ParserError::TruncatedMsg(err_str) | ParserError::Unsupported(err_str) => {
184                        if self.parser.options.show_warnings {
185                            warn!("parser warn: {}", err_str);
186                        }
187                        write_mrt_core_dump(self.parser.core_dump, e.bytes);
188                        continue;
189                    }
190                    ParserError::ParseError(err_str) => {
191                        error!("parser error: {}", err_str);
192                        write_mrt_core_dump(self.parser.core_dump, e.bytes);
193                        if self.parser.core_dump {
194                            return None;
195                        }
196                        continue;
197                    }
198                    ParserError::EofExpected => return None,
199                    ParserError::IoError(err) | ParserError::EofError(err) => {
200                        error!("{:?}", err);
201                        write_mrt_core_dump(self.parser.core_dump, e.bytes);
202                        return None;
203                    }
204                    #[cfg(feature = "oneio")]
205                    ParserError::OneIoError(_) => return None,
206                    ParserError::FilterError(_) => return None,
207                    // Labeled NLRI parsing errors - treat as malformed and skip
208                    ParserError::InvalidLabeledNlriLength
209                    | ParserError::TruncatedLabeledNlri
210                    | ParserError::TruncatedPrefix
211                    | ParserError::MaxLabelStackDepthExceeded
212                    | ParserError::PeerMaxLabelsExceeded
213                    | ParserError::InvalidPrefix => {
214                        if self.parser.options.show_warnings {
215                            warn!("parser warn: labeled NLRI parsing error: {:?}", e.error);
216                        }
217                        continue;
218                    }
219                },
220            };
221
222            let t = record.common_header.timestamp;
223            let timestamp: f64 = if let Some(micro) = &record.common_header.microsecond_timestamp {
224                let m = (*micro as f64) / 1_000_000.0;
225                t as f64 + m
226            } else {
227                f64::from(t)
228            };
229
230            match record.message {
231                MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(msg)) => {
232                    if let BgpMessage::Update(update) = msg.bgp_message {
233                        return Some(MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
234                            timestamp,
235                            peer_ip: msg.peer_ip,
236                            peer_asn: msg.peer_asn,
237                            message: update,
238                        }));
239                    }
240                    // Not an UPDATE message (OPEN, NOTIFICATION, KEEPALIVE), continue
241                    continue;
242                }
243                MrtMessage::Bgp4Mp(Bgp4MpEnum::StateChange(_)) => {
244                    // State change messages don't contain announcement data
245                    continue;
246                }
247                MrtMessage::LegacyBgp(LegacyBgp::Message(msg)) => {
248                    if let BgpMessage::Update(update) = msg.bgp_message {
249                        return Some(MrtUpdate::LegacyBgpUpdate(LegacyBgpUpdate {
250                            timestamp,
251                            peer_ip: msg.peer_ip,
252                            peer_asn: msg.peer_asn,
253                            local_ip: msg.local_ip,
254                            local_asn: msg.local_asn,
255                            message: update,
256                        }));
257                    }
258                    continue;
259                }
260                MrtMessage::LegacyBgp(LegacyBgp::StateChange(_)) => continue,
261                MrtMessage::TableDumpV2Message(msg) => {
262                    match msg {
263                        TableDumpV2Message::PeerIndexTable(p) => {
264                            // Store peer table for later use and continue
265                            self.elementor.peer_table = Some(p);
266                            continue;
267                        }
268                        TableDumpV2Message::RibAfi(entries) => {
269                            return Some(MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
270                                timestamp,
271                                rib_type: entries.rib_type,
272                                sequence_number: entries.sequence_number,
273                                prefix: entries.prefix,
274                                rib_entries: entries.rib_entries,
275                            }));
276                        }
277                        TableDumpV2Message::RibGeneric(_) => {
278                            // RibGeneric is not commonly used, skip for now
279                            continue;
280                        }
281                        TableDumpV2Message::GeoPeerTable(_) => {
282                            // GeoPeerTable doesn't contain route data
283                            continue;
284                        }
285                    }
286                }
287                MrtMessage::TableDumpMessage(msg) => {
288                    return Some(MrtUpdate::TableDumpMessage(msg));
289                }
290                MrtMessage::TableDumpMessageBatch(mut messages) => {
291                    messages.reverse();
292                    self.pending_table_dump = messages;
293                    continue;
294                }
295            }
296        }
297    }
298}
299
300/// Fallible iterator over BGP announcements that returns parsing errors.
301///
302/// Unlike the default `UpdateIterator`, this iterator returns `Result<MrtUpdate, ParserErrorWithBytes>`
303/// allowing users to handle parsing errors explicitly instead of having them logged and skipped.
304pub struct FallibleUpdateIterator<R> {
305    parser: BgpkitParser<R>,
306    elementor: Elementor,
307    pending_table_dump: Vec<TableDumpMessage>,
308}
309
310impl<R> FallibleUpdateIterator<R> {
311    pub(crate) fn new(parser: BgpkitParser<R>) -> Self {
312        FallibleUpdateIterator {
313            parser,
314            elementor: Elementor::new(),
315            pending_table_dump: Vec::new(),
316        }
317    }
318}
319
320impl<R: Read> Iterator for FallibleUpdateIterator<R> {
321    type Item = Result<MrtUpdate, crate::error::ParserErrorWithBytes>;
322
323    fn next(&mut self) -> Option<Self::Item> {
324        // Text-dump parsers have no MRT-record representation; short-circuit.
325        if self.parser.text_dump_iter.is_some() {
326            return None;
327        }
328        loop {
329            if let Some(message) = self.pending_table_dump.pop() {
330                return Some(Ok(MrtUpdate::TableDumpMessage(message)));
331            }
332            match self.parser.next_record() {
333                Ok(record) => {
334                    let t = record.common_header.timestamp;
335                    let timestamp: f64 =
336                        if let Some(micro) = &record.common_header.microsecond_timestamp {
337                            let m = (*micro as f64) / 1_000_000.0;
338                            t as f64 + m
339                        } else {
340                            f64::from(t)
341                        };
342
343                    match record.message {
344                        MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(msg)) => {
345                            if let BgpMessage::Update(update) = msg.bgp_message {
346                                return Some(Ok(MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
347                                    timestamp,
348                                    peer_ip: msg.peer_ip,
349                                    peer_asn: msg.peer_asn,
350                                    message: update,
351                                })));
352                            }
353                            continue;
354                        }
355                        MrtMessage::Bgp4Mp(Bgp4MpEnum::StateChange(_)) => {
356                            continue;
357                        }
358                        MrtMessage::LegacyBgp(LegacyBgp::Message(msg)) => {
359                            if let BgpMessage::Update(update) = msg.bgp_message {
360                                return Some(Ok(MrtUpdate::LegacyBgpUpdate(LegacyBgpUpdate {
361                                    timestamp,
362                                    peer_ip: msg.peer_ip,
363                                    peer_asn: msg.peer_asn,
364                                    local_ip: msg.local_ip,
365                                    local_asn: msg.local_asn,
366                                    message: update,
367                                })));
368                            }
369                            continue;
370                        }
371                        MrtMessage::LegacyBgp(LegacyBgp::StateChange(_)) => continue,
372                        MrtMessage::TableDumpV2Message(msg) => match msg {
373                            TableDumpV2Message::PeerIndexTable(p) => {
374                                self.elementor.peer_table = Some(p);
375                                continue;
376                            }
377                            TableDumpV2Message::RibAfi(entries) => {
378                                return Some(Ok(MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
379                                    timestamp,
380                                    rib_type: entries.rib_type,
381                                    sequence_number: entries.sequence_number,
382                                    prefix: entries.prefix,
383                                    rib_entries: entries.rib_entries,
384                                })));
385                            }
386                            TableDumpV2Message::RibGeneric(_) => {
387                                continue;
388                            }
389                            TableDumpV2Message::GeoPeerTable(_) => {
390                                continue;
391                            }
392                        },
393                        MrtMessage::TableDumpMessage(msg) => {
394                            return Some(Ok(MrtUpdate::TableDumpMessage(msg)));
395                        }
396                        MrtMessage::TableDumpMessageBatch(mut messages) => {
397                            messages.reverse();
398                            self.pending_table_dump = messages;
399                            continue;
400                        }
401                    }
402                }
403                Err(e) if matches!(e.error, ParserError::EofExpected) => {
404                    return None;
405                }
406                Err(e) => {
407                    return Some(Err(e));
408                }
409            }
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use std::io::Cursor;
418
419    #[test]
420    fn test_bgp4mp_update_struct() {
421        let update = Bgp4MpUpdate {
422            timestamp: 1234567890.123456,
423            peer_ip: "192.0.2.1".parse().unwrap(),
424            peer_asn: Asn::new_32bit(65000),
425            message: BgpUpdateMessage::default(),
426        };
427
428        assert_eq!(update.timestamp, 1234567890.123456);
429        assert_eq!(update.peer_ip.to_string(), "192.0.2.1");
430        assert_eq!(update.peer_asn, Asn::new_32bit(65000));
431    }
432
433    #[test]
434    fn test_table_dump_v2_entry_struct() {
435        let entry = TableDumpV2Entry {
436            timestamp: 1234567890.0,
437            rib_type: TableDumpV2Type::RibIpv4Unicast,
438            sequence_number: 42,
439            prefix: "10.0.0.0/8".parse().unwrap(),
440            rib_entries: vec![],
441        };
442
443        assert_eq!(entry.timestamp, 1234567890.0);
444        assert_eq!(entry.rib_type, TableDumpV2Type::RibIpv4Unicast);
445        assert_eq!(entry.sequence_number, 42);
446        assert_eq!(entry.prefix.to_string(), "10.0.0.0/8");
447        assert!(entry.rib_entries.is_empty());
448    }
449
450    #[test]
451    fn test_mrt_update_timestamp() {
452        // Test Bgp4MpUpdate variant
453        let bgp4mp = MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
454            timestamp: 1234567890.5,
455            peer_ip: "192.0.2.1".parse().unwrap(),
456            peer_asn: Asn::new_32bit(65000),
457            message: BgpUpdateMessage::default(),
458        });
459        assert_eq!(bgp4mp.timestamp(), 1234567890.5);
460
461        // Test TableDumpV2Entry variant
462        let table_dump_v2 = MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
463            timestamp: 1234567891.5,
464            rib_type: TableDumpV2Type::RibIpv4Unicast,
465            sequence_number: 1,
466            prefix: "10.0.0.0/8".parse().unwrap(),
467            rib_entries: vec![],
468        });
469        assert_eq!(table_dump_v2.timestamp(), 1234567891.5);
470
471        // Test TableDumpMessage variant
472        let table_dump_v1 = MrtUpdate::TableDumpMessage(TableDumpMessage {
473            view_number: 0,
474            sequence_number: 1,
475            prefix: "192.168.0.0/16".parse().unwrap(),
476            status: 1,
477            originated_time: 1234567892,
478            peer_ip: "10.0.0.1".parse().unwrap(),
479            peer_asn: Asn::new_32bit(65001),
480            attributes: Attributes::default(),
481        });
482        assert_eq!(table_dump_v1.timestamp(), 1234567892.0);
483    }
484
485    #[test]
486    fn test_update_iterator_empty() {
487        let cursor = Cursor::new(vec![]);
488        let parser = BgpkitParser::from_reader(cursor);
489        let mut iter = UpdateIterator::new(parser);
490
491        assert!(iter.next().is_none());
492    }
493
494    #[test]
495    fn test_fallible_update_iterator_empty() {
496        let cursor = Cursor::new(vec![]);
497        let parser = BgpkitParser::from_reader(cursor);
498        let mut iter = FallibleUpdateIterator::new(parser);
499
500        assert!(iter.next().is_none());
501    }
502
503    #[test]
504    fn test_bgp4mp_update_clone_and_debug() {
505        let update = Bgp4MpUpdate {
506            timestamp: 1234567890.123456,
507            peer_ip: "192.0.2.1".parse().unwrap(),
508            peer_asn: Asn::new_32bit(65000),
509            message: BgpUpdateMessage::default(),
510        };
511
512        // Test Clone
513        let cloned = update.clone();
514        assert_eq!(update, cloned);
515
516        // Test Debug
517        let debug_str = format!("{:?}", update);
518        assert!(debug_str.contains("Bgp4MpUpdate"));
519        assert!(debug_str.contains("192.0.2.1"));
520    }
521
522    #[test]
523    fn test_table_dump_v2_entry_clone_and_debug() {
524        let entry = TableDumpV2Entry {
525            timestamp: 1234567890.0,
526            rib_type: TableDumpV2Type::RibIpv4Unicast,
527            sequence_number: 42,
528            prefix: "10.0.0.0/8".parse().unwrap(),
529            rib_entries: vec![],
530        };
531
532        // Test Clone
533        let cloned = entry.clone();
534        assert_eq!(entry, cloned);
535
536        // Test Debug
537        let debug_str = format!("{:?}", entry);
538        assert!(debug_str.contains("TableDumpV2Entry"));
539        assert!(debug_str.contains("10.0.0.0/8"));
540    }
541
542    #[test]
543    fn test_mrt_update_clone_and_debug() {
544        let update = MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
545            timestamp: 1234567890.5,
546            peer_ip: "192.0.2.1".parse().unwrap(),
547            peer_asn: Asn::new_32bit(65000),
548            message: BgpUpdateMessage::default(),
549        });
550
551        // Test Clone
552        let cloned = update.clone();
553        assert_eq!(update, cloned);
554
555        // Test Debug
556        let debug_str = format!("{:?}", update);
557        assert!(debug_str.contains("Bgp4MpUpdate"));
558    }
559
560    #[test]
561    fn test_fallible_update_iterator_with_invalid_data() {
562        // Create invalid MRT data that will trigger a parsing error
563        let invalid_data = vec![
564            0x00, 0x00, 0x00, 0x00, // timestamp
565            0xFF, 0xFF, // invalid type
566            0x00, 0x00, // subtype
567            0x00, 0x00, 0x00, 0x04, // length
568            0x00, 0x00, 0x00, 0x00, // dummy data
569        ];
570
571        let cursor = Cursor::new(invalid_data);
572        let parser = BgpkitParser::from_reader(cursor);
573        let mut iter = FallibleUpdateIterator::new(parser);
574
575        // First item should be an error
576        let result = iter.next();
577        assert!(result.is_some());
578        assert!(result.unwrap().is_err());
579    }
580
581    #[test]
582    fn test_mrt_update_enum_variants() {
583        // Test that all enum variants can be constructed and matched
584        let updates: Vec<MrtUpdate> = vec![
585            MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
586                timestamp: 1.0,
587                peer_ip: "192.0.2.1".parse().unwrap(),
588                peer_asn: Asn::new_32bit(65000),
589                message: BgpUpdateMessage::default(),
590            }),
591            MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
592                timestamp: 2.0,
593                rib_type: TableDumpV2Type::RibIpv6Unicast,
594                sequence_number: 1,
595                prefix: "2001:db8::/32".parse().unwrap(),
596                rib_entries: vec![],
597            }),
598            MrtUpdate::TableDumpMessage(TableDumpMessage {
599                view_number: 0,
600                sequence_number: 1,
601                prefix: "10.0.0.0/8".parse().unwrap(),
602                status: 1,
603                originated_time: 3,
604                peer_ip: "10.0.0.1".parse().unwrap(),
605                peer_asn: Asn::new_32bit(65001),
606                attributes: Attributes::default(),
607            }),
608        ];
609
610        for (i, update) in updates.iter().enumerate() {
611            match update {
612                MrtUpdate::Bgp4MpUpdate(_) => assert_eq!(i, 0),
613                MrtUpdate::LegacyBgpUpdate(_) => panic!("not part of this fixture"),
614                MrtUpdate::TableDumpV2Entry(_) => assert_eq!(i, 1),
615                MrtUpdate::TableDumpMessage(_) => assert_eq!(i, 2),
616            }
617        }
618    }
619
620    #[test]
621    #[cfg(feature = "serde")]
622    fn test_bgp4mp_update_serde() {
623        let update = Bgp4MpUpdate {
624            timestamp: 1234567890.123456,
625            peer_ip: "192.0.2.1".parse().unwrap(),
626            peer_asn: Asn::new_32bit(65000),
627            message: BgpUpdateMessage::default(),
628        };
629
630        let serialized = serde_json::to_string(&update).unwrap();
631        let deserialized: Bgp4MpUpdate = serde_json::from_str(&serialized).unwrap();
632        assert_eq!(update, deserialized);
633    }
634
635    #[test]
636    #[cfg(feature = "serde")]
637    fn test_table_dump_v2_entry_serde() {
638        let entry = TableDumpV2Entry {
639            timestamp: 1234567890.0,
640            rib_type: TableDumpV2Type::RibIpv4Unicast,
641            sequence_number: 42,
642            prefix: "10.0.0.0/8".parse().unwrap(),
643            rib_entries: vec![],
644        };
645
646        let serialized = serde_json::to_string(&entry).unwrap();
647        let deserialized: TableDumpV2Entry = serde_json::from_str(&serialized).unwrap();
648        assert_eq!(entry, deserialized);
649    }
650
651    #[test]
652    #[cfg(feature = "serde")]
653    fn test_mrt_update_serde() {
654        let update = MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
655            timestamp: 1234567890.5,
656            peer_ip: "192.0.2.1".parse().unwrap(),
657            peer_asn: Asn::new_32bit(65000),
658            message: BgpUpdateMessage::default(),
659        });
660
661        let serialized = serde_json::to_string(&update).unwrap();
662        let deserialized: MrtUpdate = serde_json::from_str(&serialized).unwrap();
663        assert_eq!(update, deserialized);
664    }
665
666    /// Test parsing real UPDATES file data
667    #[test]
668    fn test_update_iterator_with_updates_file() {
669        let url = "https://spaces.bgpkit.org/parser/update-example";
670        let parser = BgpkitParser::new(url).unwrap();
671
672        let mut bgp4mp_count = 0;
673        let mut total_announced = 0;
674        let mut total_withdrawn = 0;
675
676        for update in parser.into_update_iter() {
677            match update {
678                MrtUpdate::Bgp4MpUpdate(u) => {
679                    bgp4mp_count += 1;
680                    total_announced += u.message.announced_prefixes.len();
681                    total_withdrawn += u.message.withdrawn_prefixes.len();
682                    // Also count MP_REACH/MP_UNREACH prefixes
683                    for attr in &u.message.attributes {
684                        match attr {
685                            AttributeValue::MpReachNlri(nlri) => {
686                                total_announced += nlri.prefixes.len();
687                            }
688                            AttributeValue::MpUnreachNlri(nlri) => {
689                                total_withdrawn += nlri.prefixes.len();
690                            }
691                            _ => {}
692                        }
693                    }
694                }
695                MrtUpdate::TableDumpV2Entry(_) => {
696                    panic!("Should not see TableDumpV2Entry in UPDATES file");
697                }
698                MrtUpdate::TableDumpMessage(_) => {
699                    panic!("Should not see TableDumpMessage in UPDATES file");
700                }
701                MrtUpdate::LegacyBgpUpdate(_) => {
702                    panic!("Should not see a legacy BGP update in this modern fixture");
703                }
704            }
705        }
706
707        // Verify we got some data
708        assert!(bgp4mp_count > 0, "Should have parsed some BGP4MP updates");
709        assert!(
710            total_announced + total_withdrawn > 0,
711            "Should have some prefixes"
712        );
713    }
714
715    /// Test parsing real RIB dump file data
716    #[test]
717    fn test_update_iterator_with_rib_file() {
718        let url = "https://spaces.bgpkit.org/parser/rib-example-small.bz2";
719        let parser = BgpkitParser::new(url).unwrap();
720
721        let mut rib_entry_count = 0;
722        let mut total_rib_entries = 0;
723
724        for update in parser.into_update_iter().take(100) {
725            match update {
726                MrtUpdate::Bgp4MpUpdate(_) => {
727                    panic!("Should not see Bgp4MpUpdate in RIB file");
728                }
729                MrtUpdate::LegacyBgpUpdate(_) => {
730                    panic!("Should not see LegacyBgpUpdate in RIB file");
731                }
732                MrtUpdate::TableDumpV2Entry(e) => {
733                    rib_entry_count += 1;
734                    total_rib_entries += e.rib_entries.len();
735                    // Verify the entry has valid data
736                    assert!(e.sequence_number > 0 || rib_entry_count == 1);
737                }
738                MrtUpdate::TableDumpMessage(_) => {
739                    // Legacy format is also acceptable in RIB files
740                }
741            }
742        }
743
744        // Verify we got some data
745        assert!(rib_entry_count > 0, "Should have parsed some RIB entries");
746        assert!(
747            total_rib_entries > 0,
748            "Should have some RIB entries per prefix"
749        );
750    }
751
752    /// Test fallible iterator with real data
753    #[test]
754    fn test_fallible_update_iterator_with_updates_file() {
755        let url = "https://spaces.bgpkit.org/parser/update-example";
756        let parser = BgpkitParser::new(url).unwrap();
757
758        let mut success_count = 0;
759        let mut error_count = 0;
760
761        for result in parser.into_fallible_update_iter() {
762            match result {
763                Ok(_) => success_count += 1,
764                Err(_) => error_count += 1,
765            }
766        }
767
768        assert!(
769            success_count > 0,
770            "Should have parsed some updates successfully"
771        );
772        // The test file should be valid, so we expect no errors
773        assert_eq!(
774            error_count, 0,
775            "Should have no parsing errors in valid file"
776        );
777    }
778
779    /// Test that UpdateIterator and ElemIterator yield consistent prefix counts
780    #[test]
781    fn test_update_iter_vs_elem_iter_consistency() {
782        let url = "https://spaces.bgpkit.org/parser/update-example";
783
784        // Count prefixes using UpdateIterator
785        let parser1 = BgpkitParser::new(url).unwrap();
786        let mut update_iter_announced = 0;
787        let mut update_iter_withdrawn = 0;
788
789        for update in parser1.into_update_iter() {
790            if let MrtUpdate::Bgp4MpUpdate(u) = update {
791                update_iter_announced += u.message.announced_prefixes.len();
792                update_iter_withdrawn += u.message.withdrawn_prefixes.len();
793                for attr in &u.message.attributes {
794                    match attr {
795                        AttributeValue::MpReachNlri(nlri) => {
796                            update_iter_announced += nlri.prefixes.len();
797                        }
798                        AttributeValue::MpUnreachNlri(nlri) => {
799                            update_iter_withdrawn += nlri.prefixes.len();
800                        }
801                        _ => {}
802                    }
803                }
804            }
805        }
806
807        // Count prefixes using ElemIterator
808        let parser2 = BgpkitParser::new(url).unwrap();
809        let mut elem_iter_announced = 0;
810        let mut elem_iter_withdrawn = 0;
811
812        for elem in parser2.into_elem_iter() {
813            match elem.elem_type {
814                ElemType::ANNOUNCE => elem_iter_announced += 1,
815                ElemType::WITHDRAW => elem_iter_withdrawn += 1,
816            }
817        }
818
819        // Counts should match
820        assert_eq!(
821            update_iter_announced, elem_iter_announced,
822            "Announced prefix counts should match"
823        );
824        assert_eq!(
825            update_iter_withdrawn, elem_iter_withdrawn,
826            "Withdrawn prefix counts should match"
827        );
828    }
829}