rubybits 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -3,5 +3,29 @@
3
3
  RubyBits is a library that makes dealing with binary formats easier. In
4
4
  particular, it provides the Structure class, which allows for easy parsing
5
5
  and creation of binary strings according to specific formats. More usage
6
- information can be found in the docs (generated by `rake yard`) or by looking
7
- at the specs.
6
+ information can be found in [the docs](http://rdoc.info/github/mwylde/rubybits/master/frames) or by looking
7
+ at the specs.
8
+
9
+ You can install via rubygems with `gem install rubybits`.
10
+
11
+ Example:
12
+
13
+ class NECProjectorFormat < RubyBits::Structure
14
+ unsigned :id1, 8, "Identification data assigned to each command"
15
+ unsigned :id2, 8, "Identification data assigned to each command"
16
+ unsigned :p_id, 8, "Projector ID"
17
+ unsigned :m_code, 4, "Model code for projector"
18
+ unsigned :len, 12, "Length of data in bytes"
19
+ variable :data, "Packet data", :length => :len, :unit => :byte
20
+ unsigned :checksum,8, "Checksum"
21
+
22
+ checksum :checksum do |bytes|
23
+ bytes[0..-2].inject{|sum, byte| sum + byte} & 255
24
+ end
25
+ end
26
+
27
+ NECProjectorFormat.parse(buffer)
28
+ => [[<NECProjectorFormat>, <NECProjectorFormat>], rest]
29
+
30
+ NECProjectorFormat.new(:id1 => 0x44, :id2 => 2, :p_id => 0, :m_code => 0, :len => 5, :data => "hello").to_s.bytes.to_a
31
+ => [0x44, 0x2, 0x05, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x5F]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
data/lib/rubybits.rb CHANGED
@@ -1,309 +1,339 @@
1
1
  # Provides various utilities for working with binary formats.
2
2
  module RubyBits
3
- # Raised when you set a field to a value that is invalid for the type of
4
- # the field (i.e., too large or the wrong type)
5
- class FieldValueException < Exception; end
6
-
7
- # You can subclass RubyBits::Strcuture to define new binary formats. This
8
- # can be used for lots of purposes: reading binary data, communicating in
9
- # binary formats (like TCP/IP, http, etc).
10
- #
11
- # Currently, three field types are supported: unsigned, signed and variable. Unsigned
12
- # and signed fields are big-endian and can be any number of bits in size. Unsigned
13
- # integers are assumed to be encoded with two's complement. Variable fields are binary
14
- # strings with their size defined by the value of another field (given by passing that
15
- # field's name to the :length option). This size is assumed to be in bits; if it is
16
- # in fact in bytes, you should pass :byte to the :unit option (see the example).
17
- #
18
- # @example
19
- # class NECProjectorFormat < RubyBits::Structure
20
- # unsigned :id1, 8, "Identification data assigned to each command"
21
- # unsigned :id2, 8, "Identification data assigned to each command"
22
- # unsigned :p_id, 8, "Projector ID"
23
- # unsigned :m_code, 4, "Model code for projector"
24
- # unsigned :len, 12, "Length of data in bytes"
25
- # variable :data, 8, "Packet data", :length => :len, :unit => :byte
26
- # unsigned :checksum,8, "Checksum"
27
- #
28
- # checksum :checksum do |bytes|
29
- # bytes[0..-2].inject{|sum, byte| sum += byte} & 255
30
- # end
31
- # end
32
- #
33
- # NECProjectorFormat.parse(buffer)
34
- # # => [[<NECProjectorFormat>, <NECProjectorFormat>], rest]
35
- #
36
- # NECProjectorFormat.new(:id1 => 0x44, :id2 => 2, :p_id => 0, :m_code => 0, :len => 5, :data => "hello").to_s.bytes.to_a
37
- # # => [0x44, 0x2, 0x05, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x5F]
38
- class Structure < Object
39
- class << self
40
- private
41
- #@private
42
- FIELD_TYPES = {
43
- :unsigned => {
44
- :validator => proc{|val, size, options| val.is_a?(Fixnum) && val < 2**size},
45
- :unpack => proc {|s, offset, length, options|
46
- number = 0
47
- s_iter = s.bytes
48
- byte = 0
49
- # advance the iterator by the number of whole or partial bytes in the offset (offset div 8)
50
- ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
51
-
52
- length.times{|bit|
53
- byte = s_iter.next if offset % 8 == 0
54
- src_bit = (7-offset%8)
55
- number |= (1 << (length-1-bit)) if (byte & (1 << src_bit)) > 0
56
- #puts "Reading: #{src_bit} from #{"%08b" % byte} => #{(byte & (1 << src_bit)) > 0 ? 1 : 0}"
57
- offset += 1
58
- }
59
- number
60
- }
61
- },
62
- :signed => {
63
- :validator => proc{|val, size, options| val.is_a?(Fixnum) && val.abs < 2**(size-1)},
64
- :unpack => proc{|s, offset, length, options|
65
- number = 0
66
- s_iter = s.bytes
67
- byte = 0
68
- # advance the iterator by the number of whole bytes in the offset (offset div 8)
69
- ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
70
- # is this a positive number? yes if the most significant bit is 0
71
- byte = s_iter.next if offset % 8 == 0
72
- pos = byte & (1 << 7 - offset%8) == 0
73
- #puts "String: #{s.bytes.to_a.collect{|x| "%08b" % x}.join(" ")}"
74
- #puts "Byte: #{"%08b" % byte}, offset: #{offset}"
75
-
76
- length.times{|bit|
77
- byte = s_iter.next if offset % 8 == 0 && bit > 7
78
- src_bit = (7-offset%8)
79
- number |= (1 << (length-1-bit)) if ((byte & (1 << src_bit)) > 0) ^ (!pos)
80
- offset += 1
81
- }
82
- #puts "Pos #{pos}, number: #{number}"
83
- pos ? number : -number-1
84
- }
85
- },
86
- :variable => {
87
- :validator => proc{|val, size, options| val.is_a?(String)},
88
- :unpack => proc{|s, offset, length, options|
89
- output = []
90
- s_iter = s.bytes
91
- byte = 0
92
- # advance the iterator by the number of whole bytes in the offset (offset div 8)
93
- ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
94
- length.times{|bit|
95
- byte = s_iter.next if offset % 8 == 0
96
- output << 0 if bit % 8 == 0
97
-
98
- src_bit = (7-offset%8)
99
- output[-1] |= (1 << (7-bit%8)) if (byte & (1 << src_bit)) > 0
100
- offset += 1
101
- }
102
- output.pack("c*")
103
- }
104
- }
105
- }
106
- FIELD_TYPES.each{|kind, field|
107
- define_method kind do |name, size, description, *options|
108
- field(kind, name, size, description, field[:validator], options[0])
109
- end
110
- }
111
-
112
- define_method :variable do |name, description, *options|
113
- field(:variable, name, nil, description, FIELD_TYPES[:variable][:validator], options[0])
114
- end
115
-
116
- public
117
- # Sets the checksum field. Setting a checksum field alters the functionality
118
- # in several ways: the checksum is automatically calculated and set, and #parse
119
- # will only consider a bitstring to be a valid instance of the structure if it
120
- # has a checksum appropriate to its data.
121
- # @param field [Symbol] the field that contains the checksum data
122
- # @yield [bytes] block that should calculate the checksum given bytes, which is
123
- # an array of bytes representing the full structure, with the checksum field
124
- # set to 0
125
- def checksum field, &block
126
- @_checksum_field = [field, block]
127
- self.class_eval %{
128
- def #{field}
129
- calculate_checksum unless @_calculating_checksum || @_checksum_cached
130
- @__#{field}
131
- end
132
- }
133
- end
134
-
135
- # A list of the fields in the class
136
- def fields; @_fields; end
137
-
138
- # The checksum field
139
- def checksum_field; @_checksum_field; end
140
-
141
- # Determines whether a string is a valid message
142
- # @param string [String] a binary string to be tested
143
- # @return [Boolean] whether the string is in fact a valid message
144
- def valid_message? string
145
- !!from_string(string)[0]
146
- end
147
-
148
- # Parses a message from the binary string assuming that the message starts at the first byte
149
- # of the string
150
- # @param string [String] a binary string to be interpreted
151
- # @return [Array<Structure, string>] a pair with the first element being a structure object with
152
- # the data from the input string (or nil if not a valid structure) and the second being the
153
- # left-over bytes from the string (those after the message or the entire string if no valid
154
- # message was found)
155
- def from_string(string)
156
- message = self.new
157
- iter = 0
158
- checksum = nil
159
- fields.each{|field|
160
- kind, name, size, description, options = field
161
- options ||= {}
162
- size = (kind == :variable) ? message.send(options[:length]) : size
163
- size *= 8 if options[:unit] == :byte
164
- begin
165
- value = FIELD_TYPES[kind][:unpack].call(string, iter, size, options)
166
- message.send("#{name}=", value)
167
- checksum = value if checksum_field && name == checksum_field[0]
168
- rescue StopIteration, FieldValueException => e
169
- return [nil, string]
170
- end
171
- iter += size
172
- }
173
- # if there's a checksum, make sure the provided one is valid
174
- return [nil, string] unless message.checksum == checksum if checksum_field
175
- [message, string[((iter/8.0).ceil)..-1]]
176
- end
177
-
178
- # Parses out all of the messages in a given string assuming that the first message
179
- # starts at the first byte, and there are no bytes between messages (though messages
180
- # are not allowed to span bytes; i.e., all messages must be byte-aligned).
181
- # @param string [String] a binary string containing the messages to be parsed
182
- # @return [Array<Array<Structure>, String>] a pair with the first element being an
183
- # array of messages parsed out of the string and the second being whatever part of
184
- # the string was left over after parsing.
185
- def parse(string)
186
- messages = []
187
- last_message = true
188
- while last_message
189
- last_message, string = from_string(string)
190
- #puts "Found message: #{last_message.to_s.bytes.to_a}, string=#{string.bytes.to_a.inspect}"
191
- messages << last_message if last_message
192
- end
193
- [messages, string]
194
- end
3
+ # Raised when you set a field to a value that is invalid for the type of
4
+ # the field (i.e., too large or the wrong type)
5
+ class FieldValueException < Exception; end
6
+
7
+ # You can subclass RubyBits::Strcuture to define new binary
8
+ # formats. This can be used for lots of purposes: reading binary
9
+ # data, communicating in binary formats (like TCP/IP, http, etc).
10
+ #
11
+ # Currently, three field types are supported: unsigned, signed and
12
+ # variable. Unsigned and signed fields are big-endian and can be any
13
+ # number of bits in size. Unsigned integers are assumed to be
14
+ # encoded with two's complement. Variable fields are binary strings
15
+ # with their size defined by the value of another field (given by
16
+ # passing that field's name to the :length option). This size is
17
+ # assumed to be in bytes; if it is in fact in bits, you should pass
18
+ # :bit to the :unit option (see the example). Note that
19
+ # variable-length fields must have whole-byte sizes, though they
20
+ # need not be byte-aligned.
21
+ #
22
+ # @example
23
+ # class NECProjectorFormat < RubyBits::Structure
24
+ # unsigned :id1, 8, "Identification data assigned to each command"
25
+ # unsigned :id2, 8, "Identification data assigned to each command"
26
+ # unsigned :p_id, 8, "Projector ID"
27
+ # unsigned :m_code, 4, "Model code for projector"
28
+ # unsigned :len, 12, "Length of data in bytes"
29
+ # variable :data, "Packet data", :length => :len
30
+ # unsigned :checksum,8, "Checksum"
31
+ #
32
+ # checksum :checksum do |bytes|
33
+ # bytes[0..-2].inject{|sum, byte| sum + byte} & 255
34
+ # end
35
+ # end
36
+ #
37
+ # NECProjectorFormat.parse(buffer)
38
+ # # => [[<NECProjectorFormat>, <NECProjectorFormat>], rest]
39
+ #
40
+ # NECProjectorFormat.new(:id1 => 0x44, :id2 => 2, :p_id => 0, :m_code => 0, :len => 5, :data => "hello").to_s.bytes.to_a
41
+ # # => [0x44, 0x2, 0x05, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x5F]
42
+ class Structure < Object
43
+ class << self
44
+ private
45
+ #@private
46
+ FIELD_TYPES = {
47
+ :unsigned => {
48
+ :validator => proc{|val, size, options| val.is_a?(Fixnum) && val < 2**size},
49
+ :unpack => proc {|s, offset, length, options|
50
+ number = 0
51
+ s_iter = s.bytes
52
+ byte = 0
53
+ # advance the iterator by the number of whole or partial bytes in the offset (offset div 8)
54
+ ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
55
+
56
+ length.times{|bit|
57
+ byte = s_iter.next if offset % 8 == 0
58
+ src_bit = (7-offset%8)
59
+ number |= (1 << (length-1-bit)) if (byte & (1 << src_bit)) > 0
60
+ #puts "Reading: #{src_bit} from #{"%08b" % byte} => #{(byte & (1 << src_bit)) > 0 ? 1 : 0}"
61
+ offset += 1
62
+ }
63
+ number
64
+ }
65
+ },
66
+ :signed => {
67
+ :validator => proc{|val, size, options| val.is_a?(Fixnum) && val.abs < 2**(size-1)},
68
+ :unpack => proc{|s, offset, length, options|
69
+ number = 0
70
+ s_iter = s.bytes
71
+ byte = 0
72
+ # advance the iterator by the number of whole bytes in the offset (offset div 8)
73
+ ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
74
+ # is this a positive number? yes if the most significant bit is 0
75
+ byte = s_iter.next if offset % 8 == 0
76
+ pos = byte & (1 << 7 - offset%8) == 0
77
+ #puts "String: #{s.bytes.to_a.collect{|x| "%08b" % x}.join(" ")}"
78
+ #puts "Byte: #{"%08b" % byte}, offset: #{offset}"
79
+
80
+ length.times{|bit|
81
+ byte = s_iter.next if offset % 8 == 0 && bit > 7
82
+ src_bit = (7-offset%8)
83
+ number |= (1 << (length-1-bit)) if ((byte & (1 << src_bit)) > 0) ^ (!pos)
84
+ offset += 1
85
+ }
86
+ #puts "Pos #{pos}, number: #{number}"
87
+ pos ? number : -number-1
88
+ }
89
+ },
90
+ :variable => {
91
+ :validator => proc{|val, size, options| val.is_a?(String)},
92
+ :unpack => proc{|s, offset, length, options|
93
+ output = []
94
+ s_iter = s.bytes
95
+ byte = 0
96
+ # advance the iterator by the number of whole bytes in the
97
+ # offset (offset div 8)
98
+ ((offset.to_f/8).ceil).times{|i| byte = s_iter.next}
99
+ length.times{|bit|
100
+ byte = s_iter.next if offset % 8 == 0
101
+ output << 0 if bit % 8 == 0
102
+
103
+ src_bit = (7-offset%8)
104
+ output[-1] |= (1 << (7-bit%8)) if (byte & (1 << src_bit)) > 0
105
+ offset += 1
106
+ }
107
+ output.pack("c*")
108
+ }
109
+ }
110
+ }
111
+ FIELD_TYPES.each{|kind, field|
112
+ define_method kind do |name, size, description, *options|
113
+ field(kind, name, size, description, field[:validator], options[0])
114
+ end
115
+ }
116
+
117
+ define_method :variable do |name, description, *options|
118
+ field(:variable, name, nil, description, FIELD_TYPES[:variable][:validator], options[0])
119
+ end
120
+
121
+ public
122
+ # Sets the checksum field. Setting a checksum field alters the functionality
123
+ # in several ways: the checksum is automatically calculated and set, and #parse
124
+ # will only consider a bitstring to be a valid instance of the structure if it
125
+ # has a checksum appropriate to its data.
126
+ # @param field [Symbol] the field that contains the checksum data
127
+ # @yield [bytes] block that should calculate the checksum given bytes, which is
128
+ # an array of bytes representing the full structure, with the checksum field
129
+ # set to 0
130
+ def checksum field, &block
131
+ @_checksum_field = [field, block]
132
+ self.class_eval %{
133
+ def #{field}
134
+ calculate_checksum unless @_calculating_checksum || @_checksum_cached
135
+ @__#{field}
136
+ end
137
+ }
138
+ end
139
+
140
+ # A list of the fields in the class
141
+ def fields; @_fields; end
142
+
143
+ # The checksum field
144
+ def checksum_field; @_checksum_field; end
145
+
146
+ # Determines whether a string is a valid message
147
+ # @param string [String] a binary string to be tested
148
+ # @return [Boolean] whether the string is in fact a valid message
149
+ def valid_message? string
150
+ !!from_string(string)[0]
151
+ end
195
152
 
196
- private
197
- def field kind, name, size, description, validator, options
198
- @_fields ||= []
199
- @_fields << [kind, name, size, description, options]
200
- self.class_eval do
201
- define_method "#{name}=" do |val|
202
- raise FieldValueException unless validator.call(val, size, options)
203
- self.instance_variable_set("@__#{name}", val)
204
- @_checksum_cached = false
205
- end
206
- end
207
- unless checksum_field && checksum_field[0] == name
208
- self.class_eval %{
209
- def #{name}
210
- @__#{name}
211
- end
212
- }
213
- end
214
- end
215
- end
216
-
217
- # Creates a new instance of the class. You can pass in field names to initialize to
218
- # set their values.
219
- # @example
220
- # MyStructure.new(:field1 => 44, :field2 => 0x70, :field3 => "hello")
221
- def initialize(values={})
222
- values.each{|key, value|
223
- self.send "#{key}=", value
224
- }
225
- @_checksum_cached = false
226
- end
227
-
228
- # Returns a binary string representation of the structure according to the fields defined
229
- # and their current values.
230
- # @return [String] bit string representing struct
231
- def to_s
232
- if self.class.checksum_field && !@_checksum_cached
233
- self.calculate_checksum
234
- end
235
- to_s_without_checksum
236
- end
237
-
238
- # Calculates and sets the checksum bit according to the checksum field defined by #checksum
239
- def calculate_checksum
240
- if self.class.checksum_field
241
- @_calculating_checksum = true
242
- self.send("#{self.class.checksum_field[0]}=", 0)
243
- checksum = self.class.checksum_field[1].call(self.to_s_without_checksum.bytes.to_a)
244
- self.send("#{self.class.checksum_field[0]}=", checksum)
245
- @_checksum_cached = true
246
- @_calculating_checksum = false
247
- end
248
- end
249
-
250
- protected
251
- # Returns the input number with the specified bit set to the specified value
252
- # @param byte [Fixnum] Number to be modified
253
- # @param bit [Fixnum] Bit number to be set
254
- # @param value [Fixnum: {0, 1}] Value to set (either 0 or 1)
255
- # @return [Fixnum] byte with bit set to value
256
- def set_bit(byte, bit, value)
257
- #TODO: this can probably be made more efficient
258
- byte & (1<<bit) > 0 == value > 0 ? byte : byte ^ (1<<bit)
259
- end
260
-
261
- # Returns the value at position bit of byte
262
- # @param number [Fixnum] Number to be queried
263
- # @param bit [Fixnum] bit of interest
264
- # @return [Fixnum: {0, 1}] 0 or 1, depending on the value of the bit at position bit of number
265
- def get_bit(number, bit)
266
- number & (1<<bit) > 0 ? 1 : 0
267
- end
268
-
269
- def to_s_without_checksum
270
- offset = 0
271
- buffer = []
272
- # This method works by iterating through each bit of each field and setting the bits in
273
- # the current output byte appropriately.
274
- self.class.fields.each{|field|
275
- kind, name, size, description, options = field
276
- data = self.send(name)
277
- options ||= {}
278
- case kind
279
- when :variable
280
- data ||= ""
281
- size = options[:length] && self.send(options[:length]) ? self.send(options[:length]) : data.size
282
- size *= 8 if options[:unit] == :byte
283
- byte_iter = data.bytes
284
- if offset % 8 == 0
285
- buffer += data.bytes.to_a + [0] * (size - data.size)
286
- else
287
- size.times{|i|
288
- byte = byte_iter.next rescue 0
289
- 8.times{|bit|
290
- buffer << 0 if offset % 8 == 0
291
- buffer[-1] |= get_bit(byte, 7-bit) << 7-(offset % 8)
292
- offset += 1
293
- }
294
- }
295
- end
296
- else
297
- data ||= 0
298
- size.times do |bit|
299
- buffer << 0 if offset % 8 == 0
300
- buffer[-1] |= get_bit(data, size-bit-1) << 7-(offset % 8)
301
- offset += 1
302
- end
303
- end
304
- }
305
- buffer.pack("c*")
306
- end
307
- end
308
-
309
- end
153
+ # Determines whether a string is at least the minimum correct
154
+ # length and matches the checksum. This method is less correct
155
+ # than valid_message? but considerably faster.
156
+ # @param string [String] a binary string to be tested, not
157
+ # including a checksum region if applicable
158
+ # @return [Boolean] whether the string is likely to be a valid
159
+ # message
160
+ def maybe_valid? string
161
+ if string.size >= @_size_sum
162
+ if self.class.checksum_field
163
+ checksum = self.class.checksum_field[1].call(string)
164
+ else
165
+ return true
166
+ end
167
+ end
168
+ return false
169
+ end
170
+
171
+ # Parses a message from the binary string assuming that the
172
+ # message starts at the first byte of the string
173
+ # @param string [String] a binary string to be interpreted
174
+ # @return [Array<Structure, string>] a pair with the first
175
+ # element being a structure object with the data from the
176
+ # input string (or nil if not a valid structure) and the
177
+ # second being the left-over bytes from the string (those
178
+ # after the message or the entire string if no valid message
179
+ # was found)
180
+ def from_string(string)
181
+ message = self.new
182
+ iter = 0
183
+ checksum = nil
184
+ fields.each{|field|
185
+ kind, name, size, description, options = field
186
+ options ||= {}
187
+ size = (kind == :variable) ? message.send(options[:length]) : size
188
+ size *= 8 if options[:unit] == :byte
189
+ begin
190
+ value = FIELD_TYPES[kind][:unpack].call(string, iter, size, options)
191
+ message.send("#{name}=", value)
192
+ checksum = value if checksum_field && name == checksum_field[0]
193
+ rescue StopIteration, FieldValueException => e
194
+ return [nil, string]
195
+ end
196
+ iter += size
197
+ }
198
+ # if there's a checksum, make sure the provided one is valid
199
+ return [nil, string] unless message.checksum == checksum if checksum_field
200
+ [message, string[((iter/8.0).ceil)..-1]]
201
+ end
202
+
203
+ # Parses out all of the messages in a given string assuming that
204
+ # the first message starts at the first byte, and there are no
205
+ # bytes between messages (though messages are not allowed to
206
+ # span bytes; i.e., all messages must be byte-aligned).
207
+ # @param string [String] a binary string containing the messages
208
+ # to be parsed
209
+ # @return [Array<Array<Structure>, String>] a pair with the
210
+ # first element being an array of messages parsed out of the
211
+ # string and the second being whatever part of the string was
212
+ # left over after parsing.
213
+ def parse(string)
214
+ messages = []
215
+ last_message = true
216
+ while last_message
217
+ last_message, string = from_string(string)
218
+ #puts "Found message: #{last_message.to_s.bytes.to_a}, string=#{string.bytes.to_a.inspect}"
219
+ messages << last_message if last_message
220
+ end
221
+ [messages, string]
222
+ end
223
+
224
+ private
225
+ def field kind, name, size, description, validator, options
226
+ @_fields ||= []
227
+ @_fields << [kind, name, size, description, options]
228
+ @_size_sum = @_fields.reduce(0){|acc, f|
229
+ f[0] == :variable ? acc : acc + f[2]
230
+ }/8
231
+ self.class_eval do
232
+ define_method "#{name}=" do |val|
233
+ raise FieldValueException unless validator.call(val, size, options)
234
+ self.instance_variable_set("@__#{name}", val)
235
+ @_checksum_cached = false
236
+ end
237
+ end
238
+ unless checksum_field && checksum_field[0] == name
239
+ self.class_eval %{
240
+ def #{name}
241
+ @__#{name}
242
+ end
243
+ }
244
+ end
245
+ end
246
+ end
247
+
248
+ # Creates a new instance of the class. You can pass in field names to initialize to
249
+ # set their values.
250
+ # @example
251
+ # MyStructure.new(:field1 => 44, :field2 => 0x70, :field3 => "hello")
252
+ def initialize(values={})
253
+ values.each{|key, value|
254
+ self.send "#{key}=", value
255
+ }
256
+ @_checksum_cached = false
257
+ end
258
+
259
+ # Returns a binary string representation of the structure according to the fields defined
260
+ # and their current values.
261
+ # @return [String] bit string representing struct
262
+ def to_s
263
+ if self.class.checksum_field && !@_checksum_cached
264
+ self.calculate_checksum
265
+ end
266
+ to_s_without_checksum
267
+ end
268
+
269
+ # Calculates and sets the checksum bit according to the checksum field defined by #checksum
270
+ def calculate_checksum
271
+ if self.class.checksum_field
272
+ @_calculating_checksum = true
273
+ self.send("#{self.class.checksum_field[0]}=", 0)
274
+ checksum = self.class.checksum_field[1].call(self.to_s_without_checksum.bytes.to_a)
275
+ self.send("#{self.class.checksum_field[0]}=", checksum)
276
+ @_checksum_cached = true
277
+ @_calculating_checksum = false
278
+ end
279
+ end
280
+
281
+ protected
282
+ # Returns the input number with the specified bit set to the specified value
283
+ # @param byte [Fixnum] Number to be modified
284
+ # @param bit [Fixnum] Bit number to be set
285
+ # @param value [Fixnum: {0, 1}] Value to set (either 0 or 1)
286
+ # @return [Fixnum] byte with bit set to value
287
+ def set_bit(byte, bit, value)
288
+ #TODO: this can probably be made more efficient
289
+ byte & (1 << bit) > 0 == value > 0 ? byte : byte ^ (1 << bit)
290
+ end
291
+
292
+ # Returns the value at position bit of byte
293
+ # @param number [Fixnum] Number to be queried
294
+ # @param bit [Fixnum] bit of interest
295
+ # @return [Fixnum: {0, 1}] 0 or 1, depending on the value of the bit at position bit of number
296
+ def get_bit(number, bit)
297
+ number & (1 << bit) > 0 ? 1 : 0
298
+ end
299
+
300
+ def to_s_without_checksum
301
+ offset = 0
302
+ buffer = []
303
+ # This method works by iterating through each bit of each field
304
+ # and setting the bits in the current output byte appropriately.
305
+ self.class.fields.each{|field|
306
+ kind, name, size, description, options = field
307
+ data = self.send(name)
308
+ options ||= {}
309
+ case kind
310
+ when :variable
311
+ data ||= ""
312
+ size = options[:length] && self.send(options[:length]) ? self.send(options[:length]) : data.size
313
+ size /= 8 if options[:unit] == :bit
314
+ byte_iter = data.bytes
315
+ if offset % 8 == 0
316
+ buffer += data.bytes.to_a + [0] * (size - data.size)
317
+ else
318
+ size.times{|i|
319
+ byte = byte_iter.next rescue 0
320
+ 8.times{|bit|
321
+ buffer << 0 if offset % 8 == 0
322
+ buffer[-1] |= get_bit(byte, 7-bit) << 7-(offset % 8)
323
+ offset += 1
324
+ }
325
+ }
326
+ end
327
+ else
328
+ data ||= 0
329
+ size.times do |bit|
330
+ buffer << 0 if offset % 8 == 0
331
+ buffer[-1] |= get_bit(data, size-bit-1) << 7-(offset % 8)
332
+ offset += 1
333
+ end
334
+ end
335
+ }
336
+ buffer.pack("c*")
337
+ end
338
+ end
339
+ end
data/rubybits.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{rubybits}
8
- s.version = "0.1.0"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Micah Wylde"]
12
- s.date = %q{2010-11-27}
12
+ s.date = %q{2011-03-15}
13
13
  s.description = %q{RubyBits simplifies the task of parsing and generating binary strings in particular formats.}
14
14
  s.email = %q{mwylde@wesleyan.edu}
15
15
  s.extra_rdoc_files = [
@@ -33,7 +33,7 @@ Gem::Specification.new do |s|
33
33
  s.homepage = %q{http://github.com/mwylde/rubybits}
34
34
  s.licenses = ["MIT"]
35
35
  s.require_paths = ["lib"]
36
- s.rubygems_version = %q{1.3.7}
36
+ s.rubygems_version = %q{1.5.2}
37
37
  s.summary = %q{A library that makes dealing with bit strings and binary formats easier, inspired by BitStruct}
38
38
  s.test_files = [
39
39
  "spec/rubybits_spec.rb",
@@ -41,7 +41,6 @@ Gem::Specification.new do |s|
41
41
  ]
42
42
 
43
43
  if s.respond_to? :specification_version then
44
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
45
44
  s.specification_version = 3
46
45
 
47
46
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
@@ -154,9 +154,9 @@ describe "Structure" do
154
154
  it "should allow variable length fields whose lengths are specified by another field" do
155
155
  class TestFormat11 < RubyBits::Structure
156
156
  unsigned :field1, 8, "Field1"
157
- unsigned :field2, 4, "Field2"
158
- unsigned :field3, 4, "Flag"
159
- variable :text, "text", :length => :field2
157
+ unsigned :field2, 8, "Field2"
158
+ unsigned :field3, 8, "Flag"
159
+ variable :text, "text", :length => :field2, :unit => :bit
160
160
  unsigned :checksum, 8, "Checksum (sum of all previous fields)"
161
161
 
162
162
  checksum :checksum do |bytes|
@@ -164,13 +164,36 @@ describe "Structure" do
164
164
  end
165
165
  end
166
166
 
167
- tf = TestFormat11.new(:field1 => 0x77, :field2 => 0x04, :field3 => 0x0F, :text => "abc")
168
- checksum = (0x77 + 0x4F + "abc".bytes.to_a.reduce(:+)) & 255
167
+ tf = TestFormat11.new(:field1 => 0x77, :field2 => 32, :field3 => 0x0F, :text => "abc")
168
+ checksum = (0x77 + 32 + 0x0F + "abc".bytes.to_a.reduce(:+)) & 255
169
169
  tf.checksum.should == checksum
170
170
 
171
- tf.to_s.bytes.to_a.should == [0x77, 0x4F, 0x61, 0x62, 0x63, 0, checksum]
171
+ tf.to_s.bytes.to_a.should == [0x77, 32, 0x0F, 0x61, 0x62, 0x63, 0, checksum]
172
172
  end
173
-
173
+
174
+ it "should allow variable length fields whose lengths are specified by another field in bytes" do
175
+ class TestFormat11b < RubyBits::Structure
176
+ unsigned :field1, 8, "Field1"
177
+ unsigned :field2, 8, "Field2"
178
+ unsigned :field3, 8, "Field3"
179
+ unsigned :field4, 4, "Field4"
180
+ unsigned :len, 12, "Length"
181
+ variable :text, "text", :length => :len, :unit => :byte
182
+ unsigned :checksum, 8, "Checksum (sum of all previous fields)"
183
+
184
+ checksum :checksum do |bytes|
185
+ bytes[0..-2].inject{|sum, byte| sum += byte} & 255
186
+ end
187
+ end
188
+
189
+ tf = TestFormat11b.new(:field1 => 0x77, :field2 => 0x44, :field3 => 0x10, :field4 => 0xE, :len => 4, :text => "abc")
190
+ checksum = (0x77 + 0x44 + 0x10 + 0xE0 + 0x04 + "abc".bytes.to_a.reduce(:+)) & 255
191
+ tf.checksum.should == checksum
192
+
193
+ tf.to_s.bytes.to_a.should == [0x77, 0x44, 0x10, 0xE0, 0x4,
194
+ 97, 98, 99, 0, checksum]
195
+ end
196
+
174
197
  it "should fail when setting an invalid value for a field" do
175
198
  class TestFormat12 < RubyBits::Structure
176
199
  unsigned :field1, 8, "Field1"
@@ -295,4 +318,4 @@ describe "parsing" do
295
318
  string.should == " friend"
296
319
  end
297
320
 
298
- end
321
+ end
metadata CHANGED
@@ -1,12 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubybits
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 1
8
- - 0
9
- version: 0.1.0
4
+ prerelease:
5
+ version: 0.2.0
10
6
  platform: ruby
11
7
  authors:
12
8
  - Micah Wylde
@@ -14,7 +10,7 @@ autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
12
 
17
- date: 2010-11-27 00:00:00 -05:00
13
+ date: 2011-03-15 00:00:00 -04:00
18
14
  default_executable:
19
15
  dependencies:
20
16
  - !ruby/object:Gem::Dependency
@@ -24,10 +20,6 @@ dependencies:
24
20
  requirements:
25
21
  - - ~>
26
22
  - !ruby/object:Gem::Version
27
- segments:
28
- - 2
29
- - 1
30
- - 0
31
23
  version: 2.1.0
32
24
  type: :development
33
25
  prerelease: false
@@ -39,10 +31,6 @@ dependencies:
39
31
  requirements:
40
32
  - - ~>
41
33
  - !ruby/object:Gem::Version
42
- segments:
43
- - 0
44
- - 6
45
- - 0
46
34
  version: 0.6.0
47
35
  type: :development
48
36
  prerelease: false
@@ -54,10 +42,6 @@ dependencies:
54
42
  requirements:
55
43
  - - ~>
56
44
  - !ruby/object:Gem::Version
57
- segments:
58
- - 2
59
- - 0
60
- - 9
61
45
  version: 2.0.9
62
46
  type: :development
63
47
  prerelease: false
@@ -69,10 +53,6 @@ dependencies:
69
53
  requirements:
70
54
  - - ~>
71
55
  - !ruby/object:Gem::Version
72
- segments:
73
- - 1
74
- - 0
75
- - 0
76
56
  version: 1.0.0
77
57
  type: :development
78
58
  prerelease: false
@@ -84,10 +64,6 @@ dependencies:
84
64
  requirements:
85
65
  - - ~>
86
66
  - !ruby/object:Gem::Version
87
- segments:
88
- - 1
89
- - 5
90
- - 1
91
67
  version: 1.5.1
92
68
  type: :development
93
69
  prerelease: false
@@ -99,8 +75,6 @@ dependencies:
99
75
  requirements:
100
76
  - - ">="
101
77
  - !ruby/object:Gem::Version
102
- segments:
103
- - 0
104
78
  version: "0"
105
79
  type: :development
106
80
  prerelease: false
@@ -141,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
115
  requirements:
142
116
  - - ">="
143
117
  - !ruby/object:Gem::Version
144
- hash: 1583675054934852324
118
+ hash: 4608095597125751884
145
119
  segments:
146
120
  - 0
147
121
  version: "0"
@@ -150,13 +124,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
124
  requirements:
151
125
  - - ">="
152
126
  - !ruby/object:Gem::Version
153
- segments:
154
- - 0
155
127
  version: "0"
156
128
  requirements: []
157
129
 
158
130
  rubyforge_project:
159
- rubygems_version: 1.3.7
131
+ rubygems_version: 1.5.2
160
132
  signing_key:
161
133
  specification_version: 3
162
134
  summary: A library that makes dealing with bit strings and binary formats easier, inspired by BitStruct