Class: RubyBits::Structure

Inherits:
Object
  • Object
show all
Defined in:
lib/rubybits.rb

Overview

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.

Examples:

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]

Constant Summary

FIELD_TYPES =
{
  :unsigned => {
    :validator => proc{|val, size, options| val.is_a?(Fixnum) && val < 2**size},
    :unpack => proc {|s, offset, length, options|
      number = 0
      s_iter = s.bytes
      byte = 0
      # advance the iterator by the number of whole or partial bytes in the offset (offset div 8)
      ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
      
      length.times{|bit|
        byte = s_iter.next if offset % 8 == 0
        src_bit = (7-offset%8)
        number |= (1 << (length-1-bit)) if (byte & (1 << src_bit)) > 0
        #puts "Reading: #{src_bit} from #{"%08b" % byte} => #{(byte & (1 << src_bit)) > 0 ? 1 : 0}"
        offset += 1
      }
      number
    }
  },
  :signed => {
    :validator => proc{|val, size, options| val.is_a?(Fixnum) && val.abs < 2**(size-1)},
    :unpack => proc{|s, offset, length, options|
      number = 0
      s_iter = s.bytes
      byte = 0
      # advance the iterator by the number of whole bytes in the offset (offset div 8)
      ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
      # is this a positive number? yes if the most significant bit is 0
      byte = s_iter.next if offset % 8 == 0
      pos = byte & (1 << 7 - offset%8) == 0
      #puts "String: #{s.bytes.to_a.collect{|x| "%08b" % x}.join(" ")}"
      #puts "Byte: #{"%08b" % byte}, offset: #{offset}"
      
      length.times{|bit|
        byte = s_iter.next if offset % 8 == 0 && bit > 7
        src_bit = (7-offset%8)
        number |= (1 << (length-1-bit)) if ((byte & (1 << src_bit)) > 0) ^ (!pos)
        offset += 1
      }
      #puts "Pos #{pos}, number: #{number}"
      pos ? number : -number-1
    }
  },
  :variable => {
    :validator => proc{|val, size, options| val.is_a?(String)},
    :unpack => proc{|s, offset, length, options|
      output = []
      s_iter = s.bytes
      byte = 0
      # advance the iterator by the number of whole bytes in the
      # offset (offset div 8)
      ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
      length.times{|bit|
        byte = s_iter.next if offset % 8 == 0
        output << 0 if bit % 8 == 0
        
        src_bit = (7-offset%8)
        output[-1] |= (1 << (7-bit%8)) if (byte & (1 << src_bit)) > 0
        offset += 1
      }
      output.pack("c*")
    }
  }
}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(values = {}) ⇒ Structure

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

Examples:

MyStructure.new(:field1 => 44, :field2 => 0x70, :field3 => "hello")


252
253
254
255
256
257
# File 'lib/rubybits.rb', line 252

def initialize(values={})
  values.each{|key, value|
    self.send "#{key}=", value
  }
  @_checksum_cached = false
end

Class Method Details

.checksum(field) {|bytes| ... } ⇒ Object

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.

Parameters:

  • field (Symbol)

    the field that contains the checksum data

Yields:

  • (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



130
131
132
133
134
135
136
137
138
# File 'lib/rubybits.rb', line 130

def checksum field, &block
  @_checksum_field = [field, block]
  self.class_eval %{
    def #{field}
      calculate_checksum unless @_calculating_checksum || @_checksum_cached
      @__#{field}
    end
  }
end

.checksum_fieldObject

The checksum field



144
# File 'lib/rubybits.rb', line 144

def checksum_field; @_checksum_field; end

.fieldsObject

A list of the fields in the class



141
# File 'lib/rubybits.rb', line 141

def fields; @_fields; end

.from_string(string) ⇒ Array<Structure, string>

Parses a message from the binary string assuming that the message starts at the first byte of the string

Parameters:

  • string (String)

    a binary string to be interpreted

Returns:

  • (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)



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/rubybits.rb', line 180

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) ⇒ Boolean

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.

Parameters:

  • string (String)

    a binary string to be tested, not including a checksum region if applicable

Returns:

  • (Boolean)

    whether the string is likely to be a valid message



160
161
162
163
164
165
166
167
168
169
# File 'lib/rubybits.rb', line 160

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

.parse(string) ⇒ Array<Array<Structure>, String>

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).

Parameters:

  • string (String)

    a binary string containing the messages to be parsed

Returns:

  • (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.



213
214
215
216
217
218
219
220
221
222
# File 'lib/rubybits.rb', line 213

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) ⇒ Boolean

Determines whether a string is a valid message

Parameters:

  • string (String)

    a binary string to be tested

Returns:

  • (Boolean)

    whether the string is in fact a valid message



149
150
151
# File 'lib/rubybits.rb', line 149

def valid_message? string
  !!from_string(string)[0]
end

Instance Method Details

#calculate_checksumObject

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



270
271
272
273
274
275
276
277
278
279
# File 'lib/rubybits.rb', line 270

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_sString

Returns a binary string representation of the structure according to the fields defined and their current values.

Returns:

  • (String)

    bit string representing struct



262
263
264
265
266
267
# File 'lib/rubybits.rb', line 262

def to_s
  if self.class.checksum_field && !@_checksum_cached
    self.calculate_checksum
  end
  to_s_without_checksum
end