Skip to main content

bgpkit_parser/parser/
mod.rs

1/*!
2parser module maintains the main logic for processing BGP and MRT messages.
3*/
4use crate::models::{BgpElem, MrtRecord};
5use log::warn;
6use std::io::{BufReader, Cursor, Read};
7pub use text_dump::{detect_text_dump, infer_timestamp_from_path, TextDumpElemIterator};
8
9#[macro_use]
10pub mod utils;
11pub mod bgp;
12pub mod bmp;
13pub mod filter;
14pub mod iters;
15pub mod mrt;
16pub mod rpki;
17pub mod text_dump;
18
19#[cfg(feature = "rislive")]
20pub mod rislive;
21
22pub(crate) use self::utils::*;
23
24pub use mrt::mrt_elem::{BgpUpdateElemIter, ElemError, Elementor, RecordElemIter};
25#[cfg(feature = "oneio")]
26use oneio::{get_cache_reader, get_reader, get_resumable_http_reader};
27
28pub use crate::error::{ParserError, ParserErrorWithBytes};
29pub use bmp::{parse_bmp_msg, parse_openbmp_header, parse_openbmp_msg};
30pub use filter::*;
31pub use iters::*;
32pub use mrt::*;
33
34#[cfg(feature = "rislive")]
35pub use rislive::messages::{
36    RisLiveClientMessage, RisLiveMeta, RisLiveRawFull, RisSubscribe, RisSubscribeSocketOptions,
37    RisSubscribeType,
38};
39#[cfg(feature = "rislive")]
40pub use rislive::{
41    parse_ris_live_message, parse_ris_live_message_json, parse_ris_live_message_raw,
42    parse_ris_live_message_raw_full,
43};
44
45pub struct BgpkitParser<R> {
46    reader: R,
47    core_dump: bool,
48    filters: Vec<Filter>,
49    options: ParserOptions,
50    /// Streaming element iterator for text dumps. `None` for MRT input,
51    /// which is parsed lazily through [`Self::next_record`].
52    text_dump_iter: Option<Box<dyn Iterator<Item = BgpElem> + Send>>,
53}
54
55pub(crate) struct ParserOptions {
56    show_warnings: bool,
57    warned_zebra_compat: bool,
58}
59impl Default for ParserOptions {
60    fn default() -> Self {
61        ParserOptions {
62            show_warnings: true,
63            warned_zebra_compat: false,
64        }
65    }
66}
67
68impl ParserOptions {
69    pub(crate) fn warn_zebra_compat_once(&mut self) {
70        if self.show_warnings && !self.warned_zebra_compat {
71            warn!(
72                "recovered shortened Zebra BGP4MP records with missing envelope fields; substituting IPv4 zero addresses and interface index 0 (further occurrences for this parser will not be logged)"
73            );
74            self.warned_zebra_compat = true;
75        }
76    }
77}
78
79#[cfg(feature = "oneio")]
80impl BgpkitParser<Box<dyn Read + Send>> {
81    /// Creating a new parser from a object that implements [Read] trait.
82    pub fn new(path: &str) -> Result<Self, ParserErrorWithBytes> {
83        let reader = get_reader(path)?;
84        Ok(BgpkitParser {
85            reader,
86            core_dump: false,
87            filters: vec![],
88            options: ParserOptions::default(),
89            text_dump_iter: None,
90        })
91    }
92
93    /// Creates a parser backed by an experimental resumable HTTP(S) reader.
94    ///
95    /// If a remote server drops a connection while the parser is reading, the
96    /// reader reconnects with an HTTP Range request and continues at the last
97    /// byte read. Resumed responses are validated by `oneio`; servers that do
98    /// not support Range requests, or resources that change while being read,
99    /// return an error instead of combining inconsistent bytes.
100    ///
101    /// This constructor is opt-in. [`BgpkitParser::new`] retains its existing
102    /// reader behavior. Use a fallible iterator when the caller needs to
103    /// handle an unrecoverable read failure explicitly.
104    ///
105    /// # Example
106    ///
107    /// ```no_run
108    /// use bgpkit_parser::BgpkitParser;
109    ///
110    /// let url = "https://data.ris.ripe.net/rrc00/latest-update.gz";
111    /// let parser = BgpkitParser::new_resumable_http(url)?;
112    /// # Ok::<(), bgpkit_parser::ParserErrorWithBytes>(())
113    /// ```
114    pub fn new_resumable_http(path: &str) -> Result<Self, ParserErrorWithBytes> {
115        let reader = get_resumable_http_reader(path)?;
116        Ok(BgpkitParser {
117            reader,
118            core_dump: false,
119            filters: vec![],
120            options: ParserOptions::default(),
121            text_dump_iter: None,
122        })
123    }
124
125    /// Creating a new parser that also caches the remote content to a local cache directory.
126    ///
127    /// The cache file name is generated by the following format: `cache-<crc32 of file name>-<file name>`.
128    /// For example, the remote file `http://archive.routeviews.org/route-views.chile/bgpdata/2023.03/RIBS/rib.20230326.0600.bz2`
129    /// will be cached as `cache-682cb1eb-rib.20230326.0600.bz2` in the cache directory.
130    pub fn new_cached(path: &str, cache_dir: &str) -> Result<Self, ParserErrorWithBytes> {
131        let file_name = path.rsplit('/').next().unwrap().to_string();
132        let new_file_name = format!(
133            "cache-{}",
134            add_suffix_to_filename(file_name.as_str(), crc32(path).as_str())
135        );
136        let reader = get_cache_reader(path, cache_dir, Some(new_file_name), false)?;
137        Ok(BgpkitParser {
138            reader,
139            core_dump: false,
140            filters: vec![],
141            options: ParserOptions::default(),
142            text_dump_iter: None,
143        })
144    }
145
146    /// Create a parser for a Cisco `sh ip bgp` text dump (PCH daily snapshots
147    /// or route-views `oix-full-snapshot-*` files).
148    ///
149    /// The file is auto-decompressed by oneio. The timestamp for all elements
150    /// is inferred from the file name when possible. To override the timestamp,
151    /// use [`from_text_reader_with_timestamp`](Self::from_text_reader_with_timestamp)
152    /// directly. The resulting parser streams [`BgpElem`]s lazily — one route
153    /// line at a time, constant memory. Calling [`into_record_iter`](Self::into_record_iter)
154    /// or [`next_record`](Self::next_record) on a text-dump parser returns no
155    /// records (text dumps have no MRT-record representation); use
156    /// [`into_elem_iter`](Self::into_elem_iter) or the `for elem in parser` loop instead.
157    ///
158    /// # Example
159    ///
160    /// ```no_run
161    /// use bgpkit_parser::BgpkitParser;
162    ///
163    /// let url = "https://downloads.pch.net/files/Routing_Data/IPv4_daily_snapshots/2026/07/route-collector.bom2.pch.net/route-collector.bom2.pch.net-ipv4_bgp_routes.2026.07.01.gz";
164    /// for elem in BgpkitParser::new_text(url).unwrap() {
165    ///     println!("{elem}");
166    /// }
167    /// ```
168    pub fn new_text(path: &str) -> Result<Self, ParserErrorWithBytes> {
169        let timestamp = infer_timestamp_from_path(path).unwrap_or(0.0);
170        let reader = get_reader(path)?;
171        Self::from_text_reader_with_timestamp(reader, timestamp)
172    }
173
174    /// Create a parser that auto-detects whether the input is an MRT file or a
175    /// Cisco `sh ip bgp` text dump, parsing accordingly.
176    ///
177    /// Peeks the first 256 bytes: if they look like a Cisco text dump the file
178    /// is parsed as one (timestamp inferred from the path); otherwise it is
179    /// treated as MRT and parsed lazily as usual. This is the most convenient
180    /// constructor when the input type is unknown.
181    ///
182    /// # Example
183    ///
184    /// ```no_run
185    /// use bgpkit_parser::BgpkitParser;
186    ///
187    /// // works for either MRT or text dumps
188    /// for elem in BgpkitParser::new_auto("https://downloads.pch.net/files/Routing_Data/IPv4_daily_snapshots/2026/07/route-collector.bom2.pch.net/route-collector.bom2.pch.net-ipv4_bgp_routes.2026.07.01.gz").unwrap() {
189    ///     println!("{elem}");
190    /// }
191    /// ```
192    pub fn new_auto(path: &str) -> Result<Self, ParserErrorWithBytes> {
193        let reader = get_reader(path)?;
194        Self::from_auto_reader_with_timestamp(reader, infer_timestamp_from_path(path))
195    }
196}
197
198#[cfg(feature = "oneio")]
199fn add_suffix_to_filename(filename: &str, suffix: &str) -> String {
200    let mut parts: Vec<&str> = filename.split('.').collect(); // Split filename by dots
201    if parts.len() > 1 {
202        let last_part = parts.pop().unwrap(); // Remove the last part (suffix) from the parts vector
203        let new_last_part = format!("{suffix}.{last_part}"); // Add the suffix to the last part
204        parts.push(&new_last_part); // Add the updated last part back to the parts vector
205        parts.join(".") // Join the parts back into a filename string with dots
206    } else {
207        // If the filename does not have any dots, simply append the suffix to the end
208        format!("{filename}.{suffix}")
209    }
210}
211
212impl<R: Read> BgpkitParser<R> {
213    /// Creating a new parser from an object that implements [Read] trait.
214    pub fn from_reader(reader: R) -> Self {
215        BgpkitParser {
216            reader,
217            core_dump: false,
218            filters: vec![],
219            options: ParserOptions::default(),
220            text_dump_iter: None,
221        }
222    }
223
224    /// This is used in for loop `for item in parser{}`
225    pub fn next_record(&mut self) -> Result<MrtRecord, ParserErrorWithBytes> {
226        if self.text_dump_iter.is_some() {
227            return Err(ParserError::Unsupported(
228                "text-dump parsers have no MRT record representation; iterate elements instead"
229                    .to_string(),
230            )
231            .into());
232        }
233        let (record, used_zebra_compat) =
234            mrt::mrt_record::parse_mrt_record_with_zebra_compat(&mut self.reader)?;
235        if used_zebra_compat {
236            self.warn_zebra_compat_once();
237        }
238        Ok(record)
239    }
240}
241
242impl BgpkitParser<Box<dyn Read + Send>> {
243    /// Create a text-dump parser from any reader, with timestamp `0.0`.
244    /// Prefer [`BgpkitParser::new_text`] when you have a file path or URL,
245    /// as it will infer the timestamp automatically.
246    pub fn from_text_reader(
247        reader: impl Read + Send + 'static,
248    ) -> Result<Self, ParserErrorWithBytes> {
249        Self::from_text_reader_with_timestamp(reader, 0.0)
250    }
251
252    /// Create a text-dump parser from a reader with an explicit element
253    /// timestamp. The parser streams elements lazily — one route line at a
254    /// time, constant memory. It has no MRT-record representation.
255    pub fn from_text_reader_with_timestamp(
256        reader: impl Read + Send + 'static,
257        timestamp: f64,
258    ) -> Result<Self, ParserErrorWithBytes> {
259        let buf_reader = BufReader::new(reader);
260        let iter = TextDumpElemIterator::new(buf_reader, timestamp).map_err(ParserError::from)?;
261        Ok(BgpkitParser {
262            reader: Box::new(std::io::empty()),
263            core_dump: false,
264            filters: vec![],
265            options: ParserOptions::default(),
266            text_dump_iter: Some(Box::new(iter)),
267        })
268    }
269
270    /// Create a parser from any reader, auto-detecting MRT vs text dump by
271    /// sniffing the first bytes. Timestamp defaults to `0.0` for text dumps.
272    pub fn from_auto_reader(
273        reader: impl Read + Send + 'static,
274    ) -> Result<Self, ParserErrorWithBytes> {
275        Self::from_auto_reader_with_timestamp(reader, None)
276    }
277
278    /// Create a parser from any reader, auto-detecting MRT vs text dump.
279    /// `timestamp` sets the element timestamp for text dumps (`None` → `0.0`);
280    /// for filename-based inference, use [`new_auto`](Self::new_auto) instead.
281    pub fn from_auto_reader_with_timestamp(
282        reader: impl Read + Send + 'static,
283        timestamp: Option<f64>,
284    ) -> Result<Self, ParserErrorWithBytes> {
285        let mut buf_reader = BufReader::new(reader);
286        let (is_text, head) = detect_text_dump(&mut buf_reader).map_err(ParserError::from)?;
287        if is_text {
288            let chained = BufReader::new(Cursor::new(head).chain(buf_reader));
289            let ts = timestamp.unwrap_or(0.0);
290            let iter = TextDumpElemIterator::new(chained, ts).map_err(ParserError::from)?;
291            Ok(BgpkitParser {
292                reader: Box::new(std::io::empty()),
293                core_dump: false,
294                filters: vec![],
295                options: ParserOptions::default(),
296                text_dump_iter: Some(Box::new(iter)),
297            })
298        } else {
299            Ok(BgpkitParser {
300                reader: Box::new(Cursor::new(head).chain(buf_reader)),
301                core_dump: false,
302                filters: vec![],
303                options: ParserOptions::default(),
304                text_dump_iter: None,
305            })
306        }
307    }
308}
309
310impl<R> BgpkitParser<R> {
311    pub(crate) fn warn_zebra_compat_once(&mut self) {
312        self.options.warn_zebra_compat_once();
313    }
314
315    pub fn enable_core_dump(self) -> Self {
316        BgpkitParser {
317            reader: self.reader,
318            core_dump: true,
319            filters: self.filters,
320            options: self.options,
321            text_dump_iter: self.text_dump_iter,
322        }
323    }
324
325    pub fn disable_warnings(self) -> Self {
326        let mut options = self.options;
327        options.show_warnings = false;
328        BgpkitParser {
329            reader: self.reader,
330            core_dump: self.core_dump,
331            filters: self.filters,
332            options,
333            text_dump_iter: self.text_dump_iter,
334        }
335    }
336
337    /// Add a filter to the parser by specifying filter type and value as strings.
338    ///
339    /// This method parses the filter type and value strings to create a [`Filter`] and adds it
340    /// to the parser's filter list. For the full list of available filter types and their
341    /// formats, see the [`Filter`] struct documentation.
342    ///
343    /// # Available Filter Types
344    ///
345    /// - `origin_asn` - Origin AS number (e.g., "12345")
346    /// - `origin_asns` - Multiple origin AS numbers, comma-separated (e.g., "12345,67890")
347    /// - `prefix` - Exact prefix match (e.g., "192.168.1.0/24")
348    /// - `prefix_super` - Match prefix and super-prefixes
349    /// - `prefix_sub` - Match prefix and sub-prefixes
350    /// - `prefix_super_sub` - Match prefix, super-prefixes, and sub-prefixes
351    /// - `prefixes` - Multiple prefixes (e.g., "1.1.1.0/24,8.8.8.0/24")
352    /// - `peer_ip` - Peer IP address (e.g., "192.168.1.1")
353    /// - `peer_ips` - Multiple peer IPs (e.g., "192.168.1.1,192.168.1.2")
354    /// - `peer_asn` - Peer AS number (e.g., "12345")
355    /// - `peer_asns` - Multiple peer AS numbers (e.g., "12345,67890")
356    /// - `type` - Message type: "a"/"announce" or "w"/"withdraw"
357    /// - `ts_start` - Start timestamp (unix timestamp or RFC3339)
358    /// - `ts_end` - End timestamp (unix timestamp or RFC3339)
359    /// - `as_path` - AS path regex pattern
360    /// - `community` - Community regex pattern
361    /// - `ip_version` - IP version: "4"/"ipv4" or "6"/"ipv6"
362    /// - `otc` - Only-to-customer ASN (RFC 9234); `*` for present, `!*` for absent
363    /// - `next_hop` - Next hop IP address; `*`/`!*` for presence
364    /// - `origin` - Origin attribute: "igp", "egp", or "incomplete"; `*`/`!*` for presence
365    /// - `local_pref` - Local preference value; `*`/`!*` for presence
366    /// - `med` - Multi-exit discriminator value; `*`/`!*` for presence
367    /// - `atomic` - Atomic aggregate flag: "true"/"false"
368    /// - `aggr_asn` - Aggregator ASN; `*`/`!*` for presence
369    /// - `aggr_ip` - Aggregator IP address; `*`/`!*` for presence
370    /// - `peer_bgp_id` - Peer BGP identifier (router ID); `*`/`!*` for presence
371    ///
372    /// # Negative Filters
373    ///
374    /// Most filters support negation by prefixing the value with `!`. For example:
375    /// - `origin_asn=!13335` matches elements where origin AS is NOT 13335
376    /// - `prefix=!10.0.0.0/8` matches elements where prefix is NOT 10.0.0.0/8
377    ///
378    /// # Presence Filters
379    ///
380    /// Optional fields (`Option<T>`) support `*` as a wildcard to check whether a field
381    /// is present or absent: `otc=*` matches elements with an OTC value, `otc=!*` matches
382    /// elements without one.
383    ///
384    /// # Example
385    ///
386    /// ```no_run
387    /// use bgpkit_parser::BgpkitParser;
388    ///
389    /// let parser = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
390    ///     .unwrap()
391    ///     .add_filter("peer_ip", "185.1.8.65")
392    ///     .unwrap()
393    ///     .add_filter("type", "w")
394    ///     .unwrap();
395    ///
396    /// for elem in parser {
397    ///     println!("{}", elem);
398    /// }
399    /// ```
400    pub fn add_filter(
401        self,
402        filter_type: &str,
403        filter_value: &str,
404    ) -> Result<Self, ParserErrorWithBytes> {
405        let mut filters = self.filters;
406        filters.push(Filter::new(filter_type, filter_value)?);
407        Ok(BgpkitParser {
408            reader: self.reader,
409            core_dump: self.core_dump,
410            filters,
411            options: self.options,
412            text_dump_iter: self.text_dump_iter,
413        })
414    }
415
416    /// Add multiple filters to the parser.
417    ///
418    /// This method extends the existing filters with the provided slice of filters.
419    ///
420    /// # Example
421    ///
422    /// ```no_run
423    /// use bgpkit_parser::BgpkitParser;
424    /// use bgpkit_parser::parser::Filter;
425    ///
426    /// let filters = vec![
427    ///     Filter::new("peer_ip", "185.1.8.65").unwrap(),
428    ///     Filter::new("type", "w").unwrap(),
429    /// ];
430    ///
431    /// let parser = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
432    ///     .unwrap()
433    ///     .add_filters(&filters);
434    /// ```
435    pub fn add_filters(mut self, filters: &[Filter]) -> Self {
436        self.filters.extend(filters.iter().cloned());
437        self
438    }
439
440    /// Set filters directly, replacing any existing filters.
441    ///
442    /// This method allows passing a pre-built `Vec<Filter>` directly to the parser,
443    /// bypassing the need to parse filter strings. This is useful when you want to
444    /// build filter specifications independently and reuse them across multiple parsers.
445    ///
446    /// # Example
447    ///
448    /// ```no_run
449    /// use bgpkit_parser::BgpkitParser;
450    /// use bgpkit_parser::parser::Filter;
451    ///
452    /// // Build filters independently
453    /// let filters = vec![
454    ///     Filter::new("peer_ip", "185.1.8.65").unwrap(),
455    ///     Filter::new("type", "w").unwrap(),
456    /// ];
457    ///
458    /// // Apply to multiple parsers (no manual clone needed)
459    /// let parser1 = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
460    ///     .unwrap()
461    ///     .with_filters(&filters);
462    ///
463    /// let parser2 = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
464    ///     .unwrap()
465    ///     .with_filters(&filters);
466    /// ```
467    pub fn with_filters(mut self, filters: &[Filter]) -> Self {
468        self.filters = filters.to_vec();
469        self
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::models::Asn;
477
478    #[test]
479    fn test_new_with_reader() {
480        // bzip2 reader for a compressed file
481        let reader = oneio::get_reader("http://archive.routeviews.org/route-views.ny/bgpdata/2023.02/UPDATES/updates.20230215.0630.bz2").unwrap();
482        assert_eq!(
483            12683,
484            BgpkitParser::from_reader(reader).into_elem_iter().count()
485        );
486
487        // remote reader for an uncompressed updates file
488        let reader = oneio::get_reader("https://spaces.bgpkit.org/parser/update-example").unwrap();
489        assert_eq!(
490            8160,
491            BgpkitParser::from_reader(reader).into_elem_iter().count()
492        );
493    }
494
495    #[test]
496    fn test_new_resumable_http() {
497        let parser =
498            BgpkitParser::new_resumable_http("https://spaces.bgpkit.org/parser/update-example.gz")
499                .unwrap();
500        assert_eq!(8160, parser.into_elem_iter().count());
501    }
502
503    #[test]
504    fn test_new_cached_with_reader() {
505        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
506        let parser = BgpkitParser::new_cached(url, "/tmp/bgpkit-parser-tests")
507            .unwrap()
508            .enable_core_dump()
509            .disable_warnings();
510        let count = parser.into_elem_iter().count();
511        assert_eq!(8160, count);
512        let parser = BgpkitParser::new_cached(url, "/tmp/bgpkit-parser-tests").unwrap();
513        let count = parser.into_elem_iter().count();
514        assert_eq!(8160, count);
515    }
516
517    #[test]
518    fn test_add_suffix_to_filename() {
519        // Test with a filename that has dots
520        let filename = "example.txt";
521        let suffix = "suffix";
522        let result = add_suffix_to_filename(filename, suffix);
523        assert_eq!(result, "example.suffix.txt");
524
525        // Test with a filename that has multiple dots
526        let filename = "example.tar.gz";
527        let suffix = "suffix";
528        let result = add_suffix_to_filename(filename, suffix);
529        assert_eq!(result, "example.tar.suffix.gz");
530
531        // Test with a filename that has no dots
532        let filename = "example";
533        let suffix = "suffix";
534        let result = add_suffix_to_filename(filename, suffix);
535        assert_eq!(result, "example.suffix");
536
537        // Test with an empty filename
538        let filename = "";
539        let suffix = "suffix";
540        let result = add_suffix_to_filename(filename, suffix);
541        assert_eq!(result, ".suffix");
542
543        // Test with an empty suffix
544        let filename = "example.txt";
545        let suffix = "";
546        let result = add_suffix_to_filename(filename, suffix);
547        assert_eq!(result, "example..txt");
548    }
549
550    #[test]
551    fn test_with_filters() {
552        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
553
554        // Build filters independently
555        let filters = vec![
556            Filter::new("peer_ip", "185.1.8.65").unwrap(),
557            Filter::new("type", "w").unwrap(),
558        ];
559
560        // Test with_filters - sets filters directly
561        let parser = BgpkitParser::new(url).unwrap().with_filters(&filters);
562        let count = parser.into_elem_iter().count();
563
564        // peer 185.1.8.65 has 3393 total, 132 withdrawals
565        assert_eq!(count, 132);
566
567        // Test that with_filters replaces existing filters
568        let filters1 = vec![Filter::new("peer_ip", "185.1.8.65").unwrap()];
569        let filters2 = vec![Filter::new("peer_ip", "185.1.8.50").unwrap()];
570
571        let parser = BgpkitParser::new(url)
572            .unwrap()
573            .with_filters(&filters1)
574            .with_filters(&filters2); // Should replace filters1
575        let count = parser.into_elem_iter().count();
576
577        // peer 185.1.8.50 has 1563 elements
578        assert_eq!(count, 1563);
579    }
580
581    #[test]
582    fn test_add_filters() {
583        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
584
585        // Build filters independently
586        let filters = vec![
587            Filter::new("peer_ip", "185.1.8.65").unwrap(),
588            Filter::new("type", "w").unwrap(),
589        ];
590
591        // Test add_filters - extends existing filters
592        let parser = BgpkitParser::new(url).unwrap().add_filters(&filters);
593        let count = parser.into_elem_iter().count();
594
595        // peer 185.1.8.65 has 3393 total, 132 withdrawals
596        assert_eq!(count, 132);
597
598        // Test combining add_filter and add_filters
599        let parser = BgpkitParser::new(url)
600            .unwrap()
601            .add_filter("peer_ip", "185.1.8.65")
602            .unwrap()
603            .add_filters(&[Filter::new("type", "w").unwrap()]);
604        let count = parser.into_elem_iter().count();
605        assert_eq!(count, 132);
606    }
607
608    #[test]
609    fn test_with_filters_empty() {
610        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
611
612        // Test with empty filters - should return all elements
613        let parser = BgpkitParser::new(url).unwrap().with_filters(&[]);
614        let count = parser.into_elem_iter().count();
615
616        // Total elements in the file
617        assert_eq!(count, 8160);
618    }
619
620    #[test]
621    fn test_add_filters_empty() {
622        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
623
624        // Test adding empty filters - should not change behavior
625        let parser = BgpkitParser::new(url)
626            .unwrap()
627            .add_filter("peer_ip", "185.1.8.65")
628            .unwrap()
629            .add_filters(&[]);
630        let count = parser.into_elem_iter().count();
631
632        // peer 185.1.8.65 has 3393 elements
633        assert_eq!(count, 3393);
634    }
635
636    #[test]
637    fn test_with_filters_reuse() {
638        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
639
640        // Build filters once
641        let filters = vec![
642            Filter::new("peer_ip", "185.1.8.65").unwrap(),
643            Filter::new("type", "w").unwrap(),
644        ];
645
646        // Apply to multiple parsers (simulating reuse pattern - no clone needed)
647        let parser1 = BgpkitParser::new(url).unwrap().with_filters(&filters);
648        let count1 = parser1.into_elem_iter().count();
649
650        let parser2 = BgpkitParser::new(url).unwrap().with_filters(&filters);
651        let count2 = parser2.into_elem_iter().count();
652
653        // Both should have same count: 132 withdrawals from peer 185.1.8.65
654        assert_eq!(count1, 132);
655        assert_eq!(count2, 132);
656    }
657
658    #[test]
659    fn test_from_text_reader_inline() {
660        let dump = "BGP table version is 1, local router ID is 1.2.3.4, vrf id 0\n\
661Default local pref 100, local AS 65001\n\n\
662    Network          Next Hop            Metric LocPrf Weight Path\n\
663 *> 1.0.0.0/24       10.0.0.1                 0             0 13335 i\n";
664        let parser =
665            BgpkitParser::from_text_reader(dump.as_bytes()).expect("inline text-dump parse");
666        let elems: Vec<_> = parser.into_elem_iter().collect();
667        assert_eq!(elems.len(), 1);
668        assert_eq!(elems[0].prefix.prefix.to_string(), "1.0.0.0/24");
669        assert_eq!(elems[0].peer_ip.to_string(), "1.2.3.4");
670        assert_eq!(u32::from(elems[0].peer_asn), 65001);
671        assert_eq!(elems[0].origin_asns, Some(vec![Asn::from(13335u32)]));
672    }
673
674    #[test]
675    fn test_from_auto_reader_detects_text() {
676        let dump = "BGP table version is 1, local router ID is 1.2.3.4, vrf id 0\n\
677Default local pref 100, local AS 65001\n\n\
678    Network          Next Hop            Metric LocPrf Weight Path\n\
679 *> 1.0.0.0/24       10.0.0.1                 0             0 13335 i\n";
680        let parser = BgpkitParser::from_auto_reader(dump.as_bytes()).expect("auto-detect parse");
681        let elems: Vec<_> = parser.into_elem_iter().collect();
682        assert_eq!(elems.len(), 1);
683        assert_eq!(elems[0].origin_asns, Some(vec![Asn::from(13335u32)]));
684    }
685
686    #[test]
687    fn test_from_auto_reader_detects_mrt() {
688        // A few zero bytes — not a text dump, so auto-detect should fall
689        // through to the MRT path (which will then hit EOF cleanly).
690        let data: Vec<u8> = vec![0x00u8; 16];
691        let parser =
692            BgpkitParser::from_auto_reader(std::io::Cursor::new(data)).expect("auto-detect parse");
693        let count = parser.into_elem_iter().count();
694        assert_eq!(count, 0);
695    }
696
697    #[test]
698    fn test_text_dump_parser_with_filter() {
699        let dump = "BGP table version is 1, local router ID is 1.2.3.4, vrf id 0\n\
700Default local pref 100, local AS 65001\n\n\
701    Network          Next Hop            Metric LocPrf Weight Path\n\
702 *> 1.0.0.0/24       10.0.0.1                 0             0 13335 i\n\
703 *> 8.8.8.0/24       10.0.0.2                 0             0 15169 i\n";
704        let parser = BgpkitParser::from_text_reader(dump.as_bytes())
705            .unwrap()
706            .add_filter("origin_asn", "13335")
707            .unwrap();
708        let elems: Vec<_> = parser.into_elem_iter().collect();
709        assert_eq!(elems.len(), 1);
710        assert_eq!(elems[0].prefix.prefix.to_string(), "1.0.0.0/24");
711    }
712
713    #[test]
714    fn test_text_dump_next_record_errors() {
715        let dump = "BGP table version is 1, local router ID is 1.2.3.4, vrf id 0\n\
716Default local pref 100, local AS 65001\n\n\
717    Network          Next Hop            Metric LocPrf Weight Path\n\
718 *> 1.0.0.0/24       10.0.0.1                 0             0 13335 i\n";
719        let mut parser =
720            BgpkitParser::from_text_reader(dump.as_bytes()).expect("inline text-dump parse");
721        assert!(parser.next_record().is_err());
722    }
723
724    #[test]
725    fn test_text_dump_record_iter_terminates() {
726        // Calling into_record_iter on a text-dump parser should yield 0
727        // records immediately, not spin forever on Unsupported errors.
728        let dump = "BGP table version is 1, local router ID is 1.2.3.4, vrf id 0\n\
729Default local pref 100, local AS 65001\n\n\
730    Network          Next Hop            Metric LocPrf Weight Path\n\
731 *> 1.0.0.0/24       10.0.0.1                 0             0 13335 i\n";
732        let parser =
733            BgpkitParser::from_text_reader(dump.as_bytes()).expect("inline text-dump parse");
734        assert_eq!(parser.into_record_iter().count(), 0);
735    }
736}