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