class RubyBits::Structure

You can subclass RubyBits::Strcuture to define new binary formats. This can be used for lots of purposes: reading binary data, communicating in binary formats (like TCP/IP, http, etc).

Currently, three field types are supported: unsigned, signed and variable. Unsigned and signed fields are big-endian and can be any number of bits in size. Unsigned integers are assumed to be encoded with two’s complement. Variable fields are binary strings with their size defined by the value of another field (given by passing that field’s name to the :length option). This size is assumed to be in bytes; if it is in fact in bits, you should pass :bit to the :unit option (see the example). Note that variable-length fields must have whole-byte sizes, though they need not be byte-aligned.

@example

class NECProjectorFormat < RubyBits::Structure
  unsigned :id1,     8,    "Identification data assigned to each command"
  unsigned :id2,     8,    "Identification data assigned to each command"
  unsigned :p_id,    8,    "Projector ID"
  unsigned :m_code,  4,    "Model code for projector"
  unsigned :len,     12,   "Length of data in bytes"
  variable :data,          "Packet data", :length => :len
  unsigned :checksum,8,    "Checksum"

  checksum :checksum do |bytes|
    bytes[0..-2].inject{|sum, byte| sum + byte} & 255
  end
end

NECProjectorFormat.parse(buffer)
# => [[<NECProjectorFormat>, <NECProjectorFormat>], rest]

NECProjectorFormat.new(:id1 => 0x44, :id2 => 2, :p_id => 0, :m_code => 0, :len => 5, :data => "hello").to_s.bytes.to_a
# => [0x44, 0x2, 0x05, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x5F]

Constants

FIELD_TYPES

@private

Public Class Methods

checksum(field, &block) click to toggle source

Sets the checksum field. Setting a checksum field alters the functionality in several ways: the checksum is automatically calculated and set, and parse will only consider a bitstring to be a valid instance of the structure if it has a checksum appropriate to its data. @param field [Symbol] the field that contains the checksum data @yield [bytes] block that should calculate the checksum given bytes, which is

an array of bytes representing the full structure, with the checksum field
set to 0
# File lib/rubybits.rb, line 131
def checksum field, &block
  @_checksum_field = [field, block]
  self.class_eval %{
    def #{field}
      calculate_checksum unless @_calculating_checksum || @_checksum_cached
      @__#{field}
    end
  }
end
checksum_field() click to toggle source

The checksum field

# File lib/rubybits.rb, line 145
def checksum_field; @_checksum_field; end
fields() click to toggle source

A list of the fields in the class

# File lib/rubybits.rb, line 142
def fields; @_fields; end
from_string(string) click to toggle source

Parses a message from the binary string assuming that the message starts at the first byte of the string @param string [String] a binary string to be interpreted @return [Array<Structure, string>] a pair with the first

element being a structure object with the data from the
input string (or nil if not a valid structure) and the
second being the left-over bytes from the string (those
after the message or the entire string if no valid message
was found)
# File lib/rubybits.rb, line 181
def from_string(string)
  message = self.new
  iter = 0
  checksum = nil
  fields.each{|field|
    kind, name, size, description, options = field
    options ||= {}
    size = (kind == :variable) ? message.send(options[:length]) : size
    size *= 8 if options[:unit] == :byte
    begin
      value = FIELD_TYPES[kind][:unpack].call(string, iter, size, options)
      message.send("#{name}=", value)
      checksum = value if checksum_field && name == checksum_field[0]
    rescue StopIteration, FieldValueException => e
      return [nil, string]
    end
    iter += size
  }
  # if there's a checksum, make sure the provided one is valid
  return [nil, string] unless message.checksum == checksum if checksum_field
  [message, string[((iter/8.0).ceil)..-1]]
end
maybe_valid?(string) click to toggle source

Determines whether a string is at least the minimum correct length and matches the checksum. This method is less correct than valid_message? but considerably faster. @param string [String] a binary string to be tested, not

including a checksum region if applicable

@return [Boolean] whether the string is likely to be a valid

message
# File lib/rubybits.rb, line 161
def maybe_valid? string
  if string.size >= @_size_sum
    if self.class.checksum_field
      checksum = self.class.checksum_field[1].call(string)
    else
      return true
    end
  end
  return false
end
new(values={}) click to toggle source

Creates a new instance of the class. You can pass in field names to initialize to set their values. @example

MyStructure.new(:field1 => 44, :field2 => 0x70, :field3 => "hello")
# File lib/rubybits.rb, line 253
def initialize(values={})
  values.each{|key, value|
    self.send "#{key}=", value
  }
  @_checksum_cached = false
end
parse(string) click to toggle source

