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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,35 @@ All notable changes to this project will be documented in this file.

### Breaking changes

* **Fallible encoding** ([#313](https://github.com/bgpkit/bgpkit-parser/issues/313)): all encode paths now return `Result<_, EncodingError>` instead of silently truncating values that do not fit their wire-format fields. There are no panicking wrappers; callers that want the old "just give me bytes" ergonomics can `.unwrap()`. Migration table:

| Before | After |
| --- | --- |
| `Attribute::encode(asn_len) -> Bytes` | `-> Result<Bytes, EncodingError>` (also `encode_to(asn_len, &mut BytesMut)`) |
| `Attributes::encode(asn_len) -> Bytes` | `-> Result<Bytes, EncodingError>` (also `encode_to`) |
| `BgpOpenMessage::encode() -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `BgpUpdateMessage::encode(asn_len) -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `BgpMessage::encode(asn_len) -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `TableDumpMessage::encode() -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `RibEntry::encode()` / `RibAfiEntries::encode() -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `PeerIndexTable::encode()` / `GeoPeerTable::encode() -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `PeerIndexTable::add_peer(peer) -> u16` | `-> Result<u16, EncodingError>` (rejects the 65,536th peer instead of wrapping the id) |
| `MrtMessage::encode(sub_type)` / `Bgp4MpMessage::encode(asn_len)` / `MrtRecord::encode() -> Bytes` | `-> Result<Bytes, EncodingError>` |
| `MrtRibEncoder::process_elem(&elem)` | `-> Result<(), EncodingError>` |
| `MrtRibEncoder::export_bytes()` / `MrtUpdatesEncoder::export_bytes() -> Bytes` | `-> Result<Bytes, EncodingError>` |

`EncodingError` has two variants: `ValueTooLarge { field, actual, max }` for wire-capacity overflows and `Unencodable { field, reason }` for values with no valid wire representation (ATTR_SET, RIB_GENERIC, OPEN parameter type 255, labeled NLRI with an empty label stack).
* **`Tlv::length()` and `SubTlv::length()` removed**: both silently saturated at `u16::MAX`; encoders now compute checked lengths internally.
* **`OptParam` no longer has a `param_len` field**: the field was redundant now that the encoder always derives the wire length from `param_value`, and the parser recomputes it on read. Construct `OptParam` with just `param_type` and `param_value`.

### Fixed

* **Silent truncation on encode eliminated** ([#313](https://github.com/bgpkit/bgpkit-parser/issues/313)): AS_PATH segments with >255 ASes, attribute values exceeding their (extended or non-extended) length field, TLV values in Tunnel Encapsulation / BGP-LS / SFP / BFD Discriminator / Prefix-SID / BIER attributes, RIB entry counts, peer counts, view names, and BGP message total lengths now return `EncodingError` instead of writing corrupt length fields.
* **FlowSpec NLRI length bound**: the wire length field is 12-bit (RFC 8955); NLRI data of 4096..=65535 bytes previously passed an unchecked `u16` cast and encoded a corrupt length byte. It is now rejected with `ValueTooLarge`.
* **MP_REACH/MP_UNREACH encoding errors are no longer swallowed**: labeled-NLRI failures previously logged a warning and emitted a zero-length (RFC-invalid) attribute value; they now propagate as `EncodingError`.
* **FlowSpec NLRI entries are now encoded**: `Nlri::flowspec_nlris` was previously silently dropped when encoding MP_REACH/MP_UNREACH attributes.
* **Peer index aliasing**: `PeerIndexTable::add_peer` previously wrapped the peer id at 65,536 peers, silently attributing routes to the wrong peer; it now returns an error and leaves the table unmodified.
* **BGP OPEN parameter type 255 rejected**: RFC 9072 reserves type 255 as the extended-length marker; encoding it as a real parameter produced output that round-tripped to a structurally different message.
* **BGP OPEN optional-parameter encoding**: Encode the Optional Parameters Length as the total byte length required by RFC 4271 instead of the number of parameters. OPEN messages now also use the extended length format from RFC 9072 when requested or required.

## v0.19.0 - 2026-07-28
Expand Down
6 changes: 4 additions & 2 deletions examples/filter_export_rib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ fn main() {

info!("processing rib {}", RIB_URL);
for elem in parser {
encoder.process_elem(&elem);
encoder.process_elem(&elem).unwrap();
}

info!("exporting filtered RIB...");
let mut writer = oneio::get_writer("filtered-13335.rib.gz").unwrap();
writer.write_all(encoder.export_bytes().as_ref()).unwrap();
writer
.write_all(encoder.export_bytes().unwrap().as_ref())
.unwrap();
drop(writer);

info!("exporting filtered RIB...done");
Expand Down
2 changes: 1 addition & 1 deletion examples/mrt_filter_archiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn main() {
.unwrap()
.into_record_iter()
.for_each(|record| {
let bytes = record.encode();
let bytes = record.encode().unwrap();
mrt_writer.write_all(&bytes).unwrap();
records_count += 1;
let mut elementor = Elementor::new();
Expand Down
2 changes: 1 addition & 1 deletion examples/parse_bmp_mpls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn create_bmp_mpls_message() -> Vec<u8> {
let bgp_msg = BgpMessage::Update(bgp_update);

// Encode the BGP message
let bgp_bytes = bgp_msg.encode(AsnLength::Bits32);
let bgp_bytes = bgp_msg.encode(AsnLength::Bits32).unwrap();

// Now construct the BMP message
// BMP Common Header (6 bytes) + Per-Peer Header (42 bytes) + BGP message
Expand Down
2 changes: 1 addition & 1 deletion examples/raw_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn main() {
);

// Encode and decode round-trip
let encoded = attributes.encode(AsnLength::Bits32);
let encoded = attributes.encode(AsnLength::Bits32).unwrap();
println!("\nEncoded size: {} bytes", encoded.len());

// Show raw access to the undecoded bytes
Expand Down
2 changes: 1 addition & 1 deletion examples/real_time_routeviews_kafka_to_mrt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn consume_and_archive(
}
};

let bytes = mrt_record.encode();
let bytes = mrt_record.encode().unwrap();
archive_writer.write_all(&bytes).unwrap();
archive_writer.flush().unwrap();
records_count += 1;
Expand Down
1 change: 1 addition & 0 deletions src/encoder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod rib_encoder;
pub(crate) mod sink;
mod updates_encoder;

pub use rib_encoder::MrtRibEncoder;
Expand Down
35 changes: 21 additions & 14 deletions src/encoder/rib_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! difficulty part of this process is the handling of TableDumpV2 RIB dumps, which requires
//! reconstructing the peer index table before encoding all other contents.

use crate::error::EncodingError;
use crate::models::{
Attributes, BgpElem, CommonHeader, EntryType, MrtMessage, NetworkPrefix, Peer, PeerIndexTable,
RibAfiEntries, RibEntry, TableDumpV2Message, TableDumpV2Type,
Expand Down Expand Up @@ -45,10 +46,14 @@ impl MrtRibEncoder {

/// Processes a BgpElem and updates the internal data structures.
///
/// Returns [`EncodingError::ValueTooLarge`] if the element's peer would
/// exceed the 65535-peer capacity of the PEER_INDEX_TABLE; the encoder's
/// state is left unchanged in that case.
///
/// # Arguments
///
/// * `elem` - A reference to a BgpElem that contains the information to be processed.
pub fn process_elem(&mut self, elem: &BgpElem) {
pub fn process_elem(&mut self, elem: &BgpElem) -> Result<(), EncodingError> {
if self.timestamp == 0.0 {
self.timestamp = elem.timestamp;
}
Expand All @@ -57,7 +62,7 @@ impl MrtRibEncoder {
IpAddr::V6(_ip) => Ipv4Addr::from(0),
};
let peer = Peer::new(bgp_identifier, elem.peer_ip, elem.peer_asn);
let peer_index = self.index_table.add_peer(peer);
let peer_index = self.index_table.add_peer(peer)?;
let path_id = elem.prefix.path_id;
let prefix = elem.prefix.prefix;

Expand All @@ -69,6 +74,7 @@ impl MrtRibEncoder {
attributes: Attributes::from(elem),
};
entries_map.insert(peer_index, entry);
Ok(())
}

/// Export the data stored in the struct to a byte array.
Expand All @@ -78,8 +84,9 @@ impl MrtRibEncoder {
/// The resulting `BytesMut` object is then converted to an immutable `Bytes` object using `freeze()` and returned.
///
/// # Return
/// Returns a `Bytes` object containing the exported data as a byte array.
pub fn export_bytes(&mut self) -> Bytes {
/// Returns a `Bytes` object containing the exported data as a byte array,
/// or an [EncodingError] if any element cannot be represented in wire format.
pub fn export_bytes(&mut self) -> Result<Bytes, EncodingError> {
let mut bytes = BytesMut::new();

// encode peer-index-table
Expand All @@ -88,7 +95,7 @@ impl MrtRibEncoder {
));
let (seconds, _microseconds) = convert_timestamp(self.timestamp);
let subtype = TableDumpV2Type::PeerIndexTable as u16;
let data_bytes = mrt_message.encode(subtype);
let data_bytes = mrt_message.encode(subtype)?;
let header = CommonHeader {
timestamp: seconds,
microsecond_timestamp: None,
Expand Down Expand Up @@ -120,7 +127,7 @@ impl MrtRibEncoder {

let (seconds, _microseconds) = convert_timestamp(self.timestamp);
let subtype = rib_type as u16;
let data_bytes = mrt_message.encode(subtype);
let data_bytes = mrt_message.encode(subtype)?;
let header_bytes = CommonHeader {
timestamp: seconds,
microsecond_timestamp: None,
Expand All @@ -135,7 +142,7 @@ impl MrtRibEncoder {

self.reset();

bytes.freeze()
Ok(bytes.freeze())
}
}

Expand All @@ -156,10 +163,10 @@ mod tests {
..Default::default()
};
elem.prefix.prefix = "10.250.0.0/24".parse().unwrap();
encoder.process_elem(&elem);
encoder.process_elem(&elem).unwrap();
elem.prefix.prefix = "10.251.0.0/24".parse().unwrap();
encoder.process_elem(&elem);
let bytes = encoder.export_bytes();
encoder.process_elem(&elem).unwrap();
let bytes = encoder.export_bytes().unwrap();

let mut cursor = Cursor::new(bytes.clone());
while cursor.has_remaining() {
Expand All @@ -175,8 +182,8 @@ mod tests {
};
// ipv6 prefix
elem.prefix.prefix = "2001:db8::/32".parse().unwrap();
encoder.process_elem(&elem);
let bytes = encoder.export_bytes();
encoder.process_elem(&elem).unwrap();
let bytes = encoder.export_bytes().unwrap();

let mut cursor = Cursor::new(bytes.clone());
while cursor.has_remaining() {
Expand All @@ -193,9 +200,9 @@ mod tests {
..Default::default()
};
elem.prefix = NetworkPrefix::new("10.250.0.0/24".parse().unwrap(), Some(42));
encoder.process_elem(&elem);
encoder.process_elem(&elem).unwrap();

let bytes = encoder.export_bytes();
let bytes = encoder.export_bytes().unwrap();
let mut cursor = Cursor::new(bytes);
let _peer_table = parse_mrt_record(&mut cursor).unwrap();
let parsed = parse_mrt_record(&mut cursor).unwrap();
Expand Down
Loading
Loading