Skip to content
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,37 @@ All notable changes to this project will be documented in this file.

### Breaking changes

* **Encoding is now fallible and sink-based**: all `encode()` methods now return `Result<Bytes, EncodingError>` instead of `Bytes`, and most types also expose an `encode_to(&self, buf: &mut BytesMut) -> Result<(), EncodingError>` sink method for zero-copy composition. Arbitrary input data (e.g. a round-tripped OPEN with an oversized raw capability, an AS_PATH segment with >255 ASes, or a RIB entry with >65535 bytes of attributes) previously crashed the process via `.expect()` or silently truncated length fields with `as u8`/`as u16` casts; it now surfaces as an `Err`. ([#313](https://github.com/bgpkit/bgpkit-parser/issues/313))

Migration guide:

| Before | After |
| --- | --- |
| `msg.encode()` → `Bytes` | `msg.encode()` → `Result<Bytes, EncodingError>` |
| `msg.encode(asn_len)` → `Bytes` | `msg.encode(asn_len)` → `Result<Bytes, EncodingError>` |
| `mrt_message.encode(sub_type)` → `Bytes` | `mrt_message.encode(sub_type)` → `Result<Bytes, EncodingError>` |
| `encoder.export_bytes()` → `Bytes` | `encoder.export_bytes()` → `Result<Bytes, EncodingError>` |
| `encoder.process_elem(&elem)` → `()` | `encoder.process_elem(&elem)` → `Result<(), EncodingError>` |
| `table.add_peer(peer)` → `u16` | `table.add_peer(peer)` → `Result<u16, EncodingError>` |

Callers that want the old semantics write `.unwrap()` — the panic is visibly theirs.

* **`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`.
* **`Tlv::length()` and `SubTlv::length()` removed**: these were saturating casts that duplicated what the encode paths now check properly. Use `value.len()` and the fallible encoders.
* **`PeerIndexTable::add_peer` returns `Result<u16, EncodingError>`**: errors on the 65537th distinct peer instead of silently aliasing it to id 65535 and corrupting the table.

### Added

* **`EncodingError` type**: `ValueTooLarge` for values exceeding their wire-format field capacity, and `Unencodable` for values that cannot be represented on the wire at all (e.g. `ATTR_SET` encoding not yet implemented, OPEN `param_type == 255` in non-extended framing, NLRI with an empty label stack). Exported as `bgpkit_parser::EncodingError` and `bgpkit_parser::error::EncodingError`. ([#313](https://github.com/bgpkit/bgpkit-parser/issues/313))
* **Sink-style encoding helpers**: internal `with_u8_len`/`with_u16_len` back-patching helpers centralize all length-prefix writes, replacing 24 hand-rolled `try_from(..).map_err(..)` copies. Buffers are rolled back to their pre-call state if encoding fails.

### Fixed

* **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.
* **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 (documented auto-upgrade behavior).
* **Encoding crash and silent truncation**: replaced `.expect()` panics and unchecked `as u8`/`as u16` casts throughout the encoding layer with checked conversions. ([#313](https://github.com/bgpkit/bgpkit-parser/issues/313))
* **MP_REACH/MP_UNREACH NLRI encoding failures** were silently replaced with empty attribute bytes; they now return `EncodingError::Unencodable`.
* **`ATTR_SET` encoding** returned `Ok` with empty bytes; it now returns `EncodingError::Unencodable` until the encoding is implemented.
* **FlowSpec NLRI length bound**: the check used `u16::MAX`, but the wire length field is 12-bit per RFC 8955/8956 — now bounded at 4095 (0x0FFF).

## v0.19.0 - 2026-07-28

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ bgpkit_parser::BgpkitParser::new(
});

let mut mrt_writer = oneio::get_writer("as3356_mrt.gz").unwrap();
mrt_writer.write_all(updates_encoder.export_bytes().as_ref()).unwrap();
mrt_writer.write_all(updates_encoder.export_bytes().unwrap().as_ref()).unwrap();
drop(mrt_writer);
```

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 @@ -48,7 +49,12 @@ impl MrtRibEncoder {
/// # Arguments
///
/// * `elem` - A reference to a BgpElem that contains the information to be processed.
pub fn process_elem(&mut self, elem: &BgpElem) {
///
/// # Errors
///
/// Returns [`EncodingError::ValueTooLarge`] if the peer index table is full
/// (more than 65536 distinct peers).
pub fn process_elem(&mut self, elem: &BgpElem) -> Result<(), EncodingError> {
if self.timestamp == 0.0 {
self.timestamp = elem.timestamp;
}
Expand All @@ -57,7 +63,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 +75,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 +85,8 @@ 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 `Result<Bytes, EncodingError>` containing the exported data as a byte array.
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
163 changes: 163 additions & 0 deletions src/encoder/sink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Sink-style encoding helpers.
//!
//! All "write a length prefix, then the payload" logic in the crate funnels
//! through these helpers. They write a placeholder length, encode the payload
//! into the same buffer, then back-patch the measured length — checking it
//! against the field's capacity. If the payload encoder fails, or the measured
//! length overflows, the buffer is rolled back to its pre-call state so a
//! failed encode never leaves dirty bytes behind.

use crate::error::EncodingError;
use bytes::{BufMut, BytesMut};

/// Check that `n` fits within `max`, returning [`EncodingError::ValueTooLarge`]
/// otherwise. Use for element counts and non-power-of-two byte bounds that are
/// written at a known position (not back-patched).
pub(crate) fn check_max(field: &'static str, n: usize, max: usize) -> Result<usize, EncodingError> {
if n > max {
Err(EncodingError::too_large(field, n, max))
} else {
Ok(n)
}
}

/// Encode `f`'s payload with a 1-octet length prefix, back-patched after
/// encoding. Errors if the payload exceeds 255 bytes or `f` fails; in both
/// cases `buf` is rolled back to its pre-call length.
pub(crate) fn with_u8_len(
buf: &mut BytesMut,
field: &'static str,
f: impl FnOnce(&mut BytesMut) -> Result<(), EncodingError>,
) -> Result<(), EncodingError> {
let at = buf.len();
buf.put_u8(0); // placeholder
if let Err(e) = f(buf) {
buf.truncate(at);
return Err(e);
}
let len = buf.len() - at - 1;
let Ok(len) = u8::try_from(len) else {
let actual = buf.len() - at - 1;
buf.truncate(at);
return Err(EncodingError::too_large(field, actual, u8::MAX as usize));
};
buf[at] = len;
Ok(())
}

/// Encode `f`'s payload with a 2-octet length prefix, back-patched after
/// encoding. Errors if the payload exceeds 65535 bytes or `f` fails; in both
/// cases `buf` is rolled back to its pre-call length.
pub(crate) fn with_u16_len(
buf: &mut BytesMut,
field: &'static str,
f: impl FnOnce(&mut BytesMut) -> Result<(), EncodingError>,
) -> Result<(), EncodingError> {
let at = buf.len();
buf.put_u16(0); // placeholder
if let Err(e) = f(buf) {
buf.truncate(at);
return Err(e);
}
let len = buf.len() - at - 2;
let Ok(len) = u16::try_from(len) else {
let actual = buf.len() - at - 2;
buf.truncate(at);
return Err(EncodingError::too_large(field, actual, u16::MAX as usize));
};
buf[at..at + 2].copy_from_slice(&len.to_be_bytes());
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;

#[test]
fn test_with_u8_len_roundtrip() {
let mut buf = BytesMut::new();
with_u8_len(&mut buf, "test", |b| {
b.extend_from_slice(&[1, 2, 3]);
Ok(())
})
.unwrap();
assert_eq!(buf.freeze(), Bytes::from_static(&[3, 1, 2, 3]));
}

#[test]
fn test_with_u8_len_boundary() {
let mut buf = BytesMut::new();
// 255 bytes fits exactly
with_u8_len(&mut buf, "test", |b| {
b.extend_from_slice(&[0u8; 255]);
Ok(())
})
.unwrap();
assert_eq!(buf[0], 255);

// 256 bytes overflows and rolls back
let before = buf.len();
let err = with_u8_len(&mut buf, "test", |b| {
b.extend_from_slice(&[0u8; 256]);
Ok(())
})
.unwrap_err();
assert!(matches!(err, EncodingError::ValueTooLarge { .. }));
assert_eq!(buf.len(), before, "buffer must roll back on overflow");
}

#[test]
fn test_with_u16_len_boundary() {
let mut buf = BytesMut::new();
with_u16_len(&mut buf, "test", |b| {
b.extend_from_slice(&[0u8; 65535]);
Ok(())
})
.unwrap();
assert_eq!(&buf[..2], &[0xFF, 0xFF]);

let before = buf.len();
let err = with_u16_len(&mut buf, "test", |b| {
b.extend_from_slice(&[0u8; 65536]);
Ok(())
})
.unwrap_err();
assert!(matches!(err, EncodingError::ValueTooLarge { .. }));
assert_eq!(buf.len(), before);
}

#[test]
fn test_child_error_rolls_back() {
let mut buf = BytesMut::from(&b"prefix"[..]);
let err = with_u16_len(&mut buf, "test", |b| {
b.extend_from_slice(&[1, 2, 3]);
Err(EncodingError::unencodable("test", "boom"))
})
.unwrap_err();
assert!(matches!(err, EncodingError::Unencodable { .. }));
assert_eq!(&buf[..], b"prefix", "buffer must roll back on child error");
}

#[test]
fn test_nested_prefixes() {
let mut buf = BytesMut::new();
with_u8_len(&mut buf, "outer", |b| {
b.put_u8(0xAA);
with_u8_len(b, "inner", |b2| {
b2.extend_from_slice(&[1, 2]);
Ok(())
})
})
.unwrap();
assert_eq!(buf.freeze(), Bytes::from_static(&[4, 0xAA, 2, 1, 2]));
}

#[test]
fn test_check_max() {
assert_eq!(check_max("count", 10, 255).unwrap(), 10);
assert_eq!(check_max("count", 255, 255).unwrap(), 255);
assert!(check_max("count", 256, 255).is_err());
assert!(check_max("FlowSpec length", 4096, 0x0FFF).is_err());
}
}
Loading
Loading