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