Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.

* **Internal cleanups, no behavior change**: the MP next-hop encoder (`encode_mp_next_hop`) is shared by the `NEXT_HOP` and MP_REACH encoders instead of being duplicated, `AsPathSegment` hashing skips the sort when a set is already ordered, `Elementor::record_to_elems` logs peer-table conversion errors as its documentation promises, the invariant `unreachable!()` arms state their invariant, and both the crate-wide `uninlined_format_args` allow and a module-wide `#![allow(unused)]` are removed.
* **`--format text` session labels are now `PEER`/`LOCAL`**: the endpoint lines read `PEER: <peer_ip> AS<peer_asn>` and `LOCAL: <local_ip> AS<local_asn>`, matching the peer/local names MRT ([RFC 6396](https://www.rfc-editor.org/rfc/rfc6396.html)) uses for the same fields instead of the ambiguous `FROM`/`TO`. The rendered values are unchanged.
* **Faster elem conversion**: `into_elem_iter` is about 24% faster on updates and 21% faster on RIB dumps, mostly from fewer copies and allocations when building elems; output is unchanged.

## v0.22.0 - 2026-09-10

Expand Down
27 changes: 25 additions & 2 deletions benches/internals.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bgpkit_parser::BgpkitParser;
use bgpkit_parser::{BgpkitParser, Elementor};
use bzip2::bufread::BzDecoder;
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use flate2::bufread::GzDecoder;
use std::fs::File;
use std::hint::black_box;
Expand Down Expand Up @@ -78,6 +78,29 @@ pub fn criterion_benchmark(c: &mut Criterion) {
})
});

// Record-to-elem conversion on its own: the records are parsed during setup, so only the
// Elementor is timed.
let update_records: Vec<_> = BgpkitParser::from_reader(&updates[..])
.into_record_iter()
.take(RECORD_LIMIT)
.collect();
c.bench_function("updates elementor record_to_elems_iter", |b| {
b.iter_batched(
|| update_records.clone(),
|records| {
let elementor = Elementor::new();
for record in records {
if let Ok(elems) = elementor.record_to_elems_iter(record) {
elems.for_each(|x| {
black_box(x);
});
}
}
},
BatchSize::LargeInput,
)
});

