bgpkit_parser/parser/iters/mod.rs
1/*!
2Iterator implementations for bgpkit-parser.
3
4This module contains different iterator implementations for parsing BGP data:
5- `default`: Standard iterators that skip errors (RecordIterator, ElemIterator)
6- `fallible`: Fallible iterators that return Results (FallibleRecordIterator, FallibleElemIterator)
7- `update`: Iterators for BGP UPDATE messages (UpdateIterator, FallibleUpdateIterator)
8- `route`: Route-level iterators (RouteIterator, FallibleRouteIterator)
9- `recovery`: Iterators that survive damaged MRT framing (RecoveringRecordIterator,
10 RecoveringElemIterator, RecoveryEvent)
11- `diagnostic`: Per-record dissection and validation reporting (DiagnosticIterator,
12 DissectingDiagnosticIterator)
13- `raw`: Raw MRT records with their original bytes (RawRecordIterator,
14 FilteredRawRecordIterator)
15
16It also contains the trait implementations that enable BgpkitParser to be used with
17Rust's iterator syntax.
18*/
19
20pub mod default;
21mod diagnostic;
22pub mod fallible;
23mod raw;
24mod recovery;
25mod route;
26mod update;
27
28// Re-export all iterator types for convenience
29pub use default::{ElemIterator, RecordIterator};
30pub use diagnostic::{
31 record_validation_warnings, span_record_warnings, DiagnosticEvent, DiagnosticIterator,
32 DissectedDiagnosticEvent, DissectingDiagnosticIterator,
33};
34pub use fallible::{FallibleElemIterator, FallibleRecordIterator};
35pub use raw::{FilteredRawRecordIterator, RawRecordIterator};
36pub use recovery::{
37 RecoveringElemIterator, RecoveringRecordIterator, RecoveryConfig, RecoveryError, RecoveryEvent,
38 RecoveryEvidence, RecoveryGap,
39};
40pub use route::{FallibleRouteIterator, RouteIterator};
41pub use update::{
42 Bgp4MpUpdate, FallibleUpdateIterator, LegacyBgpUpdate, MrtUpdate, TableDumpV2Entry,
43 UpdateIterator,
44};
45
46use crate::error::{ParserError, ParserErrorWithBytes};
47use crate::models::BgpElem;
48use crate::models::{MrtMessage, MrtRecord, TableDumpV2Message};
49use crate::parser::mrt::mrt_record::parse_chunked_record_with_zebra_compat;
50use crate::parser::{chunk_mrt_record, BgpkitParser};
51use crate::RawMrtRecord;
52use crate::{Elementor, Filter, Filterable};
53use log::{debug, error, warn};
54use std::io::Read;
55use std::path::Path;
56
57// Classify stream failures while framing: body parsing can also return
58// skippable IoErrors, but a failed decompressor must not be polled again.
59fn next_fallible_raw_record(
60 reader: &mut impl Read,
61 finished: &mut bool,
62) -> Option<Result<RawMrtRecord, ParserErrorWithBytes>> {
63 if *finished {
64 return None;
65 }
66 match chunk_mrt_record(reader) {
67 Ok(record) => Some(Ok(record)),
68 Err(error) if matches!(error.error, ParserError::EofExpected) => {
69 *finished = true;
70 None
71 }
72 Err(error) => {
73 match &error.error {
74 ParserError::IoError(io_error) | ParserError::EofError(io_error) => {
75 // A resumed pull starts a fresh header, so retrying is only safe
76 // when the failed read consumed nothing; otherwise the partial
77 // framing bytes are gone and the stream stays misaligned.
78 let nothing_consumed = error.bytes.as_ref().is_none_or(Vec::is_empty);
79 *finished = !(nothing_consumed
80 && matches!(
81 io_error.kind(),
82 std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
83 ));
84 }
85 ParserError::OversizedRecord { .. } => {
86 // no record boundary to resume from
87 *finished = true;
88 }
89 _ => {}
90 }
91 Some(Err(error))
92 }
93 }
94}
95
96fn next_fallible_record<R: Read>(
97 parser: &mut BgpkitParser<R>,
98 finished: &mut bool,
99) -> Option<Result<MrtRecord, ParserErrorWithBytes>> {
100 let raw_record = match next_fallible_raw_record(&mut parser.reader, finished)? {
101 Ok(record) => record,
102 Err(error) => return Some(Err(error)),
103 };
104 Some(match parse_chunked_record_with_zebra_compat(raw_record) {
105 Ok((record, used_zebra_compat)) => {
106 if used_zebra_compat {
107 parser.warn_zebra_compat_once();
108 }
109 Ok(record)
110 }
111 Err(error) => Err(error),
112 })
113}
114
115#[inline]
116pub(crate) fn record_matches_filters(
117 record: &MrtRecord,
118 filters: &[Filter],
119 elementor: &mut Elementor,
120) -> bool {
121 if filters.is_empty() {
122 return true;
123 }
124 if matches!(
125 &record.message,
126 MrtMessage::TableDumpV2Message(TableDumpV2Message::PeerIndexTable(_))
127 ) {
128 let _ = elementor.record_to_elems(record.clone());
129 return true;
130 }
131 // Filters match on the elem projection. Records that produce no elems
132 // (KEEPALIVE, OPEN, NOTIFICATION, state changes) can therefore never
133 // match and are dropped from record iteration while filters are active.
134 let elems = elementor.record_to_elems(record.clone());
135 if elems.is_empty() {
136 debug!(
137 "filters active: record of type {:?} yields no elems and is dropped",
138 record.common_header.entry_type
139 );
140 return false;
141 }
142 elems.iter().any(|element| element.match_filters(filters))
143}
144
145/// Shared body-parse error policy for record-producing iterators.
146///
147/// Mirrors the historical `RecordIterator` behavior: warnings honor
148/// `disable_warnings()`, core dumps are written for recoverable classes,
149/// and a fatal `ParseError` with core dumps enabled stops the iterator so
150/// a later failure cannot overwrite the dump. Returns `true` to continue
151/// iterating, `false` to stop.
152pub(crate) fn handle_record_parse_error<R>(
153 parser: &mut crate::parser::BgpkitParser<R>,
154 error: ParserError,
155 bytes: Option<Vec<u8>>,
156) -> bool {
157 match error {
158 ParserError::TruncatedMsg(err_str) | ParserError::Unsupported(err_str) => {
159 if parser.options.show_warnings {
160 warn!("parser warn: {}", err_str);
161 }
162 write_mrt_core_dump(parser.core_dump, bytes);
163 true
164 }
165 ParserError::ParseError(err_str) => {
166 parser.log_parse_error_once(&err_str);
167 write_mrt_core_dump(parser.core_dump, bytes);
168 // stop after writing the dump so later failures don't overwrite it
169 !parser.core_dump
170 }
171 ParserError::EofExpected => {
172 // normal end of file
173 false
174 }
175 ParserError::OversizedRecord { length } => {
176 // no record boundary to resume from, stop iterating
177 error!(
178 "parser error: MRT message too large: {} bytes; stream position is unreliable, stopping",
179 length
180 );
181 write_mrt_core_dump(parser.core_dump, bytes);
182 false
183 }
184 ParserError::IoError(err) | ParserError::EofError(err) => {
185 // when reaching IO error, stop iterating
186 error!("{:?}", err);
187 write_mrt_core_dump(parser.core_dump, bytes);
188 false
189 }
190 #[cfg(feature = "oneio")]
191 ParserError::OneIoError(_) => false,
192 ParserError::FilterError(_) => {
193 // this should not happen at this stage
194 false
195 }
196 // Labeled NLRI parsing errors - treat as malformed and skip
197 ParserError::InvalidLabeledNlriLength
198 | ParserError::TruncatedLabeledNlri
199 | ParserError::TruncatedPrefix
200 | ParserError::MaxLabelStackDepthExceeded
201 | ParserError::PeerMaxLabelsExceeded
202 | ParserError::InvalidPrefix => {
203 if parser.options.show_warnings {
204 warn!("parser warn: labeled NLRI parsing error: {:?}", error);
205 }
206 true
207 }
208 }
209}
210
211pub(crate) fn write_mrt_core_dump(enabled: bool, bytes: Option<Vec<u8>>) {
212 write_mrt_core_dump_to_path(enabled, bytes, "mrt_core_dump");
213}
214
215pub(crate) fn write_mrt_core_dump_to_path<P: AsRef<Path>>(
216 enabled: bool,
217 bytes: Option<Vec<u8>>,
218 path: P,
219) {
220 if enabled {
221 if let Some(bytes) = bytes {
222 std::fs::write(path, bytes).expect("Unable to write to mrt_core_dump");
223 }
224 }
225}
226
227/// Use [ElemIterator] as the default iterator to return [BgpElem]s instead of [MrtRecord]s.
228impl<R: Read> IntoIterator for BgpkitParser<R> {
229 type Item = BgpElem;
230 type IntoIter = ElemIterator<R>;
231
232 fn into_iter(self) -> Self::IntoIter {
233 ElemIterator::new(self)
234 }
235}
236
237impl<R> BgpkitParser<R> {
238 pub fn into_record_iter(self) -> RecordIterator<R> {
239 RecordIterator::new(self)
240 }
241
242 pub fn into_elem_iter(self) -> ElemIterator<R> {
243 ElemIterator::new(self)
244 }
245
246 pub fn into_raw_record_iter(self) -> RawRecordIterator<R> {
247 RawRecordIterator::new(self)
248 }
249
250 /// Creates an iterator over raw MRT records with record-level filter
251 /// semantics applied.
252 ///
253 /// Like [`into_raw_record_iter`](Self::into_raw_record_iter), but only
254 /// records passing the parser's filters are yielded (same semantics as
255 /// [`into_record_iter`](Self::into_record_iter): filters match on the
256 /// elem projection, so no-elem records such as KEEPALIVEs are dropped
257 /// while filters are active, and the `PeerIndexTable` always passes).
258 /// The yielded records carry their original wire bytes — no
259 /// re-encoding — which is what byte-exact consumers (hex output,
260 /// re-dissection) need. Every record body is parsed once inside the
261 /// iterator and the parsed record is yielded alongside, so consumers
262 /// do not parse the bytes twice; parse failures follow the same
263 /// variant-aware diagnostics as the record iterator.
264 ///
265 /// # Example
266 /// ```no_run
267 /// use bgpkit_parser::BgpkitParser;
268 ///
269 /// let parser = BgpkitParser::new("updates.mrt").unwrap();
270 /// for (raw, _) in parser.into_filtered_raw_record_iter() {
271 /// println!("{}", raw.raw_bytes().len());
272 /// }
273 /// ```
274 pub fn into_filtered_raw_record_iter(self) -> FilteredRawRecordIterator<R> {
275 FilteredRawRecordIterator::new(self)
276 }
277
278 /// Creates an opt-in iterator that reports skipped byte ranges while recovering MRT framing.
279 ///
280 /// Recovery never reconstructs a damaged record. It scans for a structurally valid boundary,
281 /// confirms a chain of records, emits [`RecoveryEvent::Gap`], and then resumes normal parsing.
282 /// Damage extending to the end of the stream is reported as a terminal gap. Offsets in
283 /// recovery events refer to the decompressed MRT byte stream.
284 pub fn into_recovering_record_iter(
285 self,
286 config: RecoveryConfig,
287 ) -> RecoveringRecordIterator<R> {
288 RecoveringRecordIterator::new(self, config)
289 }
290
291 /// Creates an opt-in iterator over BGP elements that reports skipped byte ranges while
292 /// recovering MRT framing.
293 ///
294 /// Behaves like [`into_recovering_record_iter`](Self::into_recovering_record_iter) but
295 /// converts each recovered record to [`BgpElem`]s, applying the parser's filters per
296 /// element.
297 pub fn into_recovering_elem_iter(self, config: RecoveryConfig) -> RecoveringElemIterator<R> {
298 RecoveringElemIterator::new(self, config)
299 }
300
301 /// Creates an iterator over BGP announcements from MRT data.
302 ///
303 /// This iterator yields `MrtUpdate` items from both UPDATES files (BGP4MP messages)
304 /// and RIB dump files (TableDump/TableDumpV2 messages). It's a middle ground
305 /// between `into_record_iter()` and `into_elem_iter()`:
306 ///
307 /// - More focused than `into_record_iter()` as it only returns BGP announcements
308 /// - More efficient than `into_elem_iter()` as it doesn't duplicate attributes per prefix
309 ///
310 /// The iterator returns an `MrtUpdate` enum with variants:
311 /// - `Bgp4MpUpdate`: BGP UPDATE messages from UPDATES files
312 /// - `LegacyBgpUpdate`: Deprecated MRT Type 5 BGP UPDATE messages
313 /// - `TableDumpV2Entry`: RIB entries from TableDumpV2 RIB dumps
314 /// - `TableDumpMessage`: Legacy TableDump v1 messages
315 ///
316 /// # Example
317 /// ```no_run
318 /// use bgpkit_parser::{BgpkitParser, MrtUpdate};
319 ///
320 /// let parser = BgpkitParser::new("updates.mrt").unwrap();
321 /// for update in parser.into_update_iter() {
322 /// match update {
323 /// MrtUpdate::Bgp4MpUpdate(u) => {
324 /// println!("Peer {} announced {} prefixes",
325 /// u.peer_ip,
326 /// u.message.announced_prefixes.len()
327 /// );
328 /// }
329 /// MrtUpdate::LegacyBgpUpdate(u) => {
330 /// println!("Legacy UPDATE from peer {}", u.peer_ip);
331 /// }
332 /// MrtUpdate::TableDumpV2Entry(e) => {
333 /// println!("RIB entry for {} with {} peers",
334 /// e.prefix,
335 /// e.rib_entries.len()
336 /// );
337 /// }
338 /// MrtUpdate::TableDumpMessage(m) => {
339 /// println!("Legacy table dump for {}", m.prefix);
340 /// }
341 /// }
342 /// }
343 /// ```
344 pub fn into_update_iter(self) -> UpdateIterator<R> {
345 UpdateIterator::new(self)
346 }
347
348 /// Creates an iterator over lightweight route elements from MRT data.
349 ///
350 /// This iterator yields [`BgpRouteElem`](crate::models::BgpRouteElem)
351 /// values and only parses route identity, peer metadata, timestamp, and
352 /// AS path. Use [`into_elem_iter`](Self::into_elem_iter) when you need
353 /// the full [`BgpElem`] attribute set. Filters that only depend on route
354 /// fields are supported; `community` filters do not match route elements.
355 pub fn into_route_iter(self) -> RouteIterator<R> {
356 RouteIterator::new(self)
357 }
358
359 /// Creates a fallible iterator over MRT records that returns parsing errors.
360 ///
361 /// Malformed records can be skipped. A framing I/O or decompression error
362 /// is yielded once and then ends iteration permanently, except for an
363 /// `Interrupted` or `WouldBlock` read that consumed no framing bytes, which
364 /// leaves the stream retryable. Normal EOF also ends iteration permanently.
365 ///
366 /// # Example
367 /// ```no_run
368 /// use bgpkit_parser::BgpkitParser;
369 ///
370 /// let parser = BgpkitParser::new("updates.mrt").unwrap();
371 /// for result in parser.into_fallible_record_iter() {
372 /// match result {
373 /// Ok(record) => {
374 /// // Process the record
375 /// }
376 /// Err(e) => {
377 /// // Handle the error
378 /// eprintln!("Error parsing record: {}", e);
379 /// }
380 /// }
381 /// }
382 /// ```
383 pub fn into_fallible_record_iter(self) -> FallibleRecordIterator<R> {
384 FallibleRecordIterator::new(self)
385 }
386
387 /// Creates a fallible iterator over BGP elements that returns parsing errors.
388 ///
389 /// Uses the same stream-error termination policy as
390 /// [`into_fallible_record_iter`](Self::into_fallible_record_iter).
391 ///
392 /// # Example
393 /// ```no_run
394 /// use bgpkit_parser::BgpkitParser;
395 ///
396 /// let parser = BgpkitParser::new("updates.mrt").unwrap();
397 /// for result in parser.into_fallible_elem_iter() {
398 /// match result {
399 /// Ok(elem) => {
400 /// // Process the element
401 /// }
402 /// Err(e) => {
403 /// // Handle the error
404 /// eprintln!("Error parsing element: {}", e);
405 /// }
406 /// }
407 /// }
408 /// ```
409 pub fn into_fallible_elem_iter(self) -> FallibleElemIterator<R> {
410 FallibleElemIterator::new(self)
411 }
412
413 /// Creates a fallible iterator over BGP announcements that returns parsing errors.
414 ///
415 /// Unlike the default `into_update_iter()`, this iterator returns
416 /// `Result<MrtUpdate, ParserErrorWithBytes>` allowing users to handle parsing
417 /// errors explicitly instead of having them logged and skipped. Uses the same
418 /// stream-error termination policy as
419 /// [`into_fallible_record_iter`](Self::into_fallible_record_iter).
420 ///
421 /// # Example
422 /// ```no_run
423 /// use bgpkit_parser::{BgpkitParser, MrtUpdate};
424 ///
425 /// let parser = BgpkitParser::new("updates.mrt").unwrap();
426 /// for result in parser.into_fallible_update_iter() {
427 /// match result {
428 /// Ok(MrtUpdate::Bgp4MpUpdate(update)) => {
429 /// println!("Peer {} announced {} prefixes",
430 /// update.peer_ip,
431 /// update.message.announced_prefixes.len()
432 /// );
433 /// }
434 /// Ok(_) => { /* handle other variants */ }
435 /// Err(e) => {
436 /// eprintln!("Error parsing: {}", e);
437 /// }
438 /// }
439 /// }
440 /// ```
441 pub fn into_fallible_update_iter(self) -> FallibleUpdateIterator<R> {
442 FallibleUpdateIterator::new(self)
443 }
444
445 /// Creates a fallible iterator over lightweight route elements.
446 ///
447 /// Uses the same stream-error termination policy as
448 /// [`into_fallible_record_iter`](Self::into_fallible_record_iter).
449 pub fn into_fallible_route_iter(self) -> FallibleRouteIterator<R> {
450 FallibleRouteIterator::new(self)
451 }
452
453 /// Creates an iterator that classifies each MRT record for malformed-data investigation.
454 ///
455 /// This iterator emits every record with its raw bytes attached: clean
456 /// records have empty warning lists, records with recoverable RFC 7606
457 /// validation findings carry them in `warnings`, and fatal parse errors
458 /// retain the consumed bytes plus a best-effort partial dissection tree
459 /// showing where parsing stopped. It ignores parser filters so that
460 /// malformed records cannot be hidden by element-oriented matching. Text-dump parsers yield
461 /// no diagnostic events because they have no MRT record representation.
462 ///
463 /// # Example
464 /// ```no_run
465 /// use bgpkit_parser::{BgpkitParser, DiagnosticEvent};
466 ///
467 /// for event in BgpkitParser::new("updates.mrt")?.into_diagnostic_iter() {
468 /// match event {
469 /// DiagnosticEvent::Record { record, raw, warnings } => {
470 /// if warnings.is_empty() {
471 /// println!("{record}");
472 /// } else {
473 /// eprintln!("validation findings: {warnings:?}");
474 /// raw.write_raw_bytes("malformed-record.mrt")?;
475 /// }
476 /// }
477 /// DiagnosticEvent::ParseError { error, raw_bytes, .. } => {
478 /// eprintln!("parse error: {error}");
479 /// if let Some(raw_bytes) = raw_bytes {
480 /// std::fs::write("malformed-record.mrt", raw_bytes)?;
481 /// }
482 /// }
483 /// _ => {}
484 /// }
485 /// }
486 /// # Ok::<(), Box<dyn std::error::Error>>(())
487 /// ```
488 pub fn into_diagnostic_iter(self) -> DiagnosticIterator<R> {
489 DiagnosticIterator::new(self)
490 }
491
492 /// Creates an Elementor pre-initialized with PeerIndexTable and an iterator over raw records.
493 ///
494 /// This is useful for parallel processing where the Elementor needs to be shared across threads.
495 /// The Elementor is created with the PeerIndexTable from the first record if present,
496 /// otherwise a new Elementor is created.
497 ///
498 /// # Example
499 /// See the `parallel_records_to_elem` example for full usage.
500 /// ```ignore
501 /// use bgpkit_parser::BgpkitParser;
502 ///
503 /// let parser = BgpkitParser::new_cached(url, "/tmp")?;
504 /// let (elementor, records) = parser.into_elementor_and_raw_record_iter();
505 /// ```
506 ///
507 pub fn into_elementor_and_raw_record_iter(
508 self,
509 ) -> (Elementor, impl Iterator<Item = RawMrtRecord>)
510 where
511 R: Read,
512 {
513 let mut raw_iter = RawRecordIterator::new(self).peekable();
514 let elementor = match raw_iter.peek().cloned().and_then(|r| r.parse().ok()) {
515 Some(MrtRecord {
516 message: MrtMessage::TableDumpV2Message(TableDumpV2Message::PeerIndexTable(pit)),
517 ..
518 }) => {
519 raw_iter.next();
520 Elementor::with_peer_table(pit)
521 }
522 _ => Elementor::new(),
523 };
524 (elementor, raw_iter)
525 }
526
527 /// Creates an Elementor pre-initialized with PeerIndexTable and an iterator over parsed records.
528 ///
529 /// This is useful for parallel processing where the Elementor needs to be shared across threads.
530 /// The Elementor is created with the PeerIndexTable from the first record if present,
531 /// otherwise a new Elementor is created.
532 ///
533 /// # Example
534 /// See the `parallel_records_to_elem` example for full usage.
535 pub fn into_elementor_and_record_iter(self) -> (Elementor, impl Iterator<Item = MrtRecord>)
536 where
537 R: Read,
538 {
539 let mut record_iter = RecordIterator::new(self).peekable();
540 let elementor = match record_iter.peek().cloned() {
541 Some(MrtRecord {
542 message: MrtMessage::TableDumpV2Message(TableDumpV2Message::PeerIndexTable(pit)),
543 ..
544 }) => {
545 record_iter.next();
546 Elementor::with_peer_table(pit)
547 }
548 _ => Elementor::new(),
549 };
550 (elementor, record_iter)
551 }
552}