Parses out all of the messages in a given string assuming that the first message starts at the first byte, and there are no bytes between messages (though messages are not allowed to span bytes; i.e., all messages must be byte-aligned). @param string [String] a binary string containing the messages

to be parsed

@return [Array<Array<Structure>, String>] a pair with the

first element being an array of messages parsed out of the
string and the second being whatever part of the string was
left over after parsing.
# File lib/rubybits.rb, line 214
def parse(string)
  messages = []
  last_message = true
  while last_message
    last_message, string = from_string(string)
    #puts "Found message: #{last_message.to_s.bytes.to_a}, string=#{string.bytes.to_a.inspect}"
    messages << last_message if last_message
  end
  [messages, string]
end
valid_message?(string) click to toggle source

Determines whether a string is a valid message @param string [String] a binary string to be tested @return [Boolean] whether the string is in fact a valid message

# File lib/rubybits.rb, line 150
def valid_message? string
  !!from_string(string)[0]
end

Private Class Methods

field(kind, name, size, description, validator, options) click to toggle source
# File lib/rubybits.rb, line 226
def field kind, name, size, description, validator, options
  @_fields ||= []
  @_fields << [kind, name, size, description, options]
  @_size_sum = @_fields.reduce(0){|acc, f|
    f[0] == :variable ? acc : acc + f[2]
  }/8
  self.class_eval do
    define_method "#{name}=" do |val|
      raise FieldValueException unless validator.call(val, size, options)
      self.instance_variable_set("@__#{name}", val)
      @_checksum_cached = false
    end
  end
  unless checksum_field && checksum_field[0] == name
    self.class_eval %{
      def #{name}
        @__#{name}
      end
    }
  end
end

Public Instance Methods

calculate_checksum() click to toggle source

Calculates and sets the checksum bit according to the checksum field defined by checksum

# File lib/rubybits.rb, line 271
def calculate_checksum
  if self.class.checksum_field
    @_calculating_checksum = true
    self.send("#{self.class.checksum_field[0]}=", 0)
    checksum = self.class.checksum_field[1].call(self.to_s_without_checksum.bytes.to_a)
    self.send("#{self.class.checksum_field[0]}=", checksum)
    @_checksum_cached = true
    @_calculating_checksum = false
  end
end
to_s() click to toggle source

Returns a binary string representation of the structure according to the fields defined and their current values. @return [String] bit string representing struct

# File lib/rubybits.rb, line 263
def to_s
  if self.class.checksum_field && !@_checksum_cached
    self.calculate_checksum
  end
  to_s_without_checksum
end

Protected Instance Methods

get_bit(number, bit) click to toggle source

Returns the value at position bit of byte @param number [Fixnum] Number to be queried @param bit [Fixnum] bit of interest @return [Fixnum: {0, 1}] 0 or 1, depending on the value of the bit at position bit of number

# File lib/rubybits.rb, line 297
def get_bit(number, bit)
  number & (1 << bit) > 0 ? 1 : 0
end
set_bit(byte, bit, value) click to toggle source

Returns the input number with the specified bit set to the specified value @param byte [Fixnum] Number to be modified @param bit [Fixnum] Bit number to be set @param value [Fixnum: {0, 1}] Value to set (either 0 or 1) @return [Fixnum] byte with bit set to value

# File lib/rubybits.rb, line 288
def set_bit(byte, bit, value)
  #TODO: this can probably be made more efficient
  byte & (1 << bit) > 0 == value > 0 ? byte : byte ^ (1 << bit)
end
to_s_without_checksum() click to toggle source
# File lib/rubybits.rb, line 301
def to_s_without_checksum
  offset = 0
  buffer = []
  # This method works by iterating through each bit of each field
  # and setting the bits in the current output byte appropriately.
  self.class.fields.each{|field|
    kind, name, size, description, options = field
    data = self.send(name)
    options ||= {}
    case kind
    when :variable
      data ||= ""
      size = options[:length] && self.send(options[:length]) ? self.send(options[:length]) : data.size
      size /= 8 if options[:unit] == :bit
      byte_iter = data.each_byte
      if offset % 8 == 0
        buffer += data.bytes.to_a + [0] * (size - data.size)
      else
        size.times{|i|
          byte = byte_iter.next rescue 0
          8.times{|bit|
            buffer << 0 if offset % 8 == 0
            buffer[-1] |= get_bit(byte, 7-bit) << 7-(offset % 8)
            offset += 1
          }
        }
      end
    else
      data ||= 0
      size.times do |bit|
        buffer << 0 if offset % 8 == 0
        buffer[-1] |= get_bit(data, size-bit-1) << 7-(offset % 8)
        offset += 1
      end
    end
  }
  buffer.pack("c*")
end