c.bench_function("updates into_update_iter", |b| {
b.iter(|| {
let mut reader = black_box(&updates[..]);
Expand Down
47 changes: 42 additions & 5 deletions src/parser/bgp/attributes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,9 @@ pub fn parse_attributes(
let estimated_attrs = (data.remaining() / 3).min(256);
let mut attributes: Vec<Attribute> = Vec::with_capacity(estimated_attrs.max(8));
let mut validation = AttributeValidationState::new();
// A handle on the whole attribute section, so an attribute that fails to parse can be kept
// raw by re-slicing it, without cloning every attribute's bytes up front.
let section = data.clone();

while data.remaining() >= 3 {
// each attribute is at least 3 bytes: flag(1) + type(1) + length(1)
Expand Down Expand Up @@ -485,16 +488,16 @@ pub fn parse_attributes(

// we know data has enough bytes to read, so we can split the bytes into a new Bytes object
data.has_n_remaining(attr_length)?;
let value_start = section.len() - data.len();
let mut attr_data = data.split_to(attr_length);
let raw_bytes = attr_data.clone();
let raw_code = u8::from(attr_type);

if let Some(t) = get_deprecated_attr_type(raw_code) {
debug!("deprecated attribute type: {} - {}", raw_code, t);
attributes.push(Attribute {
value: AttributeValue::Deprecated(AttrRaw {
code: raw_code,
bytes: raw_bytes,
bytes: attr_data,
}),
flag,
});
Expand All @@ -506,7 +509,7 @@ pub fn parse_attributes(
attributes.push(Attribute {
value: AttributeValue::Unknown(AttrRaw {
code: raw_code,
bytes: raw_bytes,
bytes: attr_data,
}),
flag,
});
Expand All @@ -518,7 +521,7 @@ pub fn parse_attributes(
attributes.push(Attribute {
value: AttributeValue::Raw(AttrRaw {
code: raw_code,
bytes: raw_bytes,
bytes: attr_data,
}),
flag,
});
Expand Down Expand Up @@ -587,7 +590,7 @@ pub fn parse_attributes(
attributes.push(Attribute {
value: AttributeValue::Raw(AttrRaw {
code: raw_code,
bytes: raw_bytes,
bytes: section.slice(value_start..value_start + attr_length),
}),
flag,
});
Expand Down Expand Up @@ -1013,6 +1016,40 @@ mod tests {
);
}

#[test]
fn test_malformed_attribute_after_others_keeps_its_own_bytes() {
let data = Bytes::from(
[
// ORIGIN IGP
&[0x40, 0x01, 0x01, 0x00][..],
// NEXT_HOP with an extended length of 3 (must be 4)
&[0x50, 0x03, 0x00, 0x03, 0x0a, 0x0b, 0x0c],
// MED 7
&[0x80, 0x04, 0x04, 0x00, 0x00, 0x00, 0x07],
]
.concat(),
);
let attributes =
parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap();

assert_eq!(attributes.inner.len(), 3);
assert_eq!(
attributes.inner[0].value,
AttributeValue::Origin(Origin::IGP)
);
match &attributes.inner[1].value {
AttributeValue::Raw(raw) => {
assert_eq!(raw.code, 3);
assert_eq!(raw.bytes, Bytes::from_static(&[0x0a, 0x0b, 0x0c]));
}
value => panic!("expected Raw fallback, got {value:?}"),
}
assert_eq!(
attributes.inner[2].value,
AttributeValue::MultiExitDiscriminator(7)
);
}

#[test]
fn test_all_raw_retained_attribute_codes_parse_and_round_trip() {
let raw_codes = [0, 22, 24, 27, 33, 128];
Expand Down
56 changes: 18 additions & 38 deletions src/parser/iters/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Default iterator implementations that skip errors and return successfully parsed
*/
use crate::models::*;
use crate::parser::iters::{handle_record_parse_error, record_matches_filters};
use crate::parser::mrt::mrt_elem::PendingElems;
use crate::parser::BgpkitParser;
use crate::{Elementor, Filterable};
use std::io::Read;
Expand Down Expand Up @@ -62,7 +63,7 @@ BgpElem Iterator
**********/

pub struct ElemIterator<R> {
cache_elems: Vec<BgpElem>,
pending: PendingElems,
record_iter: RecordIterator<R>,
elementor: Elementor,
count: u64,
Expand All @@ -73,7 +74,7 @@ impl<R> ElemIterator<R> {
ElemIterator {
record_iter: RecordIterator::new(parser),
count: 0,
cache_elems: vec![],
pending: PendingElems::Empty,
elementor: Elementor::new(),
}
}
Expand All @@ -85,47 +86,26 @@ impl<R: Read> Iterator for ElemIterator<R> {
fn next(&mut self) -> Option<BgpElem> {
self.count += 1;

loop {
// Fast path: drain streaming text-dump elems directly, with filter support.
if let Some(iter) = &mut self.record_iter.parser.text_dump_iter {
for elem in iter.by_ref() {
if elem.match_filters(&self.record_iter.parser.filters) {
return Some(elem);
}
// Fast path: drain streaming text-dump elems directly, with filter support.
if let Some(iter) = &mut self.record_iter.parser.text_dump_iter {
for elem in iter.by_ref() {
if elem.match_filters(&self.record_iter.parser.filters) {
return Some(elem);
}
return None;
}
return None;
}

if self.cache_elems.is_empty() {
// refill cache elems
loop {
match self.record_iter.next() {
None => {
// no more records
return None;
}
Some(r) => {
let mut elems = self.elementor.record_to_elems(r);
if elems.is_empty() {
// somehow this record does not contain any elems, continue to parse next record
continue;
} else {
elems.reverse();
self.cache_elems = elems;
break;
}
}
}
loop {
// drain the current record's elems before reading the next record
while let Some(elem) = self.pending.next_elem(self.elementor.peer_table.as_ref()) {
if elem.match_filters(&self.record_iter.parser.filters) {
return Some(elem);
}
// when reaching here, the `self.cache_elems` has been refilled with some more elems
}

// popping cached elems. note that the original elems order is preseved by reversing the
// vector before putting it on to cache_elems.
let elem = self.cache_elems.pop()?;
if elem.match_filters(&self.record_iter.parser.filters) {
return Some(elem);
}
// records without elems leave nothing pending, and the loop moves on to the next
let record = self.record_iter.next()?;
self.pending = self.elementor.ingest(record);
}
}
}
Loading
Loading