| Crates.io | brec |
| lib.rs | brec |
| version | 0.2.0 |
| created_at | 2025-03-24 17:13:54.791512+00 |
| updated_at | 2025-06-28 17:30:51.347822+00 |
| description | A flexible binary format for storing and streaming structured data as packets with CRC protection and recoverability from corruption. Built for extensibility and robustness. |
| homepage | https://github.com/icsmw/brec |
| repository | https://github.com/icsmw/brec.git |
| max_upload_size | |
| id | 1604015 |
| size | 292,118 |
brec is a tool that allows you to quickly and easily create a custom message exchange protocol with resilience to data "corruption" and the ability to extract messages from mixed streams (i.e., streams containing not only brec packets but also any other data). brec is developed for designing your own custom binary protocol — without predefined message formats or rigid schemas.
Notice: Public Beta
The
brecis currently in a public beta phase. Its core functionality has demonstrated strong reliability under heavy stress testing, and the system is considered stable for most use cases.However, the public API is still evolving as we work to support a wider range of scenarios. We welcome your feedback and would be grateful if you share which features or improvements would make brec more valuable for your needs.
Thanks for being part of the journey!
brec doesn’t enforce a fixed message layout. Instead, you define your own building blocks (blocks) and arbitrary payloads (payloads), combining them freely into custom packets.brec includes a powerful streaming reader capable of extracting packets even from noisy or corrupted streams — skipping irrelevant or damaged data without breaking.brec provides a high-performance storage engine for persisting packets. Its slot-based layout enables fast indexed access, filtering, and direct access by packet index.The primary unit of information in brec is a packet (Packet) — a ready-to-transmit message with a unique signature (allowing it to be recognized within mixed data) and a CRC to ensure data integrity.
A packet consists of a set of blocks (Block) and, optionally, a payload (Payload).
Blocks (Block) are the minimal units of information in the brec system. A block can contain only primitives, such as numbers, boolean values, and byte slices. A block serves as a kind of packet index, allowing for quick determination of whether a packet requires full processing (i.e., parsing the Payload) or can be ignored.
The payload (Payload) is an optional part of the packet. Unlike blocks (Block), it has no restrictions on the type of data it can contain—it can be a struct or enum of any complexity and nesting level.
Unlike most protocols, brec does not require users to define a fixed set of messages but does require them to describe blocks (Block) and payload data (Payload).
Users can construct packets (messages) by combining various sets of blocks and payloads. This means brec does not impose a predefined list of packets (Packet) within the protocol but allows them to be defined dynamically. As a result, the same block and/or payload can be used across multiple packets (messages) without any restrictions.
brec includes powerful macros that allow defining the components of a protocol with minimal effort. For example, to define a structure as a block (Block), you simply need to use the block macro:
#[brec::block]
pub struct MyBlock {
pub field_u8: u8,
pub field_u16: u16,
pub field_u32: u32,
pub field_u64: u64,
pub field_u128: u128,
pub field_i8: i8,
pub field_i16: i16,
pub field_i32: i32,
pub field_i64: i64,
pub field_i128: i128,
pub field_f32: f32,
pub field_f64: f64,
pub field_bool: bool,
pub blob_a: [u8; 1],
pub blob_b: [u8; 100],
pub blob_c: [u8; 1000],
pub blob_d: [u8; 10000],
}
The block macro automatically generates all the necessary code for MyBlock to function as a block. Specifically, it adds:
CRC field to ensure data integrity.All user-defined blocks are ultimately included in a generated enumeration Block, as shown in the example:
#[brec::block]
pub struct MyBlockA {
pub field: u8,
pub blob: [u8; 100],
}
#[brec::block]
pub struct MyBlockB {
pub field_u16: u16,
pub field_u32: u32,
}
#[brec::block]
pub struct MyBlockC {
pub field: u64,
pub blob: [u8; 10000],
}
// Instruct `brec` to generate and include all protocol types
brec::generate!();
// Generated by `brec`
pub enum Block {
MyBlockA(MyBlockA),
MyBlockB(MyBlockB),
MyBlockC(MyBlockC),
}
The generated Block enumeration is always returned to the user as a result of message (packet) parsing, allowing for easy identification of blocks by their names.
Similarly to blocks, defining a payload can be done with a simple call to the payload macro.
#[derive(serde::Deserialize, serde::Serialize)]
pub enum MyNestedEntity {
One(String),
Two(Vec<u8>),
Three,
}
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayload {
pub field_u8: u8,
pub field_u16: u16,
pub field_u32: u32,
pub field_u64: u64,
pub field_u128: u128,
pub field_nested: MyNestedEntity,
}
A payload is an unrestricted set of data (unlike Block, which is limited to primitive data). It can be either a struct or an enum with unlimited nesting levels. Additionally, there are no restrictions on the use of generic types.
brec imposes only one requirement for payloads: they must implement the traits PayloadEncode and PayloadDecode<T>, which enable encoding and decoding of data into target types.
Out of the box (with the bincode feature), brec provides automatic support for the required traits by leveraging the bincode crate. This means that to define a fully functional payload, it is enough to use #[payload(bincode)], eliminating the need for manually implementing the PayloadEncode and PayloadDecode<T> traits.
Note that bincode requires serialization and deserialization support, which is why the previous examples include #[derive(serde::Deserialize, serde::Serialize)].
With brec, defining protocol types (blocks and payloads) is reduced to simply defining structures and annotating them with the block and payload macros.
Once the protocol data types have been defined, the next step is to include the "unifying" code generated by brec using brec::generate!();
#[brec::block]
pub struct MyBlockA { ... }
#[brec::block]
pub struct MyBlockB { ... }
#[brec::block]
pub struct MyBlockC { ... }
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadA { ... }
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadB { ... }
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadC { ... }
// Instruct `brec` to generate and include all protocol types
brec::generate!();
// Now available:
// Generalized block representation
pub enum Block {
MyBlockA(MyBlockA),
MyBlockB(MyBlockB),
MyBlockC(MyBlockC),
}
// Generalized payload representation
pub enum Payload {
MyPayloadA(MyPayloadA),
MyPayloadB(MyPayloadB),
MyPayloadC(MyPayloadC),
}
// Packet type
pub type Packet = brec::PacketDef<Block, Payload, Payload>;
Once all protocol types are defined and the unifying code is generated, you can start creating packets:
let my_packet = Packet::new(
// You are limited to 255 blocks per packet.
vec![
Block::MyBlockA(MyBlockA::default()),
Block::MyBlockC(MyBlockC::default()),
],
// Note: payload is optional
Some(Payload::MyPayloadA(MyPayloadA::default()))
);
At this point, your protocol is ready for use. Packet implements all the necessary methods for reading from and writing to a data source.
brec is a binary protocol, meaning data is always transmitted and stored in a binary format.
The protocol ensures security through the following mechanisms:
Packet itself has a fixed 64-bit signature.These features enable reliable entity recognition within a data stream. Furthermore, blocks, payloads, and the packet itself have their own CRCs. While blocks always use a 32-bit CRC, payloads allow for optional support of 64-bit or 128-bit CRC to enhance protocol security.
brec ensures maximum performance through the following optimizations:
brec allows CRC to be disabled for all types or selectively. This improves performance by eliminating the need for hash calculations.The conceptual separation of a packet into blocks and a payload allows users to efficiently manage traffic load. Blocks can carry "fast" information that requires quick access, while the payload can implement more complex encoding/decoding mechanisms (such as data compression). Filtering based on blocks helps avoid unnecessary operations on the payload when they are not required.
Any structure can be used as a block, provided it contains fields of the following types:
| Type |
|---|
| u8 |
| u16 |
| u32 |
| u64 |
| u128 |
| i8 |
| i16 |
| i32 |
| i64 |
| i128 |
| f32 |
| f64 |
| bool |
| [u8; n] |
A block can also include an enum, but only if it can be converted into one of the supported types. For example:
pub enum Level {
Err,
Warn,
Info,
Debug,
}
// Ensure conversion of `Level` to `u8`
impl From<&Level> for u8 {
fn from(value: &Level) -> Self {
match value {
Level::Err => 0,
Level::Warn => 1,
Level::Debug => 2,
Level::Info => 3,
}
}
}
// Ensure conversion of `u8` to `Level`
impl TryFrom<u8> for Level {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Level::Err),
1 => Ok(Level::Warn),
2 => Ok(Level::Debug),
3 => Ok(Level::Info),
invalid => Err(format!("{invalid} isn't a valid value for Level")),
}
}
}
#[block]
pub struct BlockWithEnum {
pub level: Level,
pub data: [u8; 200],
}
A structure is declared as a block using the block macro. If the block is located outside the visibility scope of the brec::generate!() code generator call, the module path must be specified using the path directive:
#[block(path = mod_a::mod_b)]
pub struct BlockWithEnum {
pub level: Level,
pub data: [u8; 200],
}
// Since the `path` directive is the default, the path can also be defined directly:
#[block(mod_a::mod_b)]
pub struct BlockWithEnum {
pub level: Level,
pub data: [u8; 200],
}
However, in most cases, this approach is not recommended. It is better to ensure the visibility of all blocks at the location where the brec::generate!() code generator is invoked.
The code generator provides support for the following traits:
| Trait | Available Methods | Return Type | Purpose |
|---|---|---|---|
ReadBlockFrom |
read<T: std::io::Read>(buf: &mut T, skip_sig: bool) |
Result<Self, Error> |
Attempts to read a block with an option to skip signature recognition (e.g., if the signature has already been read and the user knows exactly which block is being parsed). Returns an error if reading fails for any reason. |
ReadBlockFromSlice |
read_from_slice<'b>(src: &'b [u8], skip_sig: bool) |
Result<Self, Error> |
Attempts to read a block from a byte slice, with an option to skip signature verification. Unlike other methods, this returns a reference to a block representation generated by brec, rather than the user-defined block itself (see details below). |
TryReadFrom |
try_read<T: std::io::Read + std::io::Seek>(buf: &mut T) |
Result<ReadStatus<Self>, Error> |
Attempts to read a block, but instead of returning an error when data is insufficient, it returns a corresponding read status. Also, it moves the source's position only upon successful reading, otherwise keeping it unchanged. |
TryReadFromBuffered |
try_read<T: std::io::BufRead>(reader: &mut T) |
Result<ReadStatus<Self>, Error> |
Identical to TryReadFrom, except it returns a reference to the generated block representation (see details below). |
WriteTo |
write<T: std::io::Write>(&self, writer: &mut T) |
std::io::Result<usize> |
Equivalent to the standard write method, returning the number of bytes written to the output. Does not guarantee flushing to the output, so calling flush is required if such guarantees are needed. |
WriteTo |
write_all<T: std::io::Write>(&self, writer: &mut T) |
std::io::Result<()> |
Equivalent to the standard write_all method. |
WriteVectoredTo |
slices(&self) |
std::io::Result<brec::IoSlices> |
Returns the binary representation of the block as slices. |
WriteVectoredTo |
write_vectored<T: std::io::Write>(&self, buf: &mut T) |
std::io::Result<usize> |
Attempts a vectored write of the block (analogous to the standard write_vectored). |
WriteVectoredTo |
write_vectored_all<T: std::io::Write>(&self, buf: &mut T) |
std::io::Result<()> |
Attempts a vectored write of the block (analogous to the standard write_vectored_all). |
CrcU32 |
fn crc(&self) |
[u8; 4] |
Computes the CRC of the block. |
StaticSize |
fn ssize() |
u64 |
Returns the size of the block in bytes. |
It is evident that all the listed write methods transmit the binary representation of the block to the output.
As mentioned earlier, brec also generates a reference representation of a block:
#[block]
pub struct BlockWithEnum {
pub level: Level,
pub data: [u8; 200],
}
// Generated by `brec`
struct BlockWithEnumReferred<'a> {
pub level: Level,
pub data: &'a [u8; 200],
}
As seen above, the reference representation of a block does not store the slice itself but rather a reference to it. This allows the user to inspect the block while avoiding unnecessary data copying. If needed, the referenced block can be easily converted into the actual block using .into(), at which point the data will be copied from the source.
The block macro can be used with the following directives:
path = mod::mod – Specifies the module path for the block if it is not directly imported at the location of brec::generate!(). This approach is not recommended (it is better to ensure block visibility at the generator call site), but it is not inherently inefficient or unstable. However, using this method may make future code maintenance more difficult.no_crc – Disables CRC verification for the block. Note that this does not remove the CRC field from the binary representation of the block. The CRC field will still be present but filled with zeros, and no CRC calculation will be performed.brec does not impose any restrictions on the type of data that can be defined as a payload. However, a payload must implement the following traits:
| Trait | Method | Return Type | Description |
|---|---|---|---|
PayloadSize |
size(&self) |
std::io::Result<u64> |
Returns the size of the payload body in bytes (excluding the header). |
PayloadSignature |
sig(&self) |
ByteBlock |
Returns the signature, which can have a variable length of 4, 8, 16, 32, 64, or 128 bytes. |
StaticPayloadSignature |
ssig() |
ByteBlock |
Similar to sig, but can be called without creating a payload instance. |
PayloadEncode |
encode(&self) |
std::io::Result<Vec<u8>> |
Creates a binary representation of the payload (excluding the header). |
PayloadEncodeReferred |
encode(&self) |
std::io::Result<Option<&[u8]>> |
Creates a reference representation of the payload, if possible. Used for calculation of payload CRC and can boost performance |
PayloadDecode<T> |
decode(buf: &[u8]) |
std::io::Result<T> |
Attempts to reconstruct the payload from a byte slice. |
The following traits are automatically applied and do not require manual implementation, though they can be overridden if needed:
| Trait | Method | Return Type | Description |
|---|---|---|---|
PayloadCrc |
crc(&self) |
std::io::Result<ByteBlock> |
Returns the CRC of the payload, which can have a variable length of 4, 8, 16, 32, 64, or 128 bytes. |
ReadPayloadFrom |
read<B: std::io::Read>(buf: &mut B, header: &PayloadHeader) |
Result<T, Error> |
Reads the payload from a source. |
TryReadPayloadFrom |
try_read<B: std::io::Read + std::io::Seek>(buf: &mut B, header: &PayloadHeader) |
Result<ReadStatus<T>, Error> |
Attempts to read the payload from a source. If there is insufficient data, it returns a corresponding read status instead of an error. |
TryReadPayloadFromBuffered |
try_read<B: std::io::BufRead>(buf: &mut B, header: &PayloadHeader) |
Result<ReadStatus<T>, Error> |
Similar to TryReadPayloadFrom, but returns a reference representation of the payload (if supported) instead of the actual payload. |
WritePayloadWithHeaderTo |
write<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<usize> |
Equivalent to the standard write method, returning the number of bytes written. Does not guarantee that data is flushed to the output, so calling flush is required if such guarantees are needed. |
WritePayloadWithHeaderTo |
write_all<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<()> |
Equivalent to the standard write_all method. |
WriteVectoredPayloadWithHeaderTo |
write_vectored<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<usize> |
Attempts a vectored write of the payload (analogous to the standard write_vectored). |
WriteVectoredPayloadWithHeaderTo |
slices(&mut self) |
std::io::Result<IoSlices> |
Returns the binary representation of the payload as slices. |
WriteVectoredPayloadWithHeaderTo |
write_vectored_all<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<()> |
Attempts a vectored write of the payload (analogous to the standard write_vectored_all). |
PayloadHeader)The payload header is not a generated structure but a static one included in the brec crate. PayloadHeader is used when writing a payload into a packet (Packet) and is always written before the payload body itself. When manually writing a payload to a data source, it is strongly recommended to prepend it with PayloadHeader to facilitate further reading from the source.
PayloadHeader implements the following traits:
| Trait | Method | Return Type | Description |
|---|---|---|---|
ReadFrom |
read<T: std::io::Read>(buf: &mut T) |
Result<Self, Error> |
Attempts to read PayloadHeader from a source. |
TryReadFrom |
try_read<T: std::io::Read + std::io::Seek>(buf: &mut T) |
Result<ReadStatus<Self>, Error> |
Attempts to read PayloadHeader, but if data is insufficient, returns a corresponding read status instead of an error. Also, moves the source's position only on successful reading; otherwise, it remains unchanged. |
TryReadFromBuffered |
try_read<T: std::io::BufRead>(reader: &mut T) |
Result<ReadStatus<Self>, Error> |
Identical to TryReadFrom. |
Thus, if you are manually implementing payload reading from a source, you should first read PayloadHeader, and then, using the obtained header, proceed to read the payload itself.
PayloadHeader does not implement any traits for writing to a source. However, it provides the as_vec() method, which returns its binary representation.
Out of the box, brec supports String and Vec<u8> as payload types. After code generation, these will be included in the corresponding enumeration:
brec::generate!();
pub enum Payload {
// ..
// User-defined payloads
// ..
// Default payloads
Bytes(Vec<u8>),
String(String),
}
bincodeEnabling the bincode feature provides the simplest and most flexible way to define payload types. By specifying #[payload(bincode)], any type that supports serde serialization and deserialization can be used as a payload.
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadStruct {
// User-defined fields
}
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub enum MyPayloadEnum {
// User-defined variants
}
It is important to note that the CRC for a payload is generated twice—once when the payload is converted into bytes and again after extraction (to compare with the CRC stored in the payload header). This imposes certain limitations on CRC verification, as brec does not restrict the types of data used in a payload. If a payload contains data types that do not guarantee a strict byte sequence, CRC verification will always fail due to variations in byte order. As a result, extracting such a payload from the stream will become impossible.
A simple example of this issue is HashMap, which does not guarantee a consistent field order upon reconstruction. For instance:
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadB {
items: HashMap<String, String>,
}
Extracting such a payload will be impossible because the CRC will always be different (except when the number of keys in the map is ≤ 1). This issue can be resolved in several ways:
no_crc directive: #[payload(no_crc)].PayloadCrc trait manually for the specific payload. This can be done using the no_auto_crc directive: #[payload(bincode, no_auto_crc)].The payload macro can be used with the following directives:
path = mod::mod – Specifies the module path for the payload if it is not directly imported at the location of brec::generate!(). This approach is not recommended (it is better to ensure payload visibility at the generator call site), but it is not inherently inefficient or unstable. However, using this method may make future code maintenance more difficult.no_crc – Disables CRC verification for the payload. Note that this does not remove the CRC field from the binary representation of the payload (specifically in PayloadHeader). The CRC field will still be present but filled with zeros, and no CRC calculation will be performed.no_auto_crc – Disables CRC verification for payload(bincode), requiring a manual implementation of the PayloadCrc trait. This parameter is only relevant when using the bincode feature.bincode – available only when the bincode feature is enabled. It allows using any structure as a payload as long as it meets the requirements of the bincode crate, i.e., it implements serde serialization and deserialization. Please note that bincode has a number of limitations, which you can review in its official documentation.Users do not need to define possible packet types since any combination of blocks (up to 255) and a single optional payload constitutes a valid packet.
brec::generate!();
let my_packet = Packet::new(
// You are limited to 255 blocks per packet.
vec![
Block::MyBlockA(MyBlockA::default()),
Block::MyBlockC(MyBlockC::default())
],
// Note: payload is optional
Some(Payload::MyPayloadA(MyPayloadA::default()))
);
Warning! In most cases, having 1-5 blocks per packet is more than sufficient. A significant number of blocks can lead to an increase in compilation time but will not affect the performance of the compiled code. Therefore, if compilation time is a critical factor, it is recommended to avoid a large number of blocks in packets.
To clarify, runtime performance is not affected, but the compilation time increases because the compiler has to generate multiple implementations for generic types used in PacketDef (an internal brec structure).
A Packet can be used as a standalone unit for data exchange. It implements the following traits:
| Trait | Method | Return Type | Description |
|---|---|---|---|
ReadFrom |
read<T: std::io::Read>(buf: &mut T) |
Result<Self, Error> |
Attempts to read a packet from a source. |
TryReadFrom |
try_read<T: std::io::Read + std::io::Seek>(buf: &mut T) |
Result<ReadStatus<Self>, Error> |
Attempts to read a packet, but if data is insufficient, it returns a corresponding read status instead of an error. Also, moves the source’s position only upon successful reading; otherwise, it remains unchanged. |
TryReadFromBuffered |
try_read<T: std::io::BufRead>(reader: &mut T) |
Result<ReadStatus<Self>, Error> |
Identical to TryReadFrom. |
WriteMutTo |
write<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<usize> |
Equivalent to the standard write method, returning the number of bytes written. Does not guarantee that data is flushed to the output, so calling flush is required if such guarantees are needed. |
WriteMutTo |
write_all<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<()> |
Equivalent to the standard write_all method. |
WriteVectoredMutTo |
slices(&mut self) |
std::io::Result<IoSlices> |
Returns the binary representation of the packet as slices. |
WriteVectoredMutTo |
write_vectored<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<usize> |
Attempts a vectored write of the packet (analogous to the standard write_vectored). |
WriteVectoredMutTo |
write_vectored_all<T: std::io::Write>(&mut self, buf: &mut T) |
std::io::Result<()> |
Attempts a vectored write of the packet (analogous to the standard write_vectored_all). |
Packet provides a highly useful method:
filtered<R: std::io::Read + std::io::Seek>(
reader: &mut R,
rules: &Rules
) -> Result<LookInStatus<Packet>, Error>
This method allows you to "peek" into a packet before processing the payload, which can significantly improve performance when filtering specific packets.
brec generates code in two stages:
When the #[block] macro is used, it generates code specific to the corresponding block.
The same applies to the #[payload] macro, which generates code related to a specific payload type.
However, the protocol also requires unified types such as enum Block (enumerating all user-defined blocks)
and enum Payload (enumerating all payloads). These general-purpose enums allow the system to handle
various blocks and payloads dynamically.
To generate this unified code, the generate!() macro must be invoked:
pub use blocks::*;
pub use payloads::*;
brec::generate!();
This macro must be called exactly once per crate and is responsible for:
brec traits for all user-defined Block typesbrec traits for all user-defined Payload typesenum Block { ... }enum Payload { ... }The macro defines the following aliases to reduce verbosity when using brec types:
| Alias | Expanded to |
|---|---|
Packet |
PacketDef<Block, Payload, Payload> |
PacketBufReader<'a, R> |
PacketBufReaderDef<'a, R, Block, BlockReferred<'a>, Payload, Payload> |
Rules<'a> |
RulesDef<Block, BlockReferred<'a>, Payload, Payload> |
Rule<'a> |
RuleDef<Block, BlockReferred<'a>, Payload, Payload> |
RuleFnDef<D, S> |
RuleFnDef<D, S> |
Storage<S> |
StorageDef<S, Block, BlockReferred<'static>, Payload, Payload> |
These aliases make it easier to work with generated structures and remove the need to repeat generic parameters.
To enable this macro, you must include a build.rs file with the following content:
brec::build_setup();
This step ensures the code generator runs during build and provides all required metadata.
Block, Payload) in scope. You must ensure they are visible in the location where you call the macro.Ensure that all blocks and payloads are imported at the location where the macro is used:
pub use blocks::*;
pub use payloads::*;
brec::generate!();
The macro can be used with the following parameters:
no_default_payload – Disables the built-in payloads (String and Vec<u8>).
This has no impact on runtime performance but may slightly improve compile times and reduce binary size.
payloads_derive = "Trait" –
By default, brec automatically collects all derive attributes that are common across user-defined payloads
and applies them to the generated Payload enum.
This parameter allows you to manually specify additional derives for the Payload enum—useful if you are
only using the built-in payloads (String, Vec<u8>) and do not define custom ones.
For example,
pub use blocks::*;
// You don't define any custom payloads and only want to use the built-in ones (`String`, `Vec<u8>`)
brec::generate!(payloads_derive = "Debug, Clone");
pub use blocks::*;
// You don't define any payloads and explicitly disable the built-in ones
brec::generate!(no_default_payload);
If the user fully disables payload support (as in the example above), the macro will not generate any packet-related types (see Generated Aliases).
To read from a data source, brec includes the PacketBufReader<R: std::io::Read> tool (available after code generation by calling brec::generate!()). PacketBufReader ensures safe reading from both pure brec message streams and mixed data streams (containing both brec messages and arbitrary data).
Below is an example of reading all brec messages from a stream while counting the number of "junk" bytes (i.e., data that is not a brec message):
fn reading<R: std::io::Read>(source: &mut R) -> std::io::Result<(Vec<Packet>, usize)> {
let mut packets: Vec<Packet> = Vec::new();
let mut reader: PacketBufReader<_> = PacketBufReader::new(source);
let ignored: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let ignored_inner = ignored.clone();
reader
.add_rule(Rule::Ignored(brec::RuleFnDef::Dynamic(Box::new(
move |bytes: &[u8]| {
ignored_inner.fetch_add(bytes.len(), Ordering::SeqCst);
},
))))
.unwrap();
loop {
match reader.read() {
Ok(next) => match next {
NextPacket::Found(packet) => packets.push(packet),
NextPacket::NotFound => {
// Data will be refilled on the next call
}
NextPacket::NotEnoughData(_needed) => {
// Data will be refilled on the next call
}
NextPacket::NoData => {
break;
}
NextPacket::Skipped => {
//
}
},
Err(err) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
err.to_string(),
));
}
};
}
Ok((packets, ignored.load(Ordering::SeqCst)))
}
PacketBufReaderNextPacket::NotEnoughData), PacketBufReader will attempt to load more data on each subsequent call to read().brec data is found in the current read() iteration (NextPacket::NotFound), PacketBufReader will also attempt to load more data on each subsequent read().Thus, PacketBufReader automatically manages data loading, removing the need for users to implement their own data-fetching logic.
NextPacket Read Statuses| Status | Description | Can Continue Reading? |
|---|---|---|
NextPacket::Found |
A packet was successfully found and returned. | ✅ Yes |
NextPacket::NotFound |
No packets were found in the current read iteration. | ✅ Yes |
NextPacket::NotEnoughData |
A packet was detected, but there is not enough data to read it completely. | ✅ Yes |
NextPacket::Skipped |
A packet was detected but skipped due to filtering rules. | ✅ Yes |
NextPacket::NoData |
No more data can be retrieved from the source. | ❌ No |
After receiving NextPacket::NoData, further calls to read() are meaningless, as PacketBufReader has exhausted all available data from the source.
PacketBufReaderAnother key feature of PacketBufReader is that users can define custom rules to be applied during data reading. These rules can be updated dynamically between read() calls using add_rule and remove_rule.
| Rule | Available Data | Description |
|---|---|---|
Rule::Ignored |
&[u8] |
Triggered when data not related to brec messages is encountered. Provides a byte slice of the unrelated data. |
Rule::FilterByBlocks |
&[BlockReferred<'a>] |
Triggered when a packet is found and its blocks have been partially parsed. If blocks contain slices, no copying is performed — BlockReferred will hold references instead. At this stage, the user can decide whether to proceed with parsing the "heavy" part (i.e., the payload) or skip the packet. |
Rule::FilterByPayload |
&[u8] |
Allows "peeking" into the payload bytes before deserialization. This is especially useful if the payload is, for example, a string — enabling scenarios like substring search. |
Rule::Filter |
&Packet |
Triggered after the packet is fully parsed, giving the user a final chance to accept or reject the packet. |
The rules Rule::FilterByBlocks and Rule::FilterByPayload are particularly effective at improving performance, as they allow you to skip the most expensive part — parsing the payload — if the packet is not needed.
brec Message StorageIn addition to stream reading, brec provides a tool for storing packets and accessing them efficiently — Storage<S: std::io::Read + std::io::Write + std::io::Seek> (available after invoking brec::generate!()).
| Method | Description |
|---|---|
insert(&mut self, packet: Packet) |
Inserts a packet into the storage. |
add_rule(&mut self, rule: Rule) |
Adds a filtering rule. |
remove_rule(&mut self, rule: RuleDefId) |
Removes a filtering rule. |
count(&self) |
Returns the number of records currently stored. |
iter(&mut self) |
Returns an iterator over the storage. This method does not apply filters, even if previously added. |
filtered(&mut self) |
Returns an iterator with filters applied (if any were set via add_rule). The filtering rules used in Storage are identical to those used in PacketBufReader. |
nth(&mut self, nth: usize) |
Attempts to read the packet at the specified index. Note that this method does not apply any filtering, even if filters have been previously defined. |
range(&mut self, from: usize, len: usize) |
Returns an iterator over a given range of packets. |
range_filtered(&mut self, from: usize, len: usize) |
Returns an iterator over a range of packets with filters applied (if previously set via add_rule). |
Filtering by blocks or payload improves performance by allowing the system to avoid fully parsing packets unless necessary.
The core design of Storage is based on how it organizes packets internally:
Storage can quickly locate packets by index or return a packet range efficiently.As previously mentioned, each slot maintains its own CRC to ensure data integrity. However, even if the storage file becomes corrupted and Storage can no longer operate reliably, packets remain accessible in a manual recovery mode. For example, you can use PacketBufReader to scan the file, ignoring slot metadata and extracting intact packets sequentially.
A block supports fields of the following types:
| Type | Size in Binary Format (bytes) |
|---|---|
| u8 | 1 |
| u16 | 2 |
| u32 | 4 |
| u64 | 8 |
| u128 | 16 |
| i8 | 1 |
| i16 | 2 |
| i32 | 4 |
| i64 | 8 |
| i128 | 16 |
| f32 | 4 |
| f64 | 8 |
| bool | 1 |
| [u8; n] | n |
Any structure marked with the block macro will have the following extended representation in binary format:
| Field | Type |
|---|---|
| Signature | [u8; 4] |
| User-defined fields | Available types |
| CRC | [u8; 4] |
Thus, the total binary length of a block is calculated as:
length = 4 (Signature) + Block's Fields Length + 4 (CRC)
The block signature is generated automatically based on its name (including the module path) and the names of all its fields. The signature hash is computed using a 32-bit algorithm.
The block's CRC (32-bit) is generated based on the values of user-defined fields, excluding the signature and the CRC itself.
Any data type that implements the PayloadEncode and PayloadDecode<T> traits can be used as a payload. These traits handle the conversion of a payload to bytes and its unpacking from bytes, respectively. When serializing a payload into binary format, brec automatically adds a PayloadHeader to each payload.
PayloadHeader Structure| Field | Size | Description |
|---|---|---|
| Signature Length | 1 byte | Length of the signature: 4, 8, 16, 32, 64, or 128 bytes |
| Signature | 4 to 128 bytes | Unique signature of the payload |
| CRC Length | 1 byte | Length of the CRC: 4, 8, 16, 32, 64, or 128 bytes |
| CRC | 4 to 128 bytes | CRC checksum of the payload |
| Payload Body Length | 4 bytes | Length of the payload body (u32) |
As seen in the PayloadHeader structure, it does not have a fixed size since the lengths of the signature and CRC can vary. For example, when using the bincode feature, both the signature and CRC lengths are set to 4 bytes (32 bits). However, users can implement their own versions with different lengths.
Thus, any payload in binary format is represented as follows:
| Component | Size | Description |
|---|---|---|
PayloadHeader |
14 - 262 bytes | Header containing metadata about the payload |
| Payload's body | --- | Payload data encoded using PayloadEncode |
The CRC of the payload is generated based on the bytes produced by PayloadEncode. This introduces some constraints on CRC verification since brec does not restrict the types of data used in a payload. If a payload contains data types that do not guarantee a strict byte sequence, CRC verification will always fail due to byte order variations. As a result, extracting the payload from the data stream will become impossible.
A simple example of such a situation is a HashMap, which does not guarantee a consistent field order when reconstructed. For instance, defining a payload like this:
#[payload(bincode)]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct MyPayloadB {
items: HashMap<String, String>,
}
would make it impossible to extract this payload, as the CRC would always be different (except when the number of keys in the map is ≤ 1). This issue can be resolved in several ways:
no_crc directive: #[payload(bincode, no_crc)].PayloadCrc trait manually for the specific payload. Automatic CRC calculation can be disabled using the no_auto_crc directive: #[payload(bincode, no_auto_crc)].A packet serves as a container for storing a set of blocks and optionally a single payload. Each packet includes its own header, PacketHeader:
PacketHeader Structure| Field | Size | Description |
|---|---|---|
| Signature | 8 bytes | Static packet signature |
| Size | 8 bytes | Total size of the packet (excluding the PacketHeader) |
| Block's length | 8 bytes | Total length of all blocks (including signatures and CRC) in the packet |
| Payload existence flag | 1 byte | 0 - packet without payload; 1 - packet contains a payload |
| CRC | 4 bytes | CRC of the PacketHeader (not the entire packet, only the header) |
Thus, in binary format, a packet is structured as follows:
| Component | Size | Count |
|---|---|---|
PacketHeader |
29 bytes | 1 |
Block |
--- | 0 to 255 |
Payload |
--- | 0 or 1 |
brec StabilityThe stability of brec is ensured through two levels of testing.
To test reading, writing, parsing, filtering, and other functions, brec uses proptest to generate random values for predefined (structurally consistent) blocks and payloads. Blocks and payloads are tested both separately and in combination as part of packet testing.
Packets are constructed with randomly generated blocks and payloads. Additionally, the ability of brec tools to reliably read and write randomly generated blocks is also tested, specifically focusing on Storage<S: std::io::Read + std::io::Write + std::io::Seek> and PacketBufReader.
In total, over 40 GB of test data is generated for this type of testing.
To validate the behavior of the block and payload macros, brec also uses proptest, but this time it not only generates random data but also randomly constructs block and payload structures.
Each randomly generated set of structures is saved as a separate crate. After generating these test cases, each one is compiled and executed to ensure stability. Specifically, all randomly generated packets must be successfully encoded and subsequently decoded without errors.
To evaluate the performance of the protocol, the following data structure is used:
pub enum Level {
Err,
Warn,
Info,
Debug,
}
pub enum Target {
Server,
Client,
Proxy,
}
#[block]
pub struct Metadata {
pub level: Level,
pub target: Target,
pub tm: u64,
}
Note: Conversion of Level and Target into u8 is required but omitted here for brevity.
Each packet consists of a Metadata block and a String payload. Data is randomly generated, and a special "hook" string is inserted randomly into some messages for use in filtering tests.
brec storage API — Storage<S: std::io::Read + std::io::Write + std::io::Seek> — and then read back using the same interface.PacketBufReader.Storage, but read using PacketBufReader, which ignores slot metadata (treating it as garbage).\n.serde_json and written as one JSON object per line. During reading, each line is deserialized back to the original structure.Each test is run in two modes:
Plain Text is used as a baseline due to its minimal overhead — raw sequential file reading with no parsing or decoding.
However, brec performance is more meaningfully compared with JSON, which also involves deserialization.
JSON is considered a strong baseline due to its wide use and mature, highly optimized parser.
brec component. CRC is calculated for blocks, payloads, and slots (in the case of storage).Iterations column).| Test | Mode | Size | Rows | Time (ms) | Iterations |
|---|---|---|---|---|---|
| Storage | Filtering | 908 Mb | 140,000 | 612 | 10 |
| Storage | Reading | 908 Mb | 1,000,000 | 987 | 10 |
| JSON | Reading | 919 Mb | 1,000,000 | 597 | 10 |
| JSON | Filtering | 919 Mb | 140,000 | 608 | 10 |
| Binary Stream | Reading | 831 Mb | 1,000,000 | 764 | 10 |
| Binary Stream | Filtering | 831 Mb | 140,000 | 340 | 10 |
| Plain Text | Reading | 774 Mb | 1,000,000 | 247 | 10 |
| Plain Text | Filtering | 774 Mb | 150,000 | 276 | 10 |
| Streamed Storage | Filtering | 908 Mb | 140,000 | 355 | 10 |
| Streamed Storage | Reading | 908 Mb | 1,000,000 | 790 | 10 |
PacketBufReader, even if the slot metadata becomes unreadable.PacketBufReader) shows exceptional filtering performance — nearly twice as fast as JSON — and even full reading is only slightly slower than JSON (~167ms on 1 GB), which is not significant in most scenarios.This efficiency is possible because brec's architecture allows it to skip unnecessary work. In contrast to JSON, where every line must be deserialized, brec can evaluate blocks before parsing payloads, leading to better filtering performance.