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