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

## Unreleased

### Breaking changes

* **MSRV raised to Rust 1.88** ([#338](https://github.com/bgpkit/bgpkit-parser/pull/338)): `rust-version` in `Cargo.toml` is now 1.88.0. The `idna_adapter` dev-dependency pin is removed as it is no longer needed.

### Dependencies

* **Updated `oneio` dependency from 0.25 to 0.26** ([#338](https://github.com/bgpkit/bgpkit-parser/pull/338)): clears the suppaftp advisory ([RUSTSEC-2026-0271](https://rustsec.org/advisories/RUSTSEC-2026-0271.html)) that `cargo audit` reported via oneio 0.25's lockfile entry.

### Added

* **Layered text output format** (`render::text::format_record`, `--format text`): one human-readable, indented block per MRT record — session context (`TIME`/`TYPE`/`FROM`/`TO`), `UPDATE:` sections with withdrawn/announced prefixes (including those carried in MP_REACH/MP_UNREACH) and every path attribute, `OPEN:` capabilities, session states, RIB entries, the peer table, and full legacy type-5 records. RFC 7606 validation findings render under `WARNINGS:` when present. The format is designed around this crate's own models and `Display` vocabulary — inspired by bgpdump's human-readable output, not byte-compatible with it. Rendering is a pure function of the record. In the CLI, `--format text` always uses record-level output (implies `--level records`); the other formats follow `--level` and default to elems.
* **`--hex` flag**: include each record's original wire bytes as hex in record-level output — a `HEX:` line at the end of `--format text` blocks, a `hex` field in `--format json`/`json-pretty` records. Designed as the pipe from filtered CLI output into byte-level tools (e.g. wirescope's hex paste input): `bgpkit-parser -p 8.8.8.0/24 --format text --hex updates.mrt.gz` yields blocks whose `HEX:` line can be pasted straight into a dissector. Bytes come from a new library iterator, `BgpkitParser::into_filtered_raw_record_iter()` — raw MRT records with the same record-level filter semantics as `into_record_iter`, yielding the untouched original bytes (no re-encoding, so attribute-ordering quirks such as BGP-LS hash-map iteration can never alter the output). The flag implies `--level records`, requires text/json/json-pretty formats, is mutually exclusive with `--recover`, and is ignored by count-only runs (`-e`/`-r`), which keep the normal counting pipelines and their per-elem filter semantics.
* Record-level filter semantics are now explicit: records that produce no elems (KEEPALIVE, OPEN, NOTIFICATION, state changes) never match elem-oriented filters and are dropped from record iteration while filters are active — a `debug!` line notes each drop.

### Fixed

* **RIS Live peer-state message type** ([#338](https://github.com/bgpkit/bgpkit-parser/pull/338)): the live stream sends peer-state messages with `"type": "STATE"`, not `"type": "RIS_PEER_STATE"` as documented in the RIPE RIS Live documentation and schema. `RisMessageEnum::RIS_PEER_STATE` now deserializes from both `STATE` and `RIS_PEER_STATE` and serializes as `STATE`, matching the live stream. To be re-validated against RIPE's documentation and schema on/after January 2027.

## v0.21.0 - 2026-08-21

### Examples restructure
Expand Down
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords = ["bgp", "bgpkit", "mrt"]
categories = ["network-programming"]
default-run = "bgpkit-parser"
readme = "README.md"
rust-version = "1.87.0"
rust-version = "1.88.0"
exclude = [
"tests/fixtures/packetlife/**",
"tests/fixtures/ripe/**",
Expand Down Expand Up @@ -56,7 +56,7 @@ serde = { version = "1.0", features = ["derive"], optional = true }
bytes = { version = "1.7" }
zerocopy = { version = "0.8", features = ["derive"], optional = true }
hex = { version = "0.4.3", optional = true } # bmp/openbmp parsing
oneio = { version = "0.25", default-features = false, features = ["http", "gz", "bz", "zstd"], optional = true }
oneio = { version = "0.26", default-features = false, features = ["http", "gz", "bz", "zstd"], optional = true }
regex = { version = "1", optional = true } # used in parser filter
chrono = { version = "0.4.38", optional = true } # parser filter
serde_json = { version = "1.0", optional = true } # RIS Live parsing
Expand Down Expand Up @@ -141,8 +141,6 @@ name = "bench_main"
harness = false

[dev-dependencies]
# MSRV pin - newer idna_adapter requires rust 1.88
idna_adapter = "=1.2.0"
regex = "1"
anyhow = "1"
bgpkit-broker = "0.12"
Expand Down
3 changes: 3 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "1.98"
components = ["rustfmt", "clippy"]
4 changes: 2 additions & 2 deletions src/models/bgp/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,8 +724,8 @@ mod tests {

#[test]
fn test_reserved_for_experimental() {
let experimental_ranges = [239..=254];
for code in <[_; 1]>::into_iter(experimental_ranges).flatten() {
let experimental_range = 239..=254;
for code in experimental_range {
let ty = BgpCapabilityType::from(code);
assert_eq!(ty, BgpCapabilityType::Unknown(code));
assert!(ty.is_reserved_for_experimental_use());
Expand Down
4 changes: 4 additions & 0 deletions src/parser/rislive/messages/client/ris_subscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub enum RisSubscribeType {
OPEN,
NOTIFICATION,
KEEPALIVE,
/// Subscribe requests use `RIS_PEER_STATE` because that is what the server
/// accepts.
///
/// The resulting messages will have `"type":"STATE"` (see [`RisMessageEnum::RIS_PEER_STATE`](crate::rislive::messages::RisMessageEnum)).
RIS_PEER_STATE,
}

Expand Down
33 changes: 33 additions & 0 deletions src/parser/rislive/messages/server/ris_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ pub enum RisMessageEnum {
/// RIS Peer State message
///
/// Schema: <https://ris-live.ripe.net/schemas/v1/ris_message-RIS_PEER_STATE.schema.json>
///
/// The live stream sends this message with `"type":"STATE"`, not `RIS_PEER_STATE` as documented
/// in the documentation and schema.
///
/// Serialise to `STATE` and accept `RIS_PEER_STATE` when deserialising.
///
/// TODO: re-validate the documentation state on/after January 2027
#[serde(rename = "STATE", alias = "RIS_PEER_STATE")]
RIS_PEER_STATE { state: String },
}

Expand Down Expand Up @@ -153,6 +161,31 @@ mod tests {
let _msg: RisMessage = serde_json::from_str(msg_str).unwrap();
}

#[test]
fn test_peer_state_change_msg_state_alias() {
// The live stream sends "type":"STATE" instead of "RIS_PEER_STATE"
let fixtures = [
r#"{"type":"ris_message","data":{"timestamp":1788948301.310,"peer":"2001:7f8:13::a503:4927:1","peer_asn":"34927","id":"2001:7f8:13::a503:4927:1-01a085a0c5fe0002","host":"rrc03.ripe.net","type":"STATE","state":"connected"}}"#,
r#"{"type":"ris_message","data":{"timestamp":1788948302.310,"peer":"2001:7f8:13::a503:4927:1","peer_asn":"34927","id":"2001:7f8:13::a503:4927:1-01a085a0c9e60009","host":"rrc03.ripe.net","type":"STATE","state":"down"}}"#,
];
for (fixture, expected_state) in fixtures.iter().zip(["connected", "down"]) {
let msg: RisLiveMessage = serde_json::from_str(fixture).unwrap();
let RisLiveMessage::RisMessage(msg) = msg else {
panic!("incorrect message type");
};
match &msg.msg {
Some(RisMessageEnum::RIS_PEER_STATE { state }) => {
assert_eq!(state, expected_state);
}
other => panic!("incorrect message type: {:?}", other),
}
// serializes back as "STATE", not "RIS_PEER_STATE"
let serialized = serde_json::to_string(&msg).unwrap();
assert!(serialized.contains(r#""type":"STATE""#));
assert!(!serialized.contains("RIS_PEER_STATE"));
}
}

#[test]
fn test_withdrawals() {
let msg_str = r#"{ "type": "ris_message", "data": { "timestamp": 1740561857.910, "peer": "2606:6dc0:1301::1", "peer_asn": "13781", "id": "2606:6dc0:1301::1-019541923d760008", "host": "rrc25.ripe.net", "type": "UPDATE", "path": [], "community": [], "announcements": [], "withdrawals": [ "2605:de00:bb:0:0:0:0:0/48" ] } }"#;
Expand Down
Loading