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