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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## Unreleased

### 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.
* 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.

## v0.21.0 - 2026-08-21

### Examples restructure
Expand Down
24 changes: 17 additions & 7 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@ use clap::{Parser, ValueEnum};
use ipnet::IpNet;

/// Output format for the parser
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
enum OutputFormat {
/// Default pipe-separated format
/// Default pipe-separated format (elem-level)
#[default]
Default,
/// JSON format (one object per line)
/// JSON format (one object per line, elem-level)
Json,
/// Pretty-printed JSON format
/// Pretty-printed JSON format (elem-level)
JsonPretty,
/// PSV format with header
/// PSV format with header (elem-level)
Psv,
/// Layered human-readable text, one block per MRT record. Always uses
/// record-level output (implies `--level records`); the other formats
/// follow `--level` and default to elems. Inspired by bgpdump's
/// human-readable output.
Text,
}

/// Output level granularity
Expand Down Expand Up @@ -256,8 +261,9 @@ fn main() {
// Element-level runs (element output or counting only elements) use the elem
// iterators, which apply filters per element; everything else stays at the record
// level. Counting both (-e -r) iterates records and converts once per record.
let use_elem_stream = (opts.elems_count && !opts.records_count)
|| (!opts.elems_count && !opts.records_count && matches!(opts.level, OutputLevel::Elems));
let use_elem_stream = ((opts.elems_count && !opts.records_count)
|| (!opts.elems_count && !opts.records_count && matches!(opts.level, OutputLevel::Elems)))
&& output_format != OutputFormat::Text;

let result = match (opts.recover, use_elem_stream) {
(true, true) => run_elems(
Expand Down Expand Up @@ -468,6 +474,9 @@ fn format_elem(elem: &BgpElem, format: OutputFormat, index: usize) -> String {
}
}
OutputFormat::Default => elem.to_string(),
// The dispatch above routes text output to the record pipeline;
// elem-level text rendering does not exist.
OutputFormat::Text => unreachable!("text format renders records, not elems"),
}
}

Expand All @@ -481,6 +490,7 @@ fn format_record(record: &bgpkit_parser::MrtRecord, format: OutputFormat) -> Str
let val = json!(record);
serde_json::to_string_pretty(&val).unwrap()
}
OutputFormat::Text => bgpkit_parser::render::text::format_record(record),
OutputFormat::Psv | OutputFormat::Default => {
// Use the Display implementation for MrtRecord
format!("{}", record)
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ pub mod error;
pub mod models;
#[cfg(feature = "parser")]
pub mod parser;
pub mod render;
#[cfg(feature = "wasm")]
pub mod wasm;

Expand Down
17 changes: 13 additions & 4 deletions src/parser/iters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use crate::models::{MrtMessage, MrtRecord, TableDumpV2Message};
use crate::parser::BgpkitParser;
use crate::RawMrtRecord;
use crate::{Elementor, Filter, Filterable};
use log::debug;
use std::io::Read;
use std::path::Path;

Expand All @@ -60,10 +61,18 @@ pub(crate) fn record_matches_filters(
let _ = elementor.record_to_elems(record.clone());
return true;
}
elementor
.record_to_elems(record.clone())
.iter()
.any(|element| element.match_filters(filters))
// Filters match on the elem projection. Records that produce no elems
// (KEEPALIVE, OPEN, NOTIFICATION, state changes) can therefore never
// match and are dropped from record iteration while filters are active.
let elems = elementor.record_to_elems(record.clone());
if elems.is_empty() {
debug!(
"filters active: record of type {:?} yields no elems and is dropped",
record.common_header.entry_type
);
return false;
}
elems.iter().any(|element| element.match_filters(filters))
}

pub(crate) fn write_mrt_core_dump(enabled: bool, bytes: Option<Vec<u8>>) {
Expand Down
28 changes: 28 additions & 0 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*!
Human-readable record rendering.

The [`text`] module renders one [`MrtRecord`](crate::MrtRecord) as a layered, indented text
block — a full-fidelity transcript of the record: BGP4MP session context,
withdrawn and announced prefixes, every path attribute, and RFC 7606
validation warnings. The format is designed around this crate's own models
(reusing the leaf `Display` implementations), not around any external tool's
output; it is *inspired by bgpdump's human-readable output*.

Rendering is a pure function of the record: no iterators, no I/O, no
session state. RIB entries reference peers by their table index because the
peer table lives in a separate, earlier record.

# Example

```
use bgpkit_parser::render::text::format_record;
use bgpkit_parser::BgpkitParser;

let parser = BgpkitParser::new("tests/fixtures/ripe/rrc00/2000.01/updates.20000102.2014.gz").unwrap();
for record in parser.into_record_iter() {
println!("{}", format_record(&record));
}
```
*/

pub mod text;
Loading
Loading