Skip to main content

bgpkit_parser/parser/
mod.rs

1/*!
2parser module maintains the main logic for processing BGP and MRT messages.
3*/
4use std::io::Read;
5
6#[macro_use]
7pub mod utils;
8pub mod bgp;
9pub mod bmp;
10pub mod filter;
11pub mod iters;
12pub mod mrt;
13pub mod rpki;
14
15#[cfg(feature = "rislive")]
16pub mod rislive;
17
18pub(crate) use self::utils::*;
19
20use crate::models::MrtRecord;
21pub use mrt::mrt_elem::{BgpUpdateElemIter, ElemError, Elementor, RecordElemIter};
22#[cfg(feature = "oneio")]
23use oneio::{get_cache_reader, get_reader};
24
25pub use crate::error::{ParserError, ParserErrorWithBytes};
26pub use bmp::{parse_bmp_msg, parse_openbmp_header, parse_openbmp_msg};
27pub use filter::*;
28pub use iters::*;
29pub use mrt::*;
30
31#[cfg(feature = "rislive")]
32pub use rislive::messages::{
33    RisLiveClientMessage, RisSubscribe, RisSubscribeSocketOptions, RisSubscribeType,
34};
35#[cfg(feature = "rislive")]
36pub use rislive::{
37    parse_ris_live_message, parse_ris_live_message_json, parse_ris_live_message_raw,
38};
39
40pub struct BgpkitParser<R> {
41    reader: R,
42    core_dump: bool,
43    filters: Vec<Filter>,
44    options: ParserOptions,
45}
46
47pub(crate) struct ParserOptions {
48    show_warnings: bool,
49}
50impl Default for ParserOptions {
51    fn default() -> Self {
52        ParserOptions {
53            show_warnings: true,
54        }
55    }
56}
57
58#[cfg(feature = "oneio")]
59impl BgpkitParser<Box<dyn Read + Send>> {
60    /// Creating a new parser from a object that implements [Read] trait.
61    pub fn new(path: &str) -> Result<Self, ParserErrorWithBytes> {
62        let reader = get_reader(path)?;
63        Ok(BgpkitParser {
64            reader,
65            core_dump: false,
66            filters: vec![],
67            options: ParserOptions::default(),
68        })
69    }
70
71    /// Creating a new parser that also caches the remote content to a local cache directory.
72    ///
73    /// The cache file name is generated by the following format: `cache-<crc32 of file name>-<file name>`.
74    /// For example, the remote file `http://archive.routeviews.org/route-views.chile/bgpdata/2023.03/RIBS/rib.20230326.0600.bz2`
75    /// will be cached as `cache-682cb1eb-rib.20230326.0600.bz2` in the cache directory.
76    pub fn new_cached(path: &str, cache_dir: &str) -> Result<Self, ParserErrorWithBytes> {
77        let file_name = path.rsplit('/').next().unwrap().to_string();
78        let new_file_name = format!(
79            "cache-{}",
80            add_suffix_to_filename(file_name.as_str(), crc32(path).as_str())
81        );
82        let reader = get_cache_reader(path, cache_dir, Some(new_file_name), false)?;
83        Ok(BgpkitParser {
84            reader,
85            core_dump: false,
86            filters: vec![],
87            options: ParserOptions::default(),
88        })
89    }
90}
91
92#[cfg(feature = "oneio")]
93fn add_suffix_to_filename(filename: &str, suffix: &str) -> String {
94    let mut parts: Vec<&str> = filename.split('.').collect(); // Split filename by dots
95    if parts.len() > 1 {
96        let last_part = parts.pop().unwrap(); // Remove the last part (suffix) from the parts vector
97        let new_last_part = format!("{suffix}.{last_part}"); // Add the suffix to the last part
98        parts.push(&new_last_part); // Add the updated last part back to the parts vector
99        parts.join(".") // Join the parts back into a filename string with dots
100    } else {
101        // If the filename does not have any dots, simply append the suffix to the end
102        format!("{filename}.{suffix}")
103    }
104}
105
106impl<R: Read> BgpkitParser<R> {
107    /// Creating a new parser from an object that implements [Read] trait.
108    pub fn from_reader(reader: R) -> Self {
109        BgpkitParser {
110            reader,
111            core_dump: false,
112            filters: vec![],
113            options: ParserOptions::default(),
114        }
115    }
116
117    /// This is used in for loop `for item in parser{}`
118    pub fn next_record(&mut self) -> Result<MrtRecord, ParserErrorWithBytes> {
119        parse_mrt_record(&mut self.reader)
120    }
121}
122
123impl<R> BgpkitParser<R> {
124    pub fn enable_core_dump(self) -> Self {
125        BgpkitParser {
126            reader: self.reader,
127            core_dump: true,
128            filters: self.filters,
129            options: self.options,
130        }
131    }
132
133    pub fn disable_warnings(self) -> Self {
134        let mut options = self.options;
135        options.show_warnings = false;
136        BgpkitParser {
137            reader: self.reader,
138            core_dump: self.core_dump,
139            filters: self.filters,
140            options,
141        }
142    }
143
144    /// Add a filter to the parser by specifying filter type and value as strings.
145    ///
146    /// This method parses the filter type and value strings to create a [`Filter`] and adds it
147    /// to the parser's filter list. For the full list of available filter types and their
148    /// formats, see the [`Filter`] struct documentation.
149    ///
150    /// # Available Filter Types
151    ///
152    /// - `origin_asn` - Origin AS number (e.g., "12345")
153    /// - `origin_asns` - Multiple origin AS numbers, comma-separated (e.g., "12345,67890")
154    /// - `prefix` - Exact prefix match (e.g., "192.168.1.0/24")
155    /// - `prefix_super` - Match prefix and super-prefixes
156    /// - `prefix_sub` - Match prefix and sub-prefixes
157    /// - `prefix_super_sub` - Match prefix, super-prefixes, and sub-prefixes
158    /// - `prefixes` - Multiple prefixes (e.g., "1.1.1.0/24,8.8.8.0/24")
159    /// - `peer_ip` - Peer IP address (e.g., "192.168.1.1")
160    /// - `peer_ips` - Multiple peer IPs (e.g., "192.168.1.1,192.168.1.2")
161    /// - `peer_asn` - Peer AS number (e.g., "12345")
162    /// - `peer_asns` - Multiple peer AS numbers (e.g., "12345,67890")
163    /// - `type` - Message type: "a"/"announce" or "w"/"withdraw"
164    /// - `ts_start` - Start timestamp (unix timestamp or RFC3339)
165    /// - `ts_end` - End timestamp (unix timestamp or RFC3339)
166    /// - `as_path` - AS path regex pattern
167    /// - `community` - Community regex pattern
168    /// - `ip_version` - IP version: "4"/"ipv4" or "6"/"ipv6"
169    /// - `otc` - Only-to-customer ASN (RFC 9234); `*` for present, `!*` for absent
170    /// - `next_hop` - Next hop IP address; `*`/`!*` for presence
171    /// - `origin` - Origin attribute: "igp", "egp", or "incomplete"; `*`/`!*` for presence
172    /// - `local_pref` - Local preference value; `*`/`!*` for presence
173    /// - `med` - Multi-exit discriminator value; `*`/`!*` for presence
174    /// - `atomic` - Atomic aggregate flag: "true"/"false"
175    /// - `aggr_asn` - Aggregator ASN; `*`/`!*` for presence
176    /// - `aggr_ip` - Aggregator IP address; `*`/`!*` for presence
177    /// - `peer_bgp_id` - Peer BGP identifier (router ID); `*`/`!*` for presence
178    ///
179    /// # Negative Filters
180    ///
181    /// Most filters support negation by prefixing the value with `!`. For example:
182    /// - `origin_asn=!13335` matches elements where origin AS is NOT 13335
183    /// - `prefix=!10.0.0.0/8` matches elements where prefix is NOT 10.0.0.0/8
184    ///
185    /// # Presence Filters
186    ///
187    /// Optional fields (`Option<T>`) support `*` as a wildcard to check whether a field
188    /// is present or absent: `otc=*` matches elements with an OTC value, `otc=!*` matches
189    /// elements without one.
190    ///
191    /// # Example
192    ///
193    /// ```no_run
194    /// use bgpkit_parser::BgpkitParser;
195    ///
196    /// let parser = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
197    ///     .unwrap()
198    ///     .add_filter("peer_ip", "185.1.8.65")
199    ///     .unwrap()
200    ///     .add_filter("type", "w")
201    ///     .unwrap();
202    ///
203    /// for elem in parser {
204    ///     println!("{}", elem);
205    /// }
206    /// ```
207    pub fn add_filter(
208        self,
209        filter_type: &str,
210        filter_value: &str,
211    ) -> Result<Self, ParserErrorWithBytes> {
212        let mut filters = self.filters;
213        filters.push(Filter::new(filter_type, filter_value)?);
214        Ok(BgpkitParser {
215            reader: self.reader,
216            core_dump: self.core_dump,
217            filters,
218            options: self.options,
219        })
220    }
221
222    /// Add multiple filters to the parser.
223    ///
224    /// This method extends the existing filters with the provided slice of filters.
225    ///
226    /// # Example
227    ///
228    /// ```no_run
229    /// use bgpkit_parser::BgpkitParser;
230    /// use bgpkit_parser::parser::Filter;
231    ///
232    /// let filters = vec![
233    ///     Filter::new("peer_ip", "185.1.8.65").unwrap(),
234    ///     Filter::new("type", "w").unwrap(),
235    /// ];
236    ///
237    /// let parser = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
238    ///     .unwrap()
239    ///     .add_filters(&filters);
240    /// ```
241    pub fn add_filters(mut self, filters: &[Filter]) -> Self {
242        self.filters.extend(filters.iter().cloned());
243        self
244    }
245
246    /// Set filters directly, replacing any existing filters.
247    ///
248    /// This method allows passing a pre-built `Vec<Filter>` directly to the parser,
249    /// bypassing the need to parse filter strings. This is useful when you want to
250    /// build filter specifications independently and reuse them across multiple parsers.
251    ///
252    /// # Example
253    ///
254    /// ```no_run
255    /// use bgpkit_parser::BgpkitParser;
256    /// use bgpkit_parser::parser::Filter;
257    ///
258    /// // Build filters independently
259    /// let filters = vec![
260    ///     Filter::new("peer_ip", "185.1.8.65").unwrap(),
261    ///     Filter::new("type", "w").unwrap(),
262    /// ];
263    ///
264    /// // Apply to multiple parsers (no manual clone needed)
265    /// let parser1 = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
266    ///     .unwrap()
267    ///     .with_filters(&filters);
268    ///
269    /// let parser2 = BgpkitParser::new("https://spaces.bgpkit.org/parser/update-example.gz")
270    ///     .unwrap()
271    ///     .with_filters(&filters);
272    /// ```
273    pub fn with_filters(mut self, filters: &[Filter]) -> Self {
274        self.filters = filters.to_vec();
275        self
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_new_with_reader() {
285        // bzip2 reader for a compressed file
286        let reader = oneio::get_reader("http://archive.routeviews.org/route-views.ny/bgpdata/2023.02/UPDATES/updates.20230215.0630.bz2").unwrap();
287        assert_eq!(
288            12683,
289            BgpkitParser::from_reader(reader).into_elem_iter().count()
290        );
291
292        // remote reader for an uncompressed updates file
293        let reader = oneio::get_reader("https://spaces.bgpkit.org/parser/update-example").unwrap();
294        assert_eq!(
295            8160,
296            BgpkitParser::from_reader(reader).into_elem_iter().count()
297        );
298    }
299
300    #[test]
301    fn test_new_cached_with_reader() {
302        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
303        let parser = BgpkitParser::new_cached(url, "/tmp/bgpkit-parser-tests")
304            .unwrap()
305            .enable_core_dump()
306            .disable_warnings();
307        let count = parser.into_elem_iter().count();
308        assert_eq!(8160, count);
309        let parser = BgpkitParser::new_cached(url, "/tmp/bgpkit-parser-tests").unwrap();
310        let count = parser.into_elem_iter().count();
311        assert_eq!(8160, count);
312    }
313
314    #[test]
315    fn test_add_suffix_to_filename() {
316        // Test with a filename that has dots
317        let filename = "example.txt";
318        let suffix = "suffix";
319        let result = add_suffix_to_filename(filename, suffix);
320        assert_eq!(result, "example.suffix.txt");
321
322        // Test with a filename that has multiple dots
323        let filename = "example.tar.gz";
324        let suffix = "suffix";
325        let result = add_suffix_to_filename(filename, suffix);
326        assert_eq!(result, "example.tar.suffix.gz");
327
328        // Test with a filename that has no dots
329        let filename = "example";
330        let suffix = "suffix";
331        let result = add_suffix_to_filename(filename, suffix);
332        assert_eq!(result, "example.suffix");
333
334        // Test with an empty filename
335        let filename = "";
336        let suffix = "suffix";
337        let result = add_suffix_to_filename(filename, suffix);
338        assert_eq!(result, ".suffix");
339
340        // Test with an empty suffix
341        let filename = "example.txt";
342        let suffix = "";
343        let result = add_suffix_to_filename(filename, suffix);
344        assert_eq!(result, "example..txt");
345    }
346
347    #[test]
348    fn test_with_filters() {
349        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
350
351        // Build filters independently
352        let filters = vec![
353            Filter::new("peer_ip", "185.1.8.65").unwrap(),
354            Filter::new("type", "w").unwrap(),
355        ];
356
357        // Test with_filters - sets filters directly
358        let parser = BgpkitParser::new(url).unwrap().with_filters(&filters);
359        let count = parser.into_elem_iter().count();
360
361        // peer 185.1.8.65 has 3393 total, 132 withdrawals
362        assert_eq!(count, 132);
363
364        // Test that with_filters replaces existing filters
365        let filters1 = vec![Filter::new("peer_ip", "185.1.8.65").unwrap()];
366        let filters2 = vec![Filter::new("peer_ip", "185.1.8.50").unwrap()];
367
368        let parser = BgpkitParser::new(url)
369            .unwrap()
370            .with_filters(&filters1)
371            .with_filters(&filters2); // Should replace filters1
372        let count = parser.into_elem_iter().count();
373
374        // peer 185.1.8.50 has 1563 elements
375        assert_eq!(count, 1563);
376    }
377
378    #[test]
379    fn test_add_filters() {
380        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
381
382        // Build filters independently
383        let filters = vec![
384            Filter::new("peer_ip", "185.1.8.65").unwrap(),
385            Filter::new("type", "w").unwrap(),
386        ];
387
388        // Test add_filters - extends existing filters
389        let parser = BgpkitParser::new(url).unwrap().add_filters(&filters);
390        let count = parser.into_elem_iter().count();
391
392        // peer 185.1.8.65 has 3393 total, 132 withdrawals
393        assert_eq!(count, 132);
394
395        // Test combining add_filter and add_filters
396        let parser = BgpkitParser::new(url)
397            .unwrap()
398            .add_filter("peer_ip", "185.1.8.65")
399            .unwrap()
400            .add_filters(&[Filter::new("type", "w").unwrap()]);
401        let count = parser.into_elem_iter().count();
402        assert_eq!(count, 132);
403    }
404
405    #[test]
406    fn test_with_filters_empty() {
407        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
408
409        // Test with empty filters - should return all elements
410        let parser = BgpkitParser::new(url).unwrap().with_filters(&[]);
411        let count = parser.into_elem_iter().count();
412
413        // Total elements in the file
414        assert_eq!(count, 8160);
415    }
416
417    #[test]
418    fn test_add_filters_empty() {
419        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
420
421        // Test adding empty filters - should not change behavior
422        let parser = BgpkitParser::new(url)
423            .unwrap()
424            .add_filter("peer_ip", "185.1.8.65")
425            .unwrap()
426            .add_filters(&[]);
427        let count = parser.into_elem_iter().count();
428
429        // peer 185.1.8.65 has 3393 elements
430        assert_eq!(count, 3393);
431    }
432
433    #[test]
434    fn test_with_filters_reuse() {
435        let url = "https://spaces.bgpkit.org/parser/update-example.gz";
436
437        // Build filters once
438        let filters = vec![
439            Filter::new("peer_ip", "185.1.8.65").unwrap(),
440            Filter::new("type", "w").unwrap(),
441        ];
442
443        // Apply to multiple parsers (simulating reuse pattern - no clone needed)
444        let parser1 = BgpkitParser::new(url).unwrap().with_filters(&filters);
445        let count1 = parser1.into_elem_iter().count();
446
447        let parser2 = BgpkitParser::new(url).unwrap().with_filters(&filters);
448        let count2 = parser2.into_elem_iter().count();
449
450        // Both should have same count: 132 withdrawals from peer 185.1.8.65
451        assert_eq!(count1, 132);
452        assert_eq!(count2, 132);
453    }
454}