Skip to main content

bgpkit_parser/parser/mrt/
mrt_elem.rs

1#![allow(unused)]
2//! This module handles converting MRT records into individual per-prefix BGP elements.
3//!
4//! Each MRT record may contain reachability information for multiple prefixes. This module breaks
5//! down MRT records into corresponding BGP elements, and thus allowing users to more conveniently
6//! process BGP information on a per-prefix basis.
7use crate::models::*;
8use crate::parser::bgp::messages::parse_bgp_update_message;
9use crate::ParserError;
10use crate::ParserError::ParseError;
11use bytes::Bytes;
12use itertools::Itertools;
13use log::{error, warn};
14use std::collections::HashMap;
15use std::fmt::{Display, Formatter};
16use std::net::{IpAddr, Ipv4Addr};
17
18#[derive(Default, Debug, Clone)]
19pub struct Elementor {
20    pub peer_table: Option<PeerIndexTable>,
21}
22
23/// Error returned by [`Elementor::record_to_elems_iter`].
24#[derive(Debug)]
25pub enum ElemError {
26    /// The record contains a [`PeerIndexTable`]. The contained table can be
27    /// passed to [`Elementor::with_peer_table`] to create an initialized elementor.
28    UnexpectedPeerIndexTable(Box<PeerIndexTable>),
29    /// A peer table is required for processing TableDumpV2 RIB entries,
30    /// but none has been set on this elementor.
31    MissingPeerTable,
32    /// The record contains a [`RibGenericEntries`] which is not yet supported.
33    UnsupportedRibGeneric,
34}
35
36impl Display for ElemError {
37    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38        match self {
39            ElemError::UnexpectedPeerIndexTable(_) => {
40                write!(f, "unexpected PeerIndexTable record")
41            }
42            ElemError::MissingPeerTable => {
43                write!(
44                    f,
45                    "peer table not set; call set_peer_table or use with_peer_table first"
46                )
47            }
48            ElemError::UnsupportedRibGeneric => {
49                write!(f, "RibGenericEntries not yet supported")
50            }
51        }
52    }
53}
54
55impl std::error::Error for ElemError {}
56
57// use macro_rules! <name of macro>{<Body>}
58macro_rules! get_attr_value {
59    ($a:tt, $b:expr) => {
60        if let Attribute::$a(x) = $b {
61            Some(x)
62        } else {
63            None
64        }
65    };
66}
67
68#[allow(clippy::type_complexity)]
69fn get_relevant_attributes(
70    attributes: Attributes,
71) -> (
72    Option<AsPath>,
73    Option<AsPath>,
74    Option<Origin>,
75    Option<IpAddr>,
76    Option<u32>,
77    Option<u32>,
78    Option<Vec<MetaCommunity>>,
79    bool,
80    Option<(Asn, BgpIdentifier)>,
81    Option<Nlri>,
82    Option<Nlri>,
83    Option<Asn>,
84    Option<Vec<AttrRaw>>,
85    Option<Vec<AttrRaw>>,
86) {
87    let mut as_path = None;
88    let mut as4_path = None;
89    let mut origin = None;
90    let mut next_hop = None;
91    let mut local_pref = Some(0);
92    let mut med = Some(0);
93    let mut atomic = false;
94    let mut aggregator = None;
95    let mut announced = None;
96    let mut withdrawn = None;
97    let mut otc = None;
98    let mut unknown = vec![];
99    let mut deprecated = vec![];
100
101    let mut communities_vec: Vec<MetaCommunity> = vec![];
102
103    for attr in attributes {
104        match attr {
105            AttributeValue::Origin(v) => origin = Some(v),
106            AttributeValue::AsPath {
107                path,
108                is_as4: false,
109            } => as_path = Some(path),
110            AttributeValue::AsPath { path, is_as4: true } => as4_path = Some(path),
111            AttributeValue::NextHop(v) => next_hop = Some(v),
112            AttributeValue::MultiExitDiscriminator(v) => med = Some(v),
113            AttributeValue::LocalPreference(v) => local_pref = Some(v),
114            AttributeValue::AtomicAggregate => atomic = true,
115            AttributeValue::Communities(v) => communities_vec.extend(
116                v.into_iter()
117                    .map(MetaCommunity::Plain)
118                    .collect::<Vec<MetaCommunity>>(),
119            ),
120            AttributeValue::ExtendedCommunities(v) => communities_vec.extend(
121                v.into_iter()
122                    .map(MetaCommunity::Extended)
123                    .collect::<Vec<MetaCommunity>>(),
124            ),
125            AttributeValue::Ipv6AddressSpecificExtendedCommunities(v) => communities_vec.extend(
126                v.into_iter()
127                    .map(MetaCommunity::Ipv6Extended)
128                    .collect::<Vec<MetaCommunity>>(),
129            ),
130            AttributeValue::LargeCommunities(v) => communities_vec.extend(
131                v.into_iter()
132                    .map(MetaCommunity::Large)
133                    .collect::<Vec<MetaCommunity>>(),
134            ),
135            AttributeValue::Aggregator { asn, id, .. } => aggregator = Some((asn, id)),
136            AttributeValue::MpReachNlri(nlri) => announced = Some(nlri),
137            AttributeValue::MpUnreachNlri(nlri) => withdrawn = Some(nlri),
138            AttributeValue::OnlyToCustomer(o) => otc = Some(o),
139
140            AttributeValue::Unknown(t) | AttributeValue::Raw(t) => {
141                unknown.push(t);
142            }
143            AttributeValue::Deprecated(t) => {
144                deprecated.push(t);
145            }
146
147            AttributeValue::OriginatorId(_)
148            | AttributeValue::Clusters(_)
149            | AttributeValue::Development(_)
150            | AttributeValue::LinkState(_)
151            | AttributeValue::TunnelEncapsulation(_)
152            | AttributeValue::TrafficEngineering(_)
153            | AttributeValue::Aigp(_)
154            | AttributeValue::BfdDiscriminator(_)
155            | AttributeValue::BgpPrefixSid(_)
156            | AttributeValue::Bier(_)
157            | AttributeValue::Sfp(_)
158            | AttributeValue::AttrSet(_) => {}
159        };
160    }
161
162    let communities = match !communities_vec.is_empty() {
163        true => Some(communities_vec),
164        false => None,
165    };
166
167    // If the next_hop is not set, we try to get it from the announced NLRI.
168    let next_hop = next_hop.or_else(|| {
169        announced.as_ref().and_then(|v| {
170            v.next_hop.as_ref().map(|h| match h {
171                NextHopAddress::Ipv4(v) => IpAddr::from(*v),
172                NextHopAddress::Ipv6(v) => IpAddr::from(*v),
173                NextHopAddress::Ipv6LinkLocal(v, _) => IpAddr::from(*v),
174                // RFC 8950: VPN next hops - return the IPv6 address part
175                NextHopAddress::VpnIpv6(_, v) => IpAddr::from(*v),
176                NextHopAddress::VpnIpv6LinkLocal(_, v, _, _) => IpAddr::from(*v),
177            })
178        })
179    });
180
181    (
182        as_path,
183        as4_path,
184        origin,
185        next_hop,
186        local_pref,
187        med,
188        communities,
189        atomic,
190        aggregator,
191        announced,
192        withdrawn,
193        otc,
194        if unknown.is_empty() {
195            None
196        } else {
197            Some(unknown)
198        },
199        if deprecated.is_empty() {
200            None
201        } else {
202            Some(deprecated)
203        },
204    )
205}
206
207fn rib_entry_to_elem(prefix: NetworkPrefix, peer: &Peer, entry: RibEntry) -> BgpElem {
208    let (
209        as_path,
210        as4_path,
211        origin,
212        next_hop,
213        local_pref,
214        med,
215        communities,
216        atomic,
217        aggregator,
218        announced,
219        _withdrawn,
220        only_to_customer,
221        unknown,
222        deprecated,
223    ) = get_relevant_attributes(entry.attributes);
224
225    let path = match (as_path, as4_path) {
226        (None, None) => None,
227        (Some(v), None) => Some(v),
228        (None, Some(v)) => Some(v),
229        (Some(v1), Some(v2)) => Some(AsPath::merge_aspath_as4path(&v1, &v2)),
230    };
231
232    let next_hop = match next_hop {
233        Some(v) => Some(v),
234        None => announced.and_then(|v| {
235            v.next_hop.map(|h| match h {
236                NextHopAddress::Ipv4(v) => IpAddr::from(v),
237                NextHopAddress::Ipv6(v) => IpAddr::from(v),
238                NextHopAddress::Ipv6LinkLocal(v, _) => IpAddr::from(v),
239                NextHopAddress::VpnIpv6(_, v) => IpAddr::from(v),
240                NextHopAddress::VpnIpv6LinkLocal(_, v, _, _) => IpAddr::from(v),
241            })
242        }),
243    };
244
245    let origin_asns = path
246        .as_ref()
247        .map(|as_path| as_path.iter_origins().collect());
248
249    BgpElem {
250        timestamp: entry.originated_time as f64,
251        elem_type: ElemType::ANNOUNCE,
252        peer_ip: peer.peer_ip,
253        peer_asn: peer.peer_asn,
254        peer_bgp_id: Some(peer.peer_bgp_id),
255        prefix,
256        next_hop,
257        as_path: path,
258        origin,
259        origin_asns,
260        local_pref,
261        med,
262        communities,
263        atomic,
264        aggr_asn: aggregator.map(|v| v.0),
265        aggr_ip: aggregator.map(|v| v.1),
266        only_to_customer,
267        unknown,
268        deprecated,
269    }
270}
271
272/// Iterator over [`BgpElem`]s produced from a single [`MrtRecord`],
273/// without requiring a mutable reference to the [`Elementor`].
274///
275/// This avoids allocating a `Vec` for the common RIB table dump case
276/// by lazily converting each [`RibEntry`] into a [`BgpElem`] on demand.
277pub enum RecordElemIter<'a> {
278    #[doc(hidden)]
279    Empty,
280    #[doc(hidden)]
281    TableDump(Option<BgpElem>),
282    #[doc(hidden)]
283    TableDumpBatch(std::vec::IntoIter<TableDumpMessage>),
284    #[doc(hidden)]
285    RibAfi {
286        peer_table: &'a PeerIndexTable,
287        prefix: NetworkPrefix,
288        entries: std::vec::IntoIter<RibEntry>,
289    },
290    #[doc(hidden)]
291    Bgp4Mp(BgpUpdateElemIter),
292}
293
294impl Iterator for RecordElemIter<'_> {
295    type Item = BgpElem;
296
297    fn next(&mut self) -> Option<BgpElem> {
298        match self {
299            RecordElemIter::Empty => None,
300            RecordElemIter::TableDump(elem) => elem.take(),
301            RecordElemIter::TableDumpBatch(entries) => entries.next().map(table_dump_to_elem),
302            RecordElemIter::Bgp4Mp(iter) => iter.next(),
303            RecordElemIter::RibAfi {
304                peer_table,
305                prefix,
306                entries,
307            } => {
308                let entry = entries.next()?;
309                let pid = entry.peer_index;
310                match peer_table.get_peer_by_id(&pid) {
311                    Some(peer) => Some(rib_entry_to_elem(*prefix, peer, entry)),
312                    None => {
313                        error!("peer ID {} not found in peer_index table", pid);
314                        *self = RecordElemIter::Empty;
315                        None
316                    }
317                }
318            }
319        }
320    }
321
322    fn size_hint(&self) -> (usize, Option<usize>) {
323        match self {
324            RecordElemIter::Empty => (0, Some(0)),
325            RecordElemIter::TableDump(elem) => {
326                let n = elem.is_some() as usize;
327                (n, Some(n))
328            }
329            RecordElemIter::TableDumpBatch(entries) => {
330                let len = entries.len();
331                (len, Some(len))
332            }
333            RecordElemIter::Bgp4Mp(iter) => iter.size_hint(),
334            RecordElemIter::RibAfi { entries, .. } => {
335                let len = entries.len();
336                (len, Some(len))
337            }
338        }
339    }
340}
341
342/// Iterator over [`BgpElem`]s produced from a [`BgpUpdateMessage`],
343/// avoiding allocation by lazily yielding elements from announced and
344/// withdrawn prefixes in two phases.
345pub struct BgpUpdateElemIter {
346    timestamp: f64,
347    peer_ip: IpAddr,
348    peer_asn: Asn,
349    peer_bgp_id: Option<BgpIdentifier>,
350    only_to_customer: Option<Asn>,
351    // Announce-specific shared attributes
352    path: Option<AsPath>,
353    origin_asns: Option<Vec<Asn>>,
354    origin: Option<Origin>,
355    next_hop: Option<IpAddr>,
356    local_pref: Option<u32>,
357    med: Option<u32>,
358    communities: Option<Vec<MetaCommunity>>,
359    atomic: bool,
360    aggr_asn: Option<Asn>,
361    aggr_ip: Option<BgpIdentifier>,
362    unknown: Option<Vec<AttrRaw>>,
363    deprecated: Option<Vec<AttrRaw>>,
364    // Prefix iterators (two chained sources each)
365    announced:
366        std::iter::Chain<std::vec::IntoIter<NetworkPrefix>, std::vec::IntoIter<NetworkPrefix>>,
367    withdrawn:
368        std::iter::Chain<std::vec::IntoIter<NetworkPrefix>, std::vec::IntoIter<NetworkPrefix>>,
369    in_withdrawn_phase: bool,
370}
371
372impl Iterator for BgpUpdateElemIter {
373    type Item = BgpElem;
374
375    fn next(&mut self) -> Option<BgpElem> {
376        if !self.in_withdrawn_phase {
377            if let Some(prefix) = self.announced.next() {
378                return Some(BgpElem {
379                    timestamp: self.timestamp,
380                    elem_type: ElemType::ANNOUNCE,
381                    peer_ip: self.peer_ip,
382                    peer_asn: self.peer_asn,
383                    peer_bgp_id: self.peer_bgp_id,
384                    prefix,
385                    next_hop: self.next_hop,
386                    as_path: self.path.clone(),
387                    origin: self.origin,
388                    origin_asns: self.origin_asns.clone(),
389                    local_pref: self.local_pref,
390                    med: self.med,
391                    communities: self.communities.clone(),
392                    atomic: self.atomic,
393                    aggr_asn: self.aggr_asn,
394                    aggr_ip: self.aggr_ip,
395                    only_to_customer: self.only_to_customer,
396                    unknown: self.unknown.clone(),
397                    deprecated: self.deprecated.clone(),
398                });
399            }
400            self.in_withdrawn_phase = true;
401        }
402
403        self.withdrawn.next().map(|prefix| BgpElem {
404            timestamp: self.timestamp,
405            elem_type: ElemType::WITHDRAW,
406            peer_ip: self.peer_ip,
407            peer_asn: self.peer_asn,
408            peer_bgp_id: self.peer_bgp_id,
409            prefix,
410            next_hop: None,
411            as_path: None,
412            origin: None,
413            origin_asns: None,
414            local_pref: None,
415            med: None,
416            communities: None,
417            atomic: false,
418            aggr_asn: None,
419            aggr_ip: None,
420            only_to_customer: None,
421            unknown: None,
422            deprecated: None,
423        })
424    }
425
426    fn size_hint(&self) -> (usize, Option<usize>) {
427        let (ann_lo, ann_hi) = if self.in_withdrawn_phase {
428            (0, Some(0))
429        } else {
430            self.announced.size_hint()
431        };
432        let (wd_lo, wd_hi) = self.withdrawn.size_hint();
433        (ann_lo + wd_lo, ann_hi.and_then(|a| wd_hi.map(|w| a + w)))
434    }
435}
436
437impl Elementor {
438    pub fn new() -> Elementor {
439        Self::default()
440    }
441
442    /// Sets the peer index table for the elementor.
443    ///
444    /// This method takes an MRT record and extracts the peer index table from it if the record contains one.
445    /// The peer index table is required for processing TableDumpV2 records, as it contains the mapping between
446    /// peer indices and their corresponding IP addresses and ASNs.
447    ///
448    /// # Arguments
449    ///
450    /// * `record` - An MRT record that should contain a peer index table
451    ///
452    /// # Returns
453    ///
454    /// * `Ok(())` - If the peer table was successfully extracted and set
455    /// * `Err(ParserError)` - If the record does not contain a peer index table
456    ///
457    /// # Example
458    ///
459    /// ```no_run
460    /// use bgpkit_parser::{BgpkitParser, Elementor};
461    ///
462    /// let mut parser = BgpkitParser::new("rib.dump.bz2").unwrap();
463    /// let mut elementor = Elementor::new();
464    ///
465    /// // Get the first record which should be the peer index table
466    /// if let Ok(record) = parser.next_record() {
467    ///     elementor.set_peer_table(record).unwrap();
468    /// }
469    /// ```
470    pub fn set_peer_table(&mut self, record: MrtRecord) -> Result<(), ParserError> {
471        if let MrtMessage::TableDumpV2Message(TableDumpV2Message::PeerIndexTable(p)) =
472            record.message
473        {
474            self.peer_table = Some(p);
475            Ok(())
476        } else {
477            Err(ParseError("peer_table is not a PeerIndexTable".to_string()))
478        }
479    }
480
481    /// Creates an [`Elementor`] with the given [`PeerIndexTable`] already set.
482    pub fn with_peer_table(peer_table: PeerIndexTable) -> Elementor {
483        Elementor {
484            peer_table: Some(peer_table),
485        }
486    }
487
488    /// Convert a [`MrtRecord`] into an iterator of [`BgpElem`]s without
489    /// requiring `&mut self`.
490    ///
491    /// Unlike [`record_to_elems`](Elementor::record_to_elems), this method:
492    /// - Takes `&self` instead of `&mut self`, since the peer table must
493    ///   already be set via [`set_peer_table`](Elementor::set_peer_table) or
494    ///   [`with_peer_table`](Elementor::with_peer_table).
495    /// - Returns an error if the record contains a [`PeerIndexTable`] (which
496    ///   would require mutation).
497    /// - Returns a lazy [`RecordElemIter`] instead of collecting into a `Vec`,
498    ///   avoiding allocation for the common RIB table dump case.
499    ///
500    /// # Errors
501    ///
502    /// - [`ElemError::UnexpectedPeerIndexTable`] if the record is a PeerIndexTable message.
503    /// - [`ElemError::MissingPeerTable`] if the record requires a peer table but none is set.
504    pub fn record_to_elems_iter(&self, record: MrtRecord) -> Result<RecordElemIter<'_>, ElemError> {
505        let timestamp = {
506            let t = record.common_header.timestamp;
507            if let Some(micro) = &record.common_header.microsecond_timestamp {
508                let m = (*micro as f64) / 1000000.0;
509                t as f64 + m
510            } else {
511                f64::from(t)
512            }
513        };
514
515        match record.message {
516            MrtMessage::TableDumpMessage(msg) => {
517                Ok(RecordElemIter::TableDump(Some(table_dump_to_elem(msg))))
518            }
519            MrtMessage::TableDumpMessageBatch(messages) => {
520                Ok(RecordElemIter::TableDumpBatch(messages.into_iter()))
521            }
522
523            MrtMessage::TableDumpV2Message(msg) => match msg {
524                TableDumpV2Message::PeerIndexTable(p) => {
525                    Err(ElemError::UnexpectedPeerIndexTable(Box::new(p)))
526                }
527                TableDumpV2Message::RibAfi(t) => {
528                    let peer_table = self
529                        .peer_table
530                        .as_ref()
531                        .ok_or(ElemError::MissingPeerTable)?;
532                    Ok(RecordElemIter::RibAfi {
533                        peer_table,
534                        prefix: t.prefix,
535                        entries: t.rib_entries.into_iter(),
536                    })
537                }
538                TableDumpV2Message::RibGeneric(_) => Err(ElemError::UnsupportedRibGeneric),
539                TableDumpV2Message::GeoPeerTable(_) => Ok(RecordElemIter::Empty),
540            },
541
542            MrtMessage::Bgp4Mp(msg) => match msg {
543                Bgp4MpEnum::StateChange(_) => Ok(RecordElemIter::Empty),
544                Bgp4MpEnum::Message(v) => {
545                    match Elementor::bgp_to_elems_iter(
546                        v.bgp_message,
547                        timestamp,
548                        &v.peer_ip,
549                        &v.peer_asn,
550                    ) {
551                        Some(iter) => Ok(RecordElemIter::Bgp4Mp(iter)),
552                        None => Ok(RecordElemIter::Empty),
553                    }
554                }
555            },
556            MrtMessage::LegacyBgp(msg) => match msg {
557                LegacyBgp::StateChange(_) => Ok(RecordElemIter::Empty),
558                LegacyBgp::Message(message) => match Elementor::bgp_to_elems_iter(
559                    message.bgp_message,
560                    timestamp,
561                    &message.peer_ip,
562                    &message.peer_asn,
563                ) {
564                    Some(iter) => Ok(RecordElemIter::Bgp4Mp(iter)),
565                    None => Ok(RecordElemIter::Empty),
566                },
567            },
568        }
569    }
570
571    /// Convert a [BgpMessage] to a vector of [BgpElem]s.
572    ///
573    /// A [BgpMessage] may include `Update`, `Open`, `Notification` or `KeepAlive` messages,
574    /// and only `Update` message contains [BgpElem]s.
575    pub fn bgp_to_elems(
576        msg: BgpMessage,
577        timestamp: f64,
578        peer_ip: &IpAddr,
579        peer_asn: &Asn,
580    ) -> Vec<BgpElem> {
581        Elementor::bgp_to_elems_iter(msg, timestamp, peer_ip, peer_asn)
582            .map(|iter| iter.collect())
583            .unwrap_or_default()
584    }
585
586    /// Convert a [BgpMessage] into an iterator of [BgpElem]s.
587    ///
588    /// Returns `None` for non-Update messages (Open, Notification, KeepAlive, RouteRefresh).
589    pub fn bgp_to_elems_iter(
590        msg: BgpMessage,
591        timestamp: f64,
592        peer_ip: &IpAddr,
593        peer_asn: &Asn,
594    ) -> Option<BgpUpdateElemIter> {
595        match msg {
596            BgpMessage::Update(msg) => Some(Elementor::bgp_update_to_elems_iter(
597                msg, timestamp, peer_ip, peer_asn,
598            )),
599            BgpMessage::Open(_)
600            | BgpMessage::Notification(_)
601            | BgpMessage::KeepAlive
602            | BgpMessage::RouteRefresh(_) => None,
603        }
604    }
605
606    /// Convert a [BgpUpdateMessage] to a vector of [BgpElem]s.
607    pub fn bgp_update_to_elems(
608        msg: BgpUpdateMessage,
609        timestamp: f64,
610        peer_ip: &IpAddr,
611        peer_asn: &Asn,
612    ) -> Vec<BgpElem> {
613        Elementor::bgp_update_to_elems_iter(msg, timestamp, peer_ip, peer_asn).collect()
614    }
615
616    /// Convert a [BgpUpdateMessage] into a [`BgpUpdateElemIter`] that lazily
617    /// yields [BgpElem]s without allocating a `Vec`.
618    pub fn bgp_update_to_elems_iter(
619        msg: BgpUpdateMessage,
620        timestamp: f64,
621        peer_ip: &IpAddr,
622        peer_asn: &Asn,
623    ) -> BgpUpdateElemIter {
624        let (
625            as_path,
626            as4_path,
627            origin,
628            next_hop,
629            local_pref,
630            med,
631            communities,
632            atomic,
633            aggregator,
634            announced,
635            withdrawn,
636            only_to_customer,
637            unknown,
638            deprecated,
639        ) = get_relevant_attributes(msg.attributes);
640
641        let path = match (as_path, as4_path) {
642            (None, None) => None,
643            (Some(v), None) => Some(v),
644            (None, Some(v)) => Some(v),
645            (Some(v1), Some(v2)) => Some(AsPath::merge_aspath_as4path(&v1, &v2)),
646        };
647
648        let origin_asns = path
649            .as_ref()
650            .map(|as_path| as_path.iter_origins().collect());
651
652        let nlri_announced = announced.map(|n| n.prefixes).unwrap_or_default();
653        let nlri_withdrawn = withdrawn.map(|n| n.prefixes).unwrap_or_default();
654
655        BgpUpdateElemIter {
656            timestamp,
657            peer_ip: *peer_ip,
658            peer_asn: *peer_asn,
659            peer_bgp_id: None,
660            only_to_customer,
661            path,
662            origin_asns,
663            origin,
664            next_hop,
665            local_pref,
666            med,
667            communities,
668            atomic,
669            aggr_asn: aggregator.as_ref().map(|v| v.0),
670            aggr_ip: aggregator.as_ref().map(|v| v.1),
671            unknown,
672            deprecated,
673            announced: msg.announced_prefixes.into_iter().chain(nlri_announced),
674            withdrawn: msg.withdrawn_prefixes.into_iter().chain(nlri_withdrawn),
675            in_withdrawn_phase: false,
676        }
677    }
678
679    /// Convert a [MrtRecord] to a vector of [BgpElem]s.
680    ///
681    /// If the record is a [`PeerIndexTable`], it is consumed to set the internal
682    /// peer table. Errors are logged.
683    ///
684    /// For a non-mutating, lazy alternative, see
685    /// [`record_to_elems_iter`](Elementor::record_to_elems_iter).
686    pub fn record_to_elems(&mut self, record: MrtRecord) -> Vec<BgpElem> {
687        match record.message {
688            MrtMessage::TableDumpV2Message(TableDumpV2Message::PeerIndexTable(_)) => {
689                self.set_peer_table(record);
690                vec![]
691            }
692            _ => match self.record_to_elems_iter(record) {
693                Ok(iter) => iter.collect(),
694                Err(e) => {
695                    error!("{}", e);
696                    vec![]
697                }
698            },
699        }
700    }
701}
702
703fn table_dump_to_elem(msg: TableDumpMessage) -> BgpElem {
704    let (
705        as_path,
706        _as4_path,
707        origin,
708        next_hop,
709        local_pref,
710        med,
711        communities,
712        atomic,
713        aggregator,
714        _announced,
715        _withdrawn,
716        only_to_customer,
717        unknown,
718        deprecated,
719    ) = get_relevant_attributes(msg.attributes);
720
721    let origin_asns = as_path
722        .as_ref()
723        .map(|as_path| as_path.iter_origins().collect());
724
725    BgpElem {
726        timestamp: msg.originated_time as f64,
727        elem_type: ElemType::ANNOUNCE,
728        peer_ip: msg.peer_ip,
729        peer_asn: msg.peer_asn,
730        peer_bgp_id: None,
731        prefix: msg.prefix,
732        next_hop,
733        as_path,
734        origin,
735        origin_asns,
736        local_pref,
737        med,
738        communities,
739        atomic,
740        aggr_asn: aggregator.map(|v| v.0),
741        aggr_ip: aggregator.map(|v| v.1),
742        only_to_customer,
743        unknown,
744        deprecated,
745    }
746}
747
748#[inline(always)]
749pub fn option_to_string<T>(o: &Option<T>) -> String
750where
751    T: Display,
752{
753    if let Some(v) = o {
754        v.to_string()
755    } else {
756        String::new()
757    }
758}
759
760impl From<&BgpElem> for Attributes {
761    fn from(value: &BgpElem) -> Self {
762        let mut values = Vec::<AttributeValue>::new();
763        let mut attributes = Attributes::default();
764        let prefix = value.prefix;
765
766        if value.elem_type == ElemType::WITHDRAW {
767            values.push(AttributeValue::MpUnreachNlri(Nlri::new_unreachable(prefix)));
768            attributes.extend(values);
769            return attributes;
770        }
771
772        values.push(AttributeValue::MpReachNlri(Nlri::new_reachable(
773            prefix,
774            value.next_hop,
775        )));
776
777        if let Some(v) = value.next_hop {
778            values.push(AttributeValue::NextHop(v));
779        }
780
781        if let Some(v) = value.as_path.as_ref() {
782            let is_as4 = match v.get_origin_opt() {
783                None => true,
784                Some(asn) => asn.is_four_byte(),
785            };
786            values.push(AttributeValue::AsPath {
787                path: v.clone(),
788                is_as4,
789            });
790        }
791
792        if let Some(v) = value.origin {
793            values.push(AttributeValue::Origin(v));
794        }
795
796        if let Some(v) = value.local_pref {
797            values.push(AttributeValue::LocalPreference(v));
798        }
799
800        if let Some(v) = value.med {
801            values.push(AttributeValue::MultiExitDiscriminator(v));
802        }
803
804        if let Some(v) = value.communities.as_ref() {
805            let mut communites = vec![];
806            let mut extended_communities = vec![];
807            let mut ipv6_extended_communities = vec![];
808            let mut large_communities = vec![];
809            for c in v {
810                match c {
811                    MetaCommunity::Plain(v) => communites.push(*v),
812                    MetaCommunity::Extended(v) => extended_communities.push(*v),
813                    MetaCommunity::Large(v) => large_communities.push(*v),
814                    MetaCommunity::Ipv6Extended(v) => ipv6_extended_communities.push(*v),
815                }
816            }
817            if !communites.is_empty() {
818                values.push(AttributeValue::Communities(communites));
819            }
820            if !extended_communities.is_empty() {
821                values.push(AttributeValue::ExtendedCommunities(extended_communities));
822            }
823            if !large_communities.is_empty() {
824                values.push(AttributeValue::LargeCommunities(large_communities));
825            }
826            if !ipv6_extended_communities.is_empty() {
827                values.push(AttributeValue::Ipv6AddressSpecificExtendedCommunities(
828                    ipv6_extended_communities,
829                ));
830            }
831        }
832
833        if let Some(v) = value.aggr_asn {
834            let aggregator_id = match value.aggr_ip {
835                Some(v) => v,
836                None => Ipv4Addr::UNSPECIFIED,
837            };
838            values.push(AttributeValue::Aggregator {
839                asn: v,
840                id: aggregator_id,
841                is_as4: v.is_four_byte(),
842            });
843        }
844
845        if let Some(v) = value.only_to_customer {
846            values.push(AttributeValue::OnlyToCustomer(v));
847        }
848
849        if let Some(v) = value.unknown.as_ref() {
850            for t in v {
851                values.push(AttributeValue::Unknown(t.clone()));
852            }
853        }
854
855        if let Some(v) = value.deprecated.as_ref() {
856            for t in v {
857                values.push(AttributeValue::Deprecated(t.clone()));
858            }
859        }
860
861        attributes.extend(values);
862        attributes
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869    use crate::BgpkitParser;
870    use std::net::{Ipv4Addr, Ipv6Addr};
871    use std::str::FromStr;
872
873    #[test]
874    fn test_option_to_string() {
875        let o1 = Some(1);
876        let o2: Option<u32> = None;
877        assert_eq!(option_to_string(&o1), "1");
878        assert_eq!(option_to_string(&o2), "");
879    }
880
881    #[test]
882    fn test_record_to_elems() {
883        let url_table_dump_v1 = "https://data.ris.ripe.net/rrc00/2003.01/bview.20030101.0000.gz";
884        let url_table_dump_v2 = "https://data.ris.ripe.net/rrc00/2023.01/bview.20230101.0000.gz";
885        let url_bgp4mp = "https://data.ris.ripe.net/rrc00/2021.10/updates.20211001.0000.gz";
886
887        let mut elementor = Elementor::new();
888        let parser = BgpkitParser::new(url_table_dump_v1).unwrap();
889        let mut record_iter = parser.into_record_iter();
890        let record = record_iter.next().unwrap();
891        let elems = elementor.record_to_elems(record);
892        assert_eq!(elems.len(), 1);
893
894        let parser = BgpkitParser::new(url_table_dump_v2).unwrap();
895        let mut record_iter = parser.into_record_iter();
896        let peer_index_table = record_iter.next().unwrap();
897        let _elems = elementor.record_to_elems(peer_index_table);
898        let record = record_iter.next().unwrap();
899        let elems = elementor.record_to_elems(record);
900        assert!(!elems.is_empty());
901
902        let parser = BgpkitParser::new(url_bgp4mp).unwrap();
903        let mut record_iter = parser.into_record_iter();
904        let record = record_iter.next().unwrap();
905        let elems = elementor.record_to_elems(record);
906        assert!(!elems.is_empty());
907    }
908
909    #[test]
910    fn test_attributes_from_bgp_elem() {
911        let mut elem = BgpElem {
912            timestamp: 0.0,
913            elem_type: ElemType::ANNOUNCE,
914            peer_ip: IpAddr::from_str("10.0.0.1").unwrap(),
915            peer_asn: Asn::new_32bit(65000),
916            peer_bgp_id: None,
917            prefix: NetworkPrefix::from_str("10.0.1.0/24").unwrap(),
918            next_hop: Some(IpAddr::from_str("10.0.0.2").unwrap()),
919            as_path: Some(AsPath::from_sequence([65000, 65001, 65002])),
920            origin: Some(Origin::EGP),
921            origin_asns: Some(vec![Asn::new_32bit(65000)]),
922            local_pref: Some(100),
923            med: Some(200),
924            communities: Some(vec![
925                MetaCommunity::Plain(Community::NoAdvertise),
926                MetaCommunity::Extended(ExtendedCommunity::Raw([0, 0, 0, 0, 0, 0, 0, 0])),
927                MetaCommunity::Large(LargeCommunity {
928                    global_admin: 0,
929                    local_data: [0, 0],
930                }),
931                MetaCommunity::Ipv6Extended(Ipv6AddrExtCommunity {
932                    community_type: ExtendedCommunityType::TransitiveTwoOctetAs,
933                    subtype: 0,
934                    global_admin: Ipv6Addr::from_str("2001:db8::").unwrap(),
935                    local_admin: [0, 0],
936                }),
937            ]),
938            atomic: false,
939            aggr_asn: Some(Asn::new_32bit(65000)),
940            aggr_ip: Some(Ipv4Addr::from_str("10.2.0.0").unwrap()),
941            only_to_customer: Some(Asn::new_32bit(65000)),
942            unknown: Some(vec![AttrRaw {
943                code: AttrType::RESERVED.into(),
944                bytes: Bytes::new(),
945            }]),
946            deprecated: Some(vec![AttrRaw {
947                code: AttrType::RESERVED.into(),
948                bytes: Bytes::new(),
949            }]),
950        };
951
952        let _attributes = Attributes::from(&elem);
953        elem.elem_type = ElemType::WITHDRAW;
954        let _attributes = Attributes::from(&elem);
955    }
956
957    #[test]
958    fn test_get_relevant_attributes() {
959        let attributes = vec![
960            AttributeValue::Origin(Origin::IGP),
961            AttributeValue::AsPath {
962                path: AsPath::from_sequence([65000, 65001, 65002]),
963                is_as4: true,
964            },
965            AttributeValue::NextHop(IpAddr::from_str("10.0.0.1").unwrap()),
966            AttributeValue::MultiExitDiscriminator(100),
967            AttributeValue::LocalPreference(200),
968            AttributeValue::AtomicAggregate,
969            AttributeValue::Aggregator {
970                asn: Asn::new_32bit(65000),
971                id: Ipv4Addr::from_str("10.0.0.1").unwrap(),
972                is_as4: false,
973            },
974            AttributeValue::Communities(vec![Community::NoExport]),
975            AttributeValue::ExtendedCommunities(vec![ExtendedCommunity::Raw([
976                0, 0, 0, 0, 0, 0, 0, 0,
977            ])]),
978            AttributeValue::LargeCommunities(vec![LargeCommunity {
979                global_admin: 0,
980                local_data: [0, 0],
981            }]),
982            AttributeValue::Ipv6AddressSpecificExtendedCommunities(vec![Ipv6AddrExtCommunity {
983                community_type: ExtendedCommunityType::TransitiveTwoOctetAs,
984                subtype: 0,
985                global_admin: Ipv6Addr::from_str("2001:db8::").unwrap(),
986                local_admin: [0, 0],
987            }]),
988            AttributeValue::MpReachNlri(Nlri::new_reachable(
989                NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
990                Some(IpAddr::from_str("10.0.0.1").unwrap()),
991            )),
992            AttributeValue::MpUnreachNlri(Nlri::new_unreachable(
993                NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
994            )),
995            AttributeValue::OnlyToCustomer(Asn::new_32bit(65000)),
996            AttributeValue::Unknown(AttrRaw {
997                code: AttrType::RESERVED.into(),
998                bytes: Bytes::new(),
999            }),
1000            AttributeValue::Deprecated(AttrRaw {
1001                code: AttrType::RESERVED.into(),
1002                bytes: Bytes::new(),
1003            }),
1004        ]
1005        .into_iter()
1006        .map(Attribute::from)
1007        .collect::<Vec<Attribute>>();
1008
1009        let attributes = Attributes::from(attributes);
1010
1011        let (
1012            _as_path,
1013            _as4_path, // Table dump v1 does not have 4-byte AS number
1014            _origin,
1015            _next_hop,
1016            _local_pref,
1017            _med,
1018            _communities,
1019            _atomic,
1020            _aggregator,
1021            _announced,
1022            _withdrawn,
1023            _only_to_customer,
1024            _unknown,
1025            _deprecated,
1026        ) = get_relevant_attributes(attributes);
1027    }
1028
1029    #[test]
1030    fn test_next_hop_from_nlri() {
1031        let attributes = vec![AttributeValue::NextHop(
1032            IpAddr::from_str("10.0.0.1").unwrap(),
1033        )]
1034        .into_iter()
1035        .map(Attribute::from)
1036        .collect::<Vec<Attribute>>();
1037
1038        let attributes = Attributes::from(attributes);
1039
1040        let (
1041            _as_path,
1042            _as4_path, // Table dump v1 does not have 4-byte AS number
1043            _origin,
1044            next_hop,
1045            _local_pref,
1046            _med,
1047            _communities,
1048            _atomic,
1049            _aggregator,
1050            _announced,
1051            _withdrawn,
1052            _only_to_customer,
1053            _unknown,
1054            _deprecated,
1055        ) = get_relevant_attributes(attributes);
1056
1057        assert_eq!(next_hop, Some(IpAddr::from_str("10.0.0.1").unwrap()));
1058
1059        let attributes = vec![AttributeValue::MpReachNlri(Nlri::new_reachable(
1060            NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
1061            Some(IpAddr::from_str("10.0.0.2").unwrap()),
1062        ))]
1063        .into_iter()
1064        .map(Attribute::from)
1065        .collect::<Vec<Attribute>>();
1066
1067        let attributes = Attributes::from(attributes);
1068
1069        let (
1070            _as_path,
1071            _as4_path, // Table dump v1 does not have 4-byte AS number
1072            _origin,
1073            next_hop,
1074            _local_pref,
1075            _med,
1076            _communities,
1077            _atomic,
1078            _aggregator,
1079            _announced,
1080            _withdrawn,
1081            _only_to_customer,
1082            _unknown,
1083            _deprecated,
1084        ) = get_relevant_attributes(attributes);
1085
1086        assert_eq!(next_hop, Some(IpAddr::from_str("10.0.0.2").unwrap()));
1087    }
1088
1089    #[test]
1090    fn test_record_to_elems_iter_equivalence_tabledumpv2_small() {
1091        // rib-example-small.bz2 is a TableDumpV2 file (starts with PeerIndexTable)
1092        let url = "https://spaces.bgpkit.org/parser/rib-example-small.bz2";
1093
1094        let mut elementor = Elementor::new();
1095        let parser = BgpkitParser::new(url).unwrap();
1096        let mut record_iter = parser.into_record_iter();
1097
1098        // Skip the PeerIndexTable
1099        let peer_index_table = record_iter.next().unwrap();
1100        let _ = elementor.record_to_elems(peer_index_table);
1101
1102        // Process the first RIB entry
1103        let record = record_iter.next().unwrap();
1104        let elems_vec = elementor.record_to_elems(record.clone());
1105        let elems_iter: Vec<BgpElem> = elementor.record_to_elems_iter(record).unwrap().collect();
1106        assert_eq!(elems_vec, elems_iter);
1107        assert!(!elems_vec.is_empty());
1108    }
1109
1110    #[test]
1111    fn test_record_to_elems_iter_equivalence_bgp4mp() {
1112        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
1113
1114        let mut elementor = Elementor::new();
1115        let parser = BgpkitParser::new(url).unwrap();
1116        let mut record_iter = parser.into_record_iter();
1117        let record = record_iter.next().unwrap();
1118
1119        let elems_vec = elementor.record_to_elems(record.clone());
1120        let elems_iter: Vec<BgpElem> = elementor.record_to_elems_iter(record).unwrap().collect();
1121        assert_eq!(elems_vec, elems_iter);
1122        assert!(!elems_vec.is_empty());
1123    }
1124
1125    #[test]
1126    #[ignore = "requires large RIB file download"]
1127    fn test_record_to_elems_iter_equivalence_tabledumpv2() {
1128        let url = "https://data.ris.ripe.net/rrc00/2023.01/bview.20230101.0000.gz";
1129
1130        let mut elementor = Elementor::new();
1131        let parser = BgpkitParser::new(url).unwrap();
1132        let mut record_iter = parser.into_record_iter();
1133
1134        let peer_index_table = record_iter.next().unwrap();
1135        let _ = elementor.record_to_elems(peer_index_table);
1136
1137        let record = record_iter.next().unwrap();
1138        let elems_vec = elementor.record_to_elems(record.clone());
1139        let elems_iter: Vec<BgpElem> = elementor.record_to_elems_iter(record).unwrap().collect();
1140        assert_eq!(elems_vec, elems_iter);
1141        assert!(!elems_vec.is_empty());
1142    }
1143
1144    #[test]
1145    fn test_record_to_elems_iter_tabledumpv2_with_peer_table() {
1146        let url = "https://spaces.bgpkit.org/parser/rib-example-small.bz2";
1147
1148        let parser = BgpkitParser::new(url).unwrap();
1149        let mut record_iter = parser.into_record_iter();
1150
1151        let peer_index_table = record_iter.next().unwrap();
1152        let mut elementor = Elementor::with_peer_table(
1153            if let MrtMessage::TableDumpV2Message(TableDumpV2Message::PeerIndexTable(pit)) =
1154                peer_index_table.message
1155            {
1156                pit
1157            } else {
1158                panic!("Expected PeerIndexTable");
1159            },
1160        );
1161
1162        let record = record_iter.next().unwrap();
1163        let elems_vec = elementor.record_to_elems(record.clone());
1164        let elems_iter: Vec<BgpElem> = elementor.record_to_elems_iter(record).unwrap().collect();
1165        assert_eq!(elems_vec, elems_iter);
1166        assert!(!elems_vec.is_empty());
1167    }
1168
1169    #[test]
1170    fn test_record_to_elems_iter_error_unexpected_peer_index_table() {
1171        let url = "https://spaces.bgpkit.org/parser/rib-example-small.bz2";
1172
1173        let elementor = Elementor::new();
1174        let parser = BgpkitParser::new(url).unwrap();
1175        let mut record_iter = parser.into_record_iter();
1176        let record = record_iter.next().unwrap();
1177
1178        let result = elementor.record_to_elems_iter(record);
1179        assert!(matches!(
1180            result,
1181            Err(ElemError::UnexpectedPeerIndexTable(_))
1182        ));
1183    }
1184
1185    #[test]
1186    fn test_record_to_elems_iter_error_missing_peer_table() {
1187        // rib-example-small.bz2 is a TableDumpV2 file (starts with PeerIndexTable)
1188        let url = "https://spaces.bgpkit.org/parser/rib-example-small.bz2";
1189
1190        let elementor = Elementor::new();
1191        let parser = BgpkitParser::new(url).unwrap();
1192        let mut record_iter = parser.into_record_iter();
1193
1194        // Skip the PeerIndexTable without consuming it via record_to_elems
1195        // which would set the peer table in the elementor
1196        let _peer_index_table = record_iter.next().unwrap();
1197
1198        // Now try to process a RIB entry without having set the peer table
1199        let record = record_iter.next().unwrap();
1200        let result = elementor.record_to_elems_iter(record);
1201        assert!(matches!(result, Err(ElemError::MissingPeerTable)));
1202    }
1203
1204    #[test]
1205    fn test_bgp_to_elems_iter_equivalence() {
1206        let timestamp = 0.0;
1207        let peer_ip = IpAddr::from_str("10.0.0.1").unwrap();
1208        let peer_asn = Asn::new_32bit(65000);
1209
1210        let attributes = vec![
1211            AttributeValue::Origin(Origin::IGP),
1212            AttributeValue::AsPath {
1213                path: AsPath::from_sequence([65000, 65001, 65002]),
1214                is_as4: false,
1215            },
1216            AttributeValue::NextHop(peer_ip),
1217        ]
1218        .into_iter()
1219        .map(Attribute::from)
1220        .collect::<Vec<Attribute>>();
1221        let attributes = Attributes::from(attributes);
1222
1223        let announced_prefixes = vec![NetworkPrefix::from_str("10.0.0.0/24").unwrap()];
1224
1225        let bgp_message = BgpMessage::Update(BgpUpdateMessage {
1226            attributes,
1227            announced_prefixes,
1228            withdrawn_prefixes: vec![],
1229        });
1230
1231        let elems_vec =
1232            Elementor::bgp_to_elems(bgp_message.clone(), timestamp, &peer_ip, &peer_asn);
1233        let elems_iter: Vec<BgpElem> =
1234            Elementor::bgp_to_elems_iter(bgp_message, timestamp, &peer_ip, &peer_asn)
1235                .unwrap()
1236                .collect();
1237        assert_eq!(elems_vec, elems_iter);
1238        assert_eq!(elems_vec.len(), 1);
1239    }
1240
1241    #[test]
1242    fn test_bgp_to_elems_iter_non_update_messages() {
1243        use std::net::Ipv4Addr;
1244
1245        let timestamp = 0.0;
1246        let peer_ip = IpAddr::from_str("10.0.0.1").unwrap();
1247        let peer_asn = Asn::new_32bit(65000);
1248
1249        let open_msg = BgpOpenMessage {
1250            version: 4,
1251            asn: Asn::new_32bit(1),
1252            hold_time: 180,
1253            bgp_identifier: Ipv4Addr::new(192, 0, 2, 1),
1254            extended_length: false,
1255            opt_params: vec![],
1256        };
1257        assert!(Elementor::bgp_to_elems_iter(
1258            BgpMessage::Open(open_msg),
1259            timestamp,
1260            &peer_ip,
1261            &peer_asn
1262        )
1263        .is_none());
1264
1265        let notification_msg = BgpNotificationMessage {
1266            error: BgpError::Unknown(0, 0),
1267            data: vec![],
1268        };
1269        assert!(Elementor::bgp_to_elems_iter(
1270            BgpMessage::Notification(notification_msg),
1271            timestamp,
1272            &peer_ip,
1273            &peer_asn
1274        )
1275        .is_none());
1276
1277        assert!(Elementor::bgp_to_elems_iter(
1278            BgpMessage::KeepAlive,
1279            timestamp,
1280            &peer_ip,
1281            &peer_asn
1282        )
1283        .is_none());
1284    }
1285
1286    #[test]
1287    fn test_bgp_update_to_elems_iter_equivalence() {
1288        let timestamp = 0.0;
1289        let peer_ip = IpAddr::from_str("10.0.0.1").unwrap();
1290        let peer_asn = Asn::new_32bit(65000);
1291
1292        let attributes = vec![
1293            AttributeValue::Origin(Origin::IGP),
1294            AttributeValue::AsPath {
1295                path: AsPath::from_sequence([65000, 65001, 65002]),
1296                is_as4: false,
1297            },
1298            AttributeValue::NextHop(peer_ip),
1299        ]
1300        .into_iter()
1301        .map(Attribute::from)
1302        .collect::<Vec<Attribute>>();
1303        let attributes = Attributes::from(attributes);
1304
1305        let announced_prefixes = vec![NetworkPrefix::from_str("10.0.0.0/24").unwrap()];
1306        let withdrawn_prefixes = vec![NetworkPrefix::from_str("10.0.1.0/24").unwrap()];
1307
1308        let update = BgpUpdateMessage {
1309            attributes,
1310            announced_prefixes,
1311            withdrawn_prefixes,
1312        };
1313
1314        let elems_vec =
1315            Elementor::bgp_update_to_elems(update.clone(), timestamp, &peer_ip, &peer_asn);
1316        let elems_iter: Vec<BgpElem> =
1317            Elementor::bgp_update_to_elems_iter(update, timestamp, &peer_ip, &peer_asn).collect();
1318        assert_eq!(elems_vec, elems_iter);
1319        assert_eq!(elems_vec.len(), 2);
1320    }
1321
1322    #[test]
1323    fn test_record_elem_iter_size_hint() {
1324        use std::collections::HashMap;
1325
1326        let peer_table = PeerIndexTable {
1327            collector_bgp_id: BgpIdentifier::from_str("10.0.0.1").unwrap(),
1328            view_name: "".to_string(),
1329            id_peer_map: HashMap::new(),
1330            peer_ip_id_map: HashMap::new(),
1331        };
1332
1333        let entries: Vec<RibEntry> = vec![];
1334        let iter = RecordElemIter::RibAfi {
1335            peer_table: &peer_table,
1336            prefix: NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
1337            entries: entries.into_iter(),
1338        };
1339        assert_eq!(iter.size_hint(), (0, Some(0)));
1340
1341        let entries: Vec<RibEntry> = (0..5)
1342            .map(|i| RibEntry {
1343                peer_index: i as u16,
1344                originated_time: 0,
1345                path_id: None,
1346                attributes: Attributes::default(),
1347            })
1348            .collect();
1349        let iter = RecordElemIter::RibAfi {
1350            peer_table: &peer_table,
1351            prefix: NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
1352            entries: entries.into_iter(),
1353        };
1354        assert_eq!(iter.size_hint(), (5, Some(5)));
1355    }
1356}