BinData Ruby Library

repository·master·Indexed 20 days ago

https://github.com/dmendel/bindata

A Ruby library for the declarative definition, reading, and writing of structured binary data formats. It provides a readable alternative to Ruby's native pack/unpack methods by allowing users to define binary structures using BinData::Record, supporting primitive datatypes, variable-length fields, and complex models including arrays, choices, and buffers.

Tokens
8.8K
Snippets
31
Records
39
Agent score
71%

What's inside BinData

  1. What is BinData?

    master
    BinData is a Ruby library that provides a declarative way to read and write structured binary data. Instead of manually using Ruby's #pack and #unpack methods with complex format strings, you define a class that inherits from BinData::Record. You specify the data format (endianness and field types), and BinData handles the underlying I/O operations. It supports common primitive datatypes, variable-length fields, and dependent fields.
  2. What is a BinData::Buffer and how does it work?

    master

    A BinData::Buffer is a substream within a larger data stream that has a fixed, defined size. It ensures that the amount of data read or written exactly matches its specified length.

    Key behaviors:

    • Short Reads: If the underlying data is shorter than the buffer's length, the buffer will skip over the unused bytes to reach the end of its defined size.
    • Short Writes: If the data being written is shorter than the buffer's length, the buffer will pad the remaining space with null bytes (\0).
    • Encapsulation: A buffer wraps a single type (which can be a primitive or a complex struct). You can access the fields of the wrapped type directly on the buffer instance via method delegation.
    class MyBuffer < BinData::Buffer
      default_parameter length: 8
      endian :little
    
      uint16 :num1
      uint16 :num2
      # padding occurs here to reach 8 bytes
    end
    
    obj = MyBuffer.read("\001\000\002\000\000\000\000\000")
    obj.num1 #=> 1
    obj.num2 #=> 2
    obj.raw_num_bytes #=> 4 (bytes actually used by fields)
    obj.num_bytes #=> 8 (total buffer length)
  3. Initialize BinData primitives with parameters

    master

    When creating a BinData primitive (like Uint8, Int16, etc.), you can pass specific parameters to control its behavior. Note that some parameters are mutually exclusive.

    Available Parameters

    • :initial_value: Sets a starting value to use before the object is read from an IO stream or explicitly set. If no value is read/set, the object returns this value.
    • :value: Forces the object to always have this specific value. When using this, calls to value= are ignored, and during a read operation, value will return the data actually read from the IO, not this parameter.
    • :assert: A validation parameter. It can be a specific value or a lambda. If the value read or assigned does not match the assertion, a BinData::ValidityError is raised. When using a lambda, the variable value is available for comparison.
    • :asserted_value: An alias for both :assert and :value behavior combined.

    Parameter Constraints

    • :initial_value and :value are mutually exclusive.
    • :asserted_value is mutually exclusive with both :value and :assert.
    # Using initial_value
    obj = BinData::Uint8.new(initial_value: 42)
    
    # Using assert with a value
    obj = BinData::Uint8.new(assert: 3)
    
    # Using assert with a lambda
    obj = BinData::Uint8.new(assert: -> { value < 5 })
    
    # Using value (constant value)
    obj = BinData::Uint8.new(value: 42)
  4. Transform IO streams with BinData::IO::Transform

    master

    The BinData::IO::Transform class allows you to create layers that modify the data stream (e.g., for compression, encryption, or encoding) before it reaches the underlying IO. Multiple transforms can be chained together.

    Implementing a Custom Transform

    To create a new transform, subclass BinData::IO::Transform and:

    1. Override the public #read and #write methods.
    2. Optionally implement the hooks #before_transform, #after_read_transform, and #after_write_transform.
    3. Important: If your transform changes the size of the underlying data stream (like compression), you must call transform_changes_stream_length! in your subclass.

    Chaining Transforms

    Use the transform method on a Read or Write object to wrap the current IO with a new transform layer. This method yields both the wrapper and the new transform object to a block.

    # Conceptual usage of transform
    reader.transform(MyCompressionTransform.new) do |r, transform|
      # 'r' is the reader with the transform applied
      # 'transform' is the transform instance
    end
  5. How BinData::Choice works

    master

    A BinData::Choice is a collection of mutually exclusive data objects where only one is active at a time. When a choice is active, all method calls made to the Choice instance are delegated to that specific active choice. This is useful for parsing binary formats where a header field (like a type ID) determines the structure of the subsequent data.

    Key Concepts

    • Selection: The mechanism that determines which object in the collection is currently active. This can be a static value or a dynamic proc/lambda.
    • Delegation: You interact with the Choice object as if it were the active choice itself.
    • Copy on Change: An optional feature that allows the value of the previous selection to be copied to the new selection when the active choice changes.
    require 'bindata'
    
    # Define possible types using [type_symbol, hash_params]
    type1 = [:string, {value: "Type1"}]
    type2 = [:string, {value: "Type2"}]
    
    # Use a Hash for mapping specific keys to types
    choices = {5 => type1, 17 => type2}
    
    a = BinData::Choice.new(choices: choices, selection: 5)
    puts a # => "Type1"
    
    # Use an Array for index-based selection
    choices_array = [type1, type2]
    a = BinData::Choice.new(choices: choices_array, selection: 1)
    puts a # => "Type2"
  6. How DelayedIO supports multi-pass processing

    master

    BinData declarations are typically evaluated in a single pass. However, some binary formats require multi-pass processing, such as when you need to seek backwards in the input stream to read data based on values found later in the stream.

    BinData::DelayedIO enables this by intercepting normal #read or #write calls and deferring the actual I/O operation. To execute the deferred operation, you must explicitly call #read_now! or #write_now!. These methods use the abs_offset provided during initialization to perform the I/O at the correct position in the stream.

    Manual Execution

    You can manually trigger the deferred read/write by calling the methods directly or by passing a block to the standard read/write methods:

    # Manual call
    obj.read_now!
    
    # Using a block
    obj.read("\x00\x00") { obj.read_now! }
    require 'bindata'
    
    obj = BinData::DelayedIO.new(read_abs_offset: 3, type: :uint16be)
    obj.read("\x00\x00\x00\x11\x12")
    obj #=> 0
    
    obj.read_now!
    obj #=> 0x1112
  7. Use Zstd compression transform

    master

    The BinData::Transform::Zstd class provides a transform for handling Zstd compressed data streams within BinData. It implements the BinData::IO::Transform interface to allow for transparent decompression during reads and compression during writes.

    To use this transform, you must have the zstd-ruby gem installed in your environment.

    Requirements:

    • Install the dependency via: gem install zstd-ruby
    gem install zstd-ruby
  8. Automate multi-pass I/O with auto_call_delayed_io

    master

    Instead of manually calling read_now! or write_now!, you can use the auto_call_delayed_io keyword within a BinData::Record. This automatically triggers all deferred delayed_io operations during the standard #read or #to_binary_s (write) calls.

    This is particularly useful for complex formats like Reverse Pascal Strings, where the length of a string is stored at the end of the data block rather than the beginning.

    class ReversePascalString < BinData::Record
      auto_call_delayed_io
    
      delayed_io :str, read_abs_offset: 0 do
        string read_length: :len
      end
      count_bytes_remaining :total_size
      skip to_abs_offset: -> { total_size - 1 }
      uint8  :len, value: -> { str.length }
    end
    
    # The read call will automatically trigger the delayed_io read
    s = ReversePascalString.read("hello\x05")
    s.to_binary_s #=> "hello\x05"
    class ReversePascalString < BinData::Record
      auto_call_delayed_io
    
      delayed_io :str, read_abs_offset: 0 do
        string read_length: :len
      end
      count_bytes_remaining :total_size
      skip to_abs_offset: -> { total_size - 1 }
      uint8  :len, value: -> { str.length }
    end
    
    s = ReversePascalString.read("hello\x05")
    s.to_binary_s #=> "hello\x05"