IOStruct
A Ruby Struct that can read/write itself from/to IO-like objects. Perfect for parsing binary file formats, network protocols, and other structured binary data.
Installation
Add this line to your application’s Gemfile:
ruby
gem 'iostruct'
And then execute:
bash
$ bundle install
Or install it yourself as:
bash
$ gem install iostruct
Usage
Basic Usage with Pack Format
Define structs using Ruby’s pack/unpack format strings:
```ruby require ‘iostruct’
Define a struct with two 32-bit unsigned integers
Point = IOStruct.new(‘LL’, :x, :y)
Read from binary data
data = [100, 200].pack(‘LL’) point = Point.read(data) point.x # => 100 point.y # => 200
Write back to binary
point.pack # => “\x64\x00\x00\x00\xC8\x00\x00\x00” ```
Hash-Based Definition with C Types
For more readable code, define structs using C-style type names:
```ruby Point = IOStruct.new( struct_name: ‘Point’, fields: { x: ‘int’, y: ‘int’, z: ‘int’, } )
point = Point.read(binary_data)
point.inspect # => “
Supported C Types
| Type | Aliases | Size |
|---|---|---|
uint8_t |
unsigned char, _BYTE |
1 |
uint16_t |
unsigned short |
2 |
uint32_t |
unsigned int, unsigned |
4 |
uint64_t |
unsigned long long |
8 |
int8_t |
char, signed char |
1 |
int16_t |
short, signed short |
2 |
int32_t |
int, signed int, signed |
4 |
int64_t |
long long, signed long long |
8 |
float |
4 | |
double |
8 |
Reading from IO or String
```ruby Header = IOStruct.new(‘L S S’, :magic, :version, :flags)
Read from a File
File.open(‘binary_file’, ‘rb’) do |f| header = Header.read(f) puts header.magic end
Read from a String
header = Header.read(“\x7fELF\x01\x00\x00\x00”)
Track file position with __offset
io = StringIO.new(data) record = MyStruct.read(io) record.__offset # => position where the record was read from ```
Explicit Field Offsets
Specify exact byte offsets for fields (useful for structs with padding or gaps):
ruby
MyStruct = IOStruct.new(
fields: {
magic: 'uint32_t',
flags: { type: 'uint16_t', offset: 0x10 }, # starts at byte 16
data: { type: 'uint32_t', offset: 0x20 }, # starts at byte 32
}
)
Arrays
Define fixed-size arrays within structs:
```ruby Matrix = IOStruct.new( fields: { rows: ‘int’, cols: ‘int’, data: { type: ‘float’, count: 16 }, # 16-element float array } )
m = Matrix.read(binary_data) m.data # => [1.0, 2.0, 3.0, …] m.pack # serializes back to binary ```
Nested Structs
Compose complex structures from simpler ones:
```ruby Point = IOStruct.new(fields: { x: ‘int’, y: ‘int’ })
Rect = IOStruct.new( struct_name: ‘Rect’, fields: { top_left: Point, bottom_right: Point, } )
rect = Rect.read([0, 0, 100, 100].pack(‘i*’)) rect.top_left.x # => 0 rect.bottom_right.x # => 100 rect.pack # serializes entire structure including nested structs ```
Inspect Modes
Choose between hexadecimal (default) or decimal display:
```ruby
# Hex display (default)
HexStruct = IOStruct.new(‘L L’, :a, :b, inspect: :hex)
HexStruct.new(a: 255, b: 256).inspect
# => “
Decimal display
DecStruct = IOStruct.new(‘L L’, :a, :b, inspect: :dec) DecStruct.new(a: 255, b: 256).inspect # => “#<struct DecStruct a=255, b=256>”
Table format for aligned output
struct.to_table
# => “
Auto-Generated Field Names
If you don’t specify field names, they’re generated based on byte offset:
ruby
s = IOStruct.new('C S L')
s.members # => [:f0, :f1, :f3] (offsets 0, 1, 3)
Field Renaming
Rename auto-generated or explicit field names:
```ruby # Rename auto-generated names IOStruct.new(‘C S L’, f0: :byte_val, f3: :long_val)
Rename explicit names
IOStruct.new(‘C S L’, :a, :b, :c, a: :first, c: :last) ```
API Reference
Class Methods
| Method | Description |
|---|---|
IOStruct.new(fmt, *names, **options) |
Create a new struct class with pack format |
IOStruct.new(fields:, **options) |
Create a new struct class with hash definition |
IOStruct.get_type_size(typename) |
Get byte size for a C type name |
MyStruct.read(io_or_string) |
Read and parse binary data |
MyStruct.size |
Return struct size in bytes |
MyStruct::SIZE |
Struct size constant |
MyStruct::FORMAT |
Pack format string |
MyStruct::FIELDS |
Hash of field names to FieldInfo |
Instance Methods
| Method | Description |
|---|---|
#pack |
Serialize to binary string |
#empty? |
True if all fields are zero/nil/empty |
#to_table |
Formatted string with aligned values |
#__offset |
File position where struct was read (nil if from string) |
Constructor Options
| Option | Description |
|---|---|
struct_name: |
Custom name for inspect output |
inspect: |
:hex (default) or :dec for display format |
size: |
Override calculated struct size |
fields: |
Hash defining fields (for hash-based definition) |
Examples
Parsing a BMP File Header
```ruby BMPHeader = IOStruct.new( struct_name: ‘BMPHeader’, fields: { magic: { type: ‘uint16_t’ }, file_size: { type: ‘uint32_t’ }, reserved: { type: ‘uint32_t’ }, data_offset: { type: ‘uint32_t’ }, } )
File.open(‘image.bmp’, ‘rb’) do |f| header = BMPHeader.read(f) puts “File size: #headerheader.file_size bytes” puts “Pixel data starts at: #headerheader.data_offset” end ```
Network Protocol Packet
```ruby Packet = IOStruct.new(‘n n N’, :src_port, :dst_port, :sequence, struct_name: ‘TCPHeader’ )
Big-endian format for network byte order
packet = Packet.read(socket.read(8)) ```
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
License
MIT License - see LICENSE.txt for details.