8 stable releases
2.2.0 | Nov 26, 2024 |
---|---|
2.1.2 | Oct 27, 2024 |
2.0.0 | Oct 26, 2024 |
1.1.1 | Oct 26, 2024 |
1.0.0 | Oct 25, 2024 |
#326 in Rust patterns
355 downloads per month
Used in endian-writer-derive
135KB
2K
SLoC
endian-writer
About
[no_std] endian_writer is a Rust crate that provides utilities for reading and writing data in both little-endian and big-endian formats.
It offers interchangeable readers and writers through traits, allowing for flexible and efficient serialization and deserialization of data structures.
Installation
Add endian_writer
to your Cargo.toml
:
[dependencies]
endian_writer = "2.2.0" # Replace with the actual version
Usage
Basic Usage of BigEndianReader
/ LittleEndianReader
The BigEndianReader
and LittleEndianReader
allow you to read data from a raw byte pointer in
a specified endian format.
use endian_writer::*;
use core::mem::size_of;
unsafe fn example_reader() {
// Example with BigEndianReader
let big_data: [u8; 8] = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]; // Big-endian for 0x0807060504030201u64
let mut big_reader = BigEndianReader::new(big_data.as_ptr());
// Read a u64 value
let big_value: u64 = big_reader.read_u64();
assert_eq!(big_value, 0x0807060504030201);
println!("Big-endian read value: {:#x}", big_value);
// Example with LittleEndianReader
let little_data: [u8; 8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; // Little-endian for 0x0807060504030201u64
let mut little_reader = LittleEndianReader::new(little_data.as_ptr());
// Read a u64 value
let little_value: u64 = little_reader.read_u64();
assert_eq!(little_value, 0x0807060504030201);
println!("Little-endian read value: {:#x}", little_value);
}
Basic Usage of BigEndianWriter
/ LittleEndianWriter
The BigEndianWriter
and LittleEndianWriter
allow you to write data to a raw byte pointer in
a specified endian format.
use endian_writer::*;
use core::mem::size_of;
unsafe fn example_writer() {
// Example with BigEndianWriter
let mut big_data: [u8; 8] = [0; 8];
let mut big_writer = BigEndianWriter::new(big_data.as_mut_ptr());
// Write a u64 value
big_writer.write_u64(0x0807060504030201u64);
assert_eq!(big_data, [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]); // Big-endian for 0x0807060504030201u64
println!("Big-endian written data: {:?}", big_data);
// Example with LittleEndianWriter
let mut little_data: [u8; 8] = [0; 8];
let mut little_writer = LittleEndianWriter::new(little_data.as_mut_ptr());
// Write a u64 value
little_writer.write_u64(0x0807060504030201u64);
assert_eq!(little_data, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); // Little-endian for 0x0807060504030201u64
println!("Little-endian written data: {:?}", little_data);
}
Interchangeable Readers/Writers via Traits
Readers and writers are interchangeable through the use of traits:
- Readers implement the
EndianReader
trait. - Writers implement the
EndianWriter
trait. - Types that can be written/read implement
EndianWritableAt
andEndianReadableAt
, respectively, along withHasSize
.- This provides the
write
,write_at
,read
,read_at
function, along with future extension methods.
- This provides the
This allows you to write generic functions that can operate on any endian-specific reader or writer.
use endian_writer::*;
struct FileEntry {
hash: u64,
data: u64,
}
/// Allows you to serialize with either LittleEndianWriter or BigEndianWriter
impl EndianWritableAt for FileEntry {
unsafe fn write_at<W: EndianWriter>(&self, writer: &mut W, offset: isize) {
writer.write_u64_at(self.hash, offset);
writer.write_u64_at(self.data, offset + 8);
}
}
impl HasSize for FileEntry {
const SIZE: usize = 16;
}
/// Allows you to deserialize with either LittleEndianReader or BigEndianReader
impl EndianReadableAt for FileEntry {
unsafe fn read_at<R: EndianReader>(reader: &mut R, offset: isize) -> Self {
let hash = reader.read_u64_at(offset);
let data = reader.read_u64_at(offset + 8);
FileEntry { hash, data }
}
}
unsafe fn example_serialize_deserialize() {
let entry = FileEntry {
hash: 0x123456789ABCDEF0,
data: 0x0FEDCBA987654321,
};
// Write using LittleEndianWriter
let mut buffer_le: [u8; 16] = [0; 16];
let mut writer_le = LittleEndianWriter::new(buffer_le.as_mut_ptr());
writer_le.write(&entry);
assert_eq!(
buffer_le,
[
0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, // hash in little-endian
0x21, 0x43, 0x65, 0x87, 0xA9, 0xCB, 0xED, 0x0F // data in little-endian
]
);
println!("Written buffer (Little Endian): {:?}", buffer_le);
// Write using BigEndianWriter
let mut buffer_be: [u8; 16] = [0; 16];
let mut writer_be = BigEndianWriter::new(buffer_be.as_mut_ptr());
writer_be.write(&entry);
assert_eq!(
buffer_be,
[
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, // hash in big-endian
0x0F, 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21 // data in big-endian
]
);
println!("Written buffer (Big Endian): {:?}", buffer_be);
// Read using LittleEndianReader
let mut reader_le = LittleEndianReader::new(buffer_le.as_ptr());
let read_le: FileEntry = reader_le.read();
assert_eq!(read_le.hash, entry.hash);
assert_eq!(read_le.data, entry.data);
println!(
"Read entry (Little Endian): hash={:#x}, data={:#x}",
read_le.hash, read_le.data
);
// Read using BigEndianReader
let mut reader_be = BigEndianReader::new(buffer_be.as_ptr());
let read_be: FileEntry = reader_be.read();
assert_eq!(read_be.hash, entry.hash);
assert_eq!(read_be.data, entry.data);
println!(
"Read entry (Big Endian): hash={:#x}, data={:#x}",
read_be.hash, read_be.data
);
}
Automatically Deriving The Traits
The following piece of Rust code.
use endian_writer_derive::EndianWritable;
#[derive(EndianWritable)]
#[repr(C)]
struct MyStruct {
a: u32,
b: u16,
c: u8,
}
Will automatically derive the traits EndianWritableAt, EndianReadableAt, and HasSize provided all children of the struct also implement these traits.
For more details and caveats, see the endian-writer-derive crate.
Performance Note
As seen in the code above, it's more efficient to use multiple write_at
or read_at
methods which don't advance the read pointer. When all fields are read, use a seek
to advance the pointer.
This approach makes it an easier job for the compiler to optimize the code, and is also faster in debug builds.
Extensions: Reading Slices of Structs
You can use write_entries
and read_entries
for efficient batch operations.
use endian_writer::*;
#[derive(Debug, PartialEq, Copy, Clone)]
struct FileEntry {
hash: u64,
data: u64,
}
impl EndianWritableAt for FileEntry {
unsafe fn write_at<W: EndianWriter>(&self, writer: &mut W, offset: isize) {
writer.write_u64_at(self.hash, offset);
writer.write_u64_at(self.data, offset + 8);
}
}
impl EndianReadableAt for FileEntry {
unsafe fn read_at<R: EndianReader>(reader: &mut R, offset: isize) -> Self {
let hash = reader.read_u64_at(offset);
let data = reader.read_u64_at(offset + 8);
FileEntry { hash, data }
}
}
impl HasSize for FileEntry {
const SIZE: usize = 16;
}
unsafe {
let entries = [
FileEntry { hash: 0x123456789ABCDEF0, data: 0x0FEDCBA987654321 },
FileEntry { hash: 0x0, data: 0x1 },
];
// Write a slice of 2 entries.
let mut buffer = [0u8; 32];
let mut writer = LittleEndianWriter::new(buffer.as_mut_ptr());
writer.write_entries(&entries);
// Read a slice of 2 entries.
let mut reader = LittleEndianReader::new(buffer.as_ptr());
let mut read_entries = [FileEntry { hash: 0, data: 0 }; 2];
reader.read_entries(&mut read_entries);
}
You can also convert to/from as part of the write step, for example, write FileEntryB
as FileEntry
.
And read FileEntry
as FileEntryB
.
use endian_writer::*;
// The entry represented in the bytes
#[derive(Debug, PartialEq, Copy, Clone)]
struct FileEntry {
hash: u64,
data: u64,
}
// The entry represented in the code
#[derive(Debug, PartialEq, Copy, Clone)]
struct FileEntryB {
hash: u64,
data: u64,
}
// To FileEntry for writing.
impl From<FileEntryB> for FileEntry {
fn from(source: FileEntryB) -> Self {
FileEntry {
hash: source.hash,
data: source.data,
}
}
}
// To FileEntryB for reading.
impl From<FileEntry> for FileEntryB {
fn from(source: FileEntry) -> Self {
FileEntryB {
hash: source.hash,
data: source.data,
}
}
}
unsafe {
let sources = vec![
FileEntryB { hash: 0x123456789ABCDEF0, data: 0x0FEDCBA987654321 },
FileEntryB { hash: 0x0, data: 0x1 },
];
// Write 2 FileEntryB as FileEntry
let mut buffer = [0u8; 32];
let mut writer = LittleEndianWriter::new(buffer.as_mut_ptr());
writer.write_entries_into::<FileEntry, FileEntryB>(&sources);
// Read 2 FileEntry as FileEntryB
let mut reader = LittleEndianReader::new(buffer.as_ptr());
let mut read_entries = vec![FileEntryB { hash: 0, data: 0 }; 2];
reader.read_entries_into::<FileEntryB, FileEntry>(&mut read_entries);
}
// Manual trait implementation, same as if you derived `EndianSerializable`
impl HasSize for FileEntry {
const SIZE: usize = 16;
}
impl EndianWritableAt for FileEntry {
unsafe fn write_at<W: EndianWriter>(&self, writer: &mut W, offset: isize) {
writer.write_u64_at(self.hash, offset);
writer.write_u64_at(self.data, offset + 8);
}
}
impl EndianReadableAt for FileEntry {
unsafe fn read_at<R: EndianReader>(reader: &mut R, offset: isize) -> Self {
let hash = reader.read_u64_at(offset);
let data = reader.read_u64_at(offset + 8);
FileEntry { hash, data }
}
}
Development
For information on how to work with this codebase, see README-DEV.MD.
License
Licensed under MIT.
Learn more about Reloaded's general choice of licensing for projects..
Dependencies
~220–660KB
~16K SLoC