Class: RubyBits::Structure
- Inherits:
-
Object
- Object
- RubyBits::Structure
- 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.
Constant Summary
- FIELD_TYPES =
{ :unsigned => { :validator => proc{|val, size, | val.is_a?(Fixnum) && val < 2**size}, :unpack => proc {|s, offset, length, | 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, | val.is_a?(Fixnum) && val.abs < 2**(size-1)}, :unpack => proc{|s, offset, length, | 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, | val.is_a?(String)}, :unpack => proc{|s, offset, length, | 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)
-
+ (Object) checksum(field) {|bytes| ... }
Sets the checksum field.
-
+ (Object) checksum_field
The checksum field.
-
+ (Object) fields
A list of the fields in the class.
-
+ (Array<Structure, string>) from_string(string)
Parses a message from the binary string assuming that the message starts at the first byte of the string.
-
+ (Boolean) maybe_valid?(string)
Determines whether a string is at least the minimum correct length and matches the checksum.
-
+ (Array<Array<Structure>, String>) parse(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).
-
+ (Boolean) valid_message?(string)
Determines whether a string is a valid message.
Instance Method Summary (collapse)
-
- (Object) calculate_checksum
Calculates and sets the checksum bit according to the checksum field defined by #checksum.
-
- (Structure) initialize(values = {})
constructor
Creates a new instance of the class.
-
- (String) to_s
Returns a binary string representation of the structure according to the fields defined and their current values.
Constructor Details
- (Structure) initialize(values = {})
Creates a new instance of the class. You can pass in field names to initialize to set their values.
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
+ (Object) checksum(field) {|bytes| ... }
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.
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 |
+ (Object) checksum_field
The checksum field
144 |
# File 'lib/rubybits.rb', line 144 def checksum_field; @_checksum_field; end |
+ (Object) fields
A list of the fields in the class
141 |
# File 'lib/rubybits.rb', line 141 def fields; @_fields; end |
+ (Array<Structure, string>) from_string(string)
Parses a message from the binary string assuming that the message starts at the first byte of the string
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) = self.new iter = 0 checksum = nil fields.each{|field| kind, name, size, description, = field ||= {} size = (kind == :variable) ? .send([:length]) : size size *= 8 if [:unit] == :byte begin value = FIELD_TYPES[kind][:unpack].call(string, iter, size, ) .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 .checksum == checksum if checksum_field [, string[((iter/8.0).ceil)..-1]] end |
+ (Boolean) maybe_valid?(string)
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.
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 |
+ (Array<Array<Structure>, String>) parse(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).
213 214 215 216 217 218 219 220 221 222 |
# File 'lib/rubybits.rb', line 213 def parse(string) = [] = true while , string = from_string(string) #puts "Found message: #{last_message.to_s.bytes.to_a}, string=#{string.bytes.to_a.inspect}" << if end [, string] end |
+ (Boolean) valid_message?(string)
Determines whether a string is a valid message
149 150 151 |
# File 'lib/rubybits.rb', line 149 def string !!from_string(string)[0] end |
Instance Method Details
- (Object) calculate_checksum
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 |
- (String) to_s
Returns a binary string representation of the structure according to the fields defined and their current values.
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 |