1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! The [binary-layout](https://crates.io/crates/binary-layout) library allows type-safe, inplace, zero-copy access to structured binary data.
//! You define a custom data layout and give it a slice of binary data, and it will allow you to read and
//! write the fields defined in the layout from the binary data without having to copy any of the data.
//! It's similar to transmuting to/from a `#[repr(packed)]` struct, but [much safer](#why-not-reprpacked).
//!
//! Note that the data does not go through serialization/deserialization or a parsing step.
//! All accessors access the underlying package data directly.
//!
//! # Example
//! ```
//! use binary_layout::prelude::*;
//!
//! // See https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol for ICMP package layout
//! define_layout!(icmp_packet, BigEndian, {
//!   packet_type: u8,
//!   code: u8,
//!   checksum: u16,
//!   rest_of_header: [u8; 4],
//!   data_section: [u8], // open ended byte array, matches until the end of the package
//! });
//!
//! fn func(packet_data: &mut [u8]) {
//!   let mut view = icmp_packet::View::new(packet_data);
//!
//!   // read some data
//!   let code: u8 = view.code().read();
//!   // equivalent: let code: u8 = packet_data[1];
//!
//!   // write some data
//!   view.checksum_mut().write(10);
//!   // equivalent: packet_data[2..4].copy_from_slice(&10u16.to_le_bytes());
//!
//!   // access an open ended byte array
//!   let data_section: &[u8] = view.data_section().data();
//!   // equivalent: let data_section: &[u8] = &packet_data[8..];
//!
//!   // and modify it
//!   view.data_section_mut().data_mut()[..5].copy_from_slice(&[1, 2, 3, 4, 5]);
//!   // equivalent: packet_data[8..13].copy_from_slice(&[1, 2, 3, 4, 5]);
//! }
//! ```
//!
//! # APIs
//! This library offers two alternative APIs:
//! 1. The [Field](binary_layout::Field) API that offers free functions to read/write the data based on an underlying slice of storage (`packet_data` in the example above) holding the packet data. This API does not wrap the underlying slice of storage data, which means you have to pass it in to each accessor.
//!    This is not the API used in the example above, see [Field](binary_layout::Field) for an API example.
//! 2. The [FieldView](binary_layout::FieldView) API that wraps a slice of storage data and remembers it in a `View` object, allowing access to the fields without having to pass in the packed data slice each time. This is the API used in the example above. See [FieldView](binary_layout::FieldView) for another example.
//!
//! # What to use this library for?
//! Anything that needs inplace zero-copy access to structured binary data.
//! - Network packets are an obvious example
//! - File system inodes
//! - Structured binary data in files if you want to avoid explicit (de)serialization, possibly in combination with [memmap](https://docs.rs/memmap).
//!
//! # Why use this library?
//! - Inplace, zero-copy, type-safe access to your data.
//! - Data layout is defined in one central place, call sites can't accidentally use wrong field offsets.
//! - Convenient and simple macro DSL to define layouts.
//! - Define a fixed endianness in the layout, ensuring cross platform compatibility.
//! - Fully written in safe Rust, no [std::mem::transmute] or similar shenanigans.
//! - Const generics make sure that all offset calculations happen at compile time and will not have a runtime overhead.
//! - Comprehensive test coverage.
//!
//! # Why not `#[repr(packed)]`?
//! Annotating structs with `#[repr(packed)]` gives some of the features of this crate, namely it lays out the data fields exactly in the order they're specified
//! without padding. But it has serious shortcomings that this library solves.
//! - `#[repr(packed)]` uses the system byte order, which will be different depending on if you're running on a little endian or big endian system. `#[repr(packed)]` is not cross-platform compatible. This library is.
//! - `#[repr(packed)]` [can cause undefined behavior on some CPUs when taking references to unaligned data](https://doc.rust-lang.org/nomicon/other-reprs.html#reprpacked).
//!    This library avoids that by not offering any API that takes references to unaligned data. The only data type you can get a reference to is byte arrays, and they only require an alignment of 1 which is trivially always fulfilled.
//!
//! # When not to use this library?
//! - You need dynamic data structures, e.g. a list that can change size. This library only supports static data layouts.
//! - Not all of your data fits into the memory and you need to process streams of data.
//!
//! # Alternatives
//! To the best of my knowledge, there is no other library offering inplace, zero-copy and type-safe access to structured binary data.
//! But if you don't need direct access to your data and are ok with a serialization/deserialization step, then there is a number of amazing libraries out there.
//! - [Nom](https://crates.io/crates/nom) is a great crate for all your parsing needs. It can for example parse binary data and put them in your custom structs.
//! - [Binread](https://crates.io/crates/binread), [Binwrite](https://crates.io/crates/binwrite), [Binrw](https://crates.io/crates/binrw) are great libraries for (de)serializing binary data.
//!
//! # Supported field types
//! ## Primitive integer types
//! - [u8](::std::primitive.u8), [u16](::std::primitive.u16), [u32](::std::primitive.u32), [u64](::std::primitive.u64)
//! - [i8](::std::primitive.i8), [i16](::std::primitive.i16), [i32](::std::primitive.i32), [i64](::std::primitive.i64)
//!
//! For these fields, the [Field](binary_layout::Field) API offers [Field::read](binary_layout::Field::read), [Field::write](binary_layout::Field::write) and the [FieldView](binary_layout::FieldView) API offers [FieldView::read](binary_layout::FieldView::Read) and [FieldView::write](binary_layout::FieldView::write).
//!
//! ## Fixed size byte arrays: `[u8; N]`.
//! For these fields, the [Field](binary_layout::Field) API offers [Field::data](binary_layout::Field::data), [Field::data_mut](binary_layout::Field::data_mut), and the [FieldView](binary_layout::FieldView) API offers [FieldView::data](binary_layout::FieldView::data) and [FieldView::data_mut](binary_layout::FieldView::data_mut).
//!
//! ## Open ended byte arrays: `[u8]`.
//! This field type can only occur as the last field of a layout and will mach the remaining data until the end of the storage.
//! This field has a dynamic size, depending on how large the package data is.
//! For these fields, the [Field](binary_layout::Field) API offers [Field::data](binary_layout::Field::data), [Field::data_mut](binary_layout::Field::data_mut) and the [FieldView](binary_layout::FieldView) API offers [FieldView::data](binary_layout::FieldView::data), [FieldView::data_mut](binary_layout::FieldView::data_mut) and [FieldView::extract](binary_layout::FieldView::extract).
//!
//! # Data types maybe supported in the future
//! These data types aren't supported yet, but they could be added in theory and might be added in future versions.
//! - [bool](::std::primitive.bool) stored as 1 byte
//! - [bool](::std::primitive.bool) stored as 1 bit
//!
//! ## Data types with dynamic length
//! This crate relies on a static layout, it cannot support data types with dynamic length.
//! In theory, types with dynamic length could be supported if they either
//! - are the last field of a layout, an already implemented example of this are open ended byte arrays.
//! - or they may be in the middle of the package but have a maximal size defined and will always reserve storage for their maximal size, even if smaller.
//!   This way, the fields after it would still have a constant offset.
//!
//! Both of these, however, would be some effort to implement and it is unclear if that will ever happen (unless somebody opens a PR for it).
//!
//! ## Strings
//! For strings, note that even fixed-size UTF-8 strings take a variable number of characters because of the UTF-8 encoding and that brings all the issues of data types with dynamic length with it.
//! This is why strings aren't supported yet.
//!
//! ## Fixed-size arrays other than `[u8; N]`
//! Say we wanted to have a `[u32; N]` field. The API couldn't just return a zero-copy `&[u32; N]` to the caller because that would use the system byte order (i.e. endianness) which might be different from the byte order defined in the package layout.
//! To make this cross-platform compatible, we'd have to wrap these slices into our own slice type that enforces the correct byte order and return that from the API.
//! This complexity is why it wasn't implemented yet, but feel free to open a PR if you need this.

mod fields;
mod macro_define_layout;

pub use fields::{
    BigEndian, Field, FieldMetadata, FieldSize, FieldView, LittleEndian, SizedFieldMetadata,
};

pub mod prelude {
    pub use super::{FieldMetadata, SizedFieldMetadata};
    pub use crate::define_layout;
}