Crate binary_util

source ·
Expand description

Binary Util

A panic-free way to read and write binary data over the wire.

BinaryUtils provides the following features:

Getting Started

Binary Utils is available on crates.io, add the following to your Cargo.toml:

[dependencies]
binary_util = "0.3.0"

Optionally, if you wish to remove the macros feature, you can add the following to your Cargo.toml:

[dependencies]
binary_util = { version = "0.3.0", default-features = false }

Binary IO

The io module provides a way to contingiously write and read binary data with the garauntees of being panic-free. This module provides two structs, ByteReader and ByteWriter, which are both wrappers around bytes::Buf and bytes::BufMut respectively.

Generally, you will want to use ByteReader and ByteWriter when you are reading and writing binary data manually.

Read Example:

The following example shows how to read a varint from a stream:

use binary_util::io::ByteReader;

const BUFFER: &[u8] = &[255, 255, 255, 255, 7]; // 2147483647

fn main() {
    let mut buf = ByteReader::from(&BUFFER[..]);
    buf.read_var_u32().unwrap();
}

Write Example:

The following is an example of how to write a string to a stream:

use binary_util::io::ByteWriter;

fn main() {
    let mut buf = ByteWriter::new();
    buf.write_string("Hello world!");
}

Real-world example:

A more real-world use-case of this module could be a simple pong server, where you have two packets, Ping and Pong, that respectively get relayed over udp. This is an example using both ByteReader and ByteWriter utilizing std::net::UdpSocket to send and receive packets.

use binary_util::io::{ByteReader, ByteWriter};
use std::net::UdpSocket;

pub struct PingPacket {
    pub time: u64
}

pub struct PongPacket {
    pub time: u64,
    pub ping_time: u64
}

fn main() -> std::io::Result<()> {
    let socket = UdpSocket::bind("127.0.0.1:5000")?;
    let mut buf = [0; 1024];

    loop {
        let (amt, src) = socket.recv_from(&mut buf)?;
        let mut buf = ByteReader::from(&buf[..amt]);

        match buf.read_u8()? {
            0 => {
                let ping = PingPacket {
                    time: buf.read_var_u64()?
                };

                println!("Received ping from {}", src);

                let mut writer = ByteWriter::new();
                let pong = PongPacket {
                    time: std::time::SystemTime::now()
                            .duration_since(
                                std::time::UNIX_EPOCH
                            )
                            .unwrap()
                            .as_millis() as u64,
                    ping_time: ping.time
                };

                // Write pong packet
                writer.write_u8(1);
                writer.write_var_u64(pong.time);
                writer.write_var_u64(pong.ping_time);
                socket.send_to(writer.as_slice(), src)?;
            },
            1 => {
                let pong = PongPacket {
                    time: buf.read_var_u64()?,
                    ping_time: buf.read_var_u64()?
                };
                println!(
                    "Received pong from {} with ping time of {}ms",
                    src,
                    pong.time - pong.ping_time
                );
            }
            _ => {
                println!("Received unknown packet from {}", src);
            }
        }
    }
}

Interfaces

The interfaces module provides a way to implement reading and writing binary data with two traits, Reader and Writer.

Generally, you will refer to using BinaryIo when you are implementing or enum; However in the scenario you are implementing a type that may not be compatible with BinaryIo, you can use these traits instead.

Example: The following example implements the Reader and Writer traits for a HelloPacket allowing it to be used with BinaryIo; this example also allows you to read and write the packet with an easier convention.

use binary_util::interfaces::{Reader, Writer};
use binary_util::io::{ByteReader, ByteWriter};

pub struct HelloPacket {
    pub name: String,
    pub age: u8,
    pub is_cool: bool,
    pub friends: Vec<String>
}

impl Reader<HelloPacket> for HelloPacket {
    fn read(buf: &mut ByteReader) -> std::io::Result<Self> {
        Ok(Self {
            name: buf.read_string()?,
            age: buf.read_u8()?,
            is_cool: buf.read_bool()?,
            friends: Vec::<String>::read(buf)?
        })
    }
}

impl Writer<HelloPacket> for HelloPacket {
    fn write(&self, buf: &mut ByteWriter) -> std::io::Result<()> {
        buf.write_string(&self.name);
        buf.write_u8(self.age);
        buf.write_bool(self.is_cool);
        self.friends.write(buf)?;
        Ok(())
    }
}

With the example above, you now are able to read and write the packet with BinaryIo, as well as the added functionality of being able to read and write the packet with easier with the read and write methods that are now implemented.

fn main() {
    let mut buf = ByteWriter::new();
    let packet = HelloPacket {
        name: "John".to_string(),
        age: 18,
        is_cool: true,
        friends: vec!["Bob".to_string(), "Joe".to_string()]
    };
    buf.write_type(&packet).unwrap();
}

Codegen

The BinaryIo derive macro provides a way to implement both Reader and Writer for a type. This macro is extremely useful when you are trying to implement multiple data structures that you want to seemlessly read and write with the io module.

Example: The following example implements the BinaryIo trait for a HelloPacket, shortening the previous example to just a few lines of code.

use binary_util::BinaryIo;

#[derive(BinaryIo)]
pub struct HelloPacket {
    pub name: String,
    pub age: u8,
    pub is_cool: bool,
    pub friends: Vec<String>
}

fn main() {
    let mut buf = ByteWriter::new();
    let packet = HelloPacket {
        name: "John".to_string(),
        age: 18,
        is_cool: true,
        friends: vec!["Bob".to_string(), "Joe".to_string()]
    };
    buf.write_type(&packet).unwrap();
}

You can view additional implementations of the derive macro by looking at the examples on the module page.

Re-exports

Modules

  • This is a legacy module that will be removed in the future. This module has been replaced in favor of std::io::Error.
  • Provides a panic-free way to read and write binary data. All of the methods within this module follow the protobuf specification at https://protobuf.dev/programming-guides/encoding/.
  • The io module contains implementations of these traits for bytes::Buf and bytes::BufMut.

Derive Macros

  • This proc-macro implements both the Reader and Writer traits from binary_util::interfaces. It is important to note that not all attributes can be used on all types, and some attributes are exclusive to certain variants.
  • DEPRECATED. This is a legacy proc-macro that is used to generate a BufferStream. It provides an easy way to implement the Streamable trait.