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 +26 -2
- data/VERSION +1 -1
- data/lib/rubybits.rb +336 -306
- data/rubybits.gemspec +3 -4
- data/spec/rubybits_spec.rb +31 -8
- metadata +5 -33
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
|
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.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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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.
|
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{
|
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.
|
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
|
data/spec/rubybits_spec.rb
CHANGED
@@ -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,
|
158
|
-
unsigned :field3,
|
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 =>
|
168
|
-
checksum = (0x77 +
|
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,
|
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:
|
5
|
-
|
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:
|
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:
|
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.
|
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
|