rubybits 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.1.0"
10
+ gem "yard", "~> 0.6.0"
11
+ gem "bluecloth", "~> 2.0.9"
12
+ gem "bundler", "~> 1.0.0"
13
+ gem "jeweler", "~> 1.5.1"
14
+ gem "rcov", ">= 0"
15
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ bluecloth (2.0.9)
5
+ diff-lcs (1.1.2)
6
+ git (1.2.5)
7
+ jeweler (1.5.1)
8
+ bundler (~> 1.0.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ rake (0.8.7)
12
+ rcov (0.9.9)
13
+ rspec (2.1.0)
14
+ rspec-core (~> 2.1.0)
15
+ rspec-expectations (~> 2.1.0)
16
+ rspec-mocks (~> 2.1.0)
17
+ rspec-core (2.1.0)
18
+ rspec-expectations (2.1.0)
19
+ diff-lcs (~> 1.1.2)
20
+ rspec-mocks (2.1.0)
21
+ yard (0.6.3)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ bluecloth (~> 2.0.9)
28
+ bundler (~> 1.0.0)
29
+ jeweler (~> 1.5.1)
30
+ rcov
31
+ rspec (~> 2.1.0)
32
+ yard (~> 0.6.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Micah Wylde
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # RubyBits
2
+
3
+ RubyBits is a library that makes dealing with binary formats easier. In
4
+ particular, it provides the Structure class, which allows for easy parsing
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.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "rubybits"
16
+ gem.homepage = "http://github.com/mwylde/rubybits"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{A library that makes dealing with bit strings and binary formats easier, inspired by BitStruct}
19
+ gem.description = %Q{RubyBits simplifies the task of parsing and generating binary strings in particular formats.}
20
+ gem.email = "mwylde@wesleyan.edu"
21
+ gem.authors = ["Micah Wylde"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'yard'
43
+ YARD::Rake::YardocTask.new do |t|
44
+ t.options = ['--no-private']
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/rubybits.rb ADDED
@@ -0,0 +1,309 @@
1
+ # Provides various utilities for working with binary formats.
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
195
+
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
data/rubybits.gemspec ADDED
@@ -0,0 +1,71 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{rubybits}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Micah Wylde"]
12
+ s.date = %q{2010-11-27}
13
+ s.description = %q{RubyBits simplifies the task of parsing and generating binary strings in particular formats.}
14
+ s.email = %q{mwylde@wesleyan.edu}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.md",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/rubybits.rb",
29
+ "rubybits.gemspec",
30
+ "spec/rubybits_spec.rb",
31
+ "spec/spec_helper.rb"
32
+ ]
33
+ s.homepage = %q{http://github.com/mwylde/rubybits}
34
+ s.licenses = ["MIT"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.3.7}
37
+ s.summary = %q{A library that makes dealing with bit strings and binary formats easier, inspired by BitStruct}
38
+ s.test_files = [
39
+ "spec/rubybits_spec.rb",
40
+ "spec/spec_helper.rb"
41
+ ]
42
+
43
+ if s.respond_to? :specification_version then
44
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
45
+ s.specification_version = 3
46
+
47
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
48
+ s.add_development_dependency(%q<rspec>, ["~> 2.1.0"])
49
+ s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
50
+ s.add_development_dependency(%q<bluecloth>, ["~> 2.0.9"])
51
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
52
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
53
+ s.add_development_dependency(%q<rcov>, [">= 0"])
54
+ else
55
+ s.add_dependency(%q<rspec>, ["~> 2.1.0"])
56
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
57
+ s.add_dependency(%q<bluecloth>, ["~> 2.0.9"])
58
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
59
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
60
+ s.add_dependency(%q<rcov>, [">= 0"])
61
+ end
62
+ else
63
+ s.add_dependency(%q<rspec>, ["~> 2.1.0"])
64
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
65
+ s.add_dependency(%q<bluecloth>, ["~> 2.0.9"])
66
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
67
+ s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
68
+ s.add_dependency(%q<rcov>, [">= 0"])
69
+ end
70
+ end
71
+
@@ -0,0 +1,298 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Structure" do
4
+ it "should allow definition of a format with unsigned fields" do
5
+ class TestFormat1 < RubyBits::Structure
6
+ unsigned :field1, 8, "Field1"
7
+ unsigned :field2, 4, "Field2"
8
+ unsigned :flag, 1, "Flag"
9
+ unsigned :field3, 16, "Field3"
10
+ end
11
+ end
12
+
13
+ it "should allow accessing fields created" do
14
+ class TestFormat2 < RubyBits::Structure
15
+ unsigned :field1, 8, "Field1"
16
+ unsigned :field2, 4, "Field2"
17
+ unsigned :flag, 1, "Flag"
18
+ unsigned :field3, 16, "Field3"
19
+ end
20
+
21
+ tf = TestFormat2.new
22
+ tf.field1 = 0x40
23
+ tf.field1.should == 0x40
24
+ end
25
+
26
+ it "should allow initialization with data" do
27
+ class TestFormat3 < RubyBits::Structure
28
+ unsigned :field1, 8, "Field1"
29
+ unsigned :field2, 4, "Field2"
30
+ unsigned :field3, 4, "Flag"
31
+ unsigned :field4, 16, "Field3"
32
+ end
33
+
34
+ tf = TestFormat3.new(:field1 => 0x77, :field2 => 0x06, :field3 => 0x0F, :field4 => 0x726b)
35
+ tf.field1.should == 0x77
36
+ tf.field2.should == 0x06
37
+ tf.field3.should == 0x0F
38
+ tf.field4.should == 0x726b
39
+ end
40
+
41
+ it "should allow creation of bitstrings from structure spec" do
42
+ class TestFormat4 < RubyBits::Structure
43
+ unsigned :field1, 8, "Field1"
44
+ unsigned :field2, 4, "Field2"
45
+ unsigned :field3, 4, "Flag"
46
+ unsigned :field4, 16, "Field3"
47
+ end
48
+
49
+ tf = TestFormat4.new(:field1 => 0x77, :field2 => 0x06, :field3 => 0x0F, :field4 => 0x726b)
50
+ tf.to_s.should == "work"
51
+ end
52
+
53
+ it "should allow weird field sizes" do
54
+ class TestFormat5 < RubyBits::Structure
55
+ unsigned :field1, 5, "Field1"
56
+ unsigned :field2, 3, "Field2"
57
+ unsigned :field3, 6, "Flag"
58
+ unsigned :field4, 4, "Field3"
59
+ unsigned :field5, 11, "Field5"
60
+ unsigned :field6, 2, "Field6"
61
+ end
62
+
63
+ tf = TestFormat5.new(:field1 => 0b11010, :field2 => 0b001, :field3 => 0b101010, :field4 => 0b1011, :field5 => 0b11101010001, :field6 => 0b11)
64
+ tf.to_s.bytes.to_a.should == [0b11010001, 0b10101010, 0b11111010, 0b10001110]
65
+ end
66
+
67
+ it "should allow signed integers" do
68
+ class TestFormat6 < RubyBits::Structure
69
+ signed :field1, 8, "Field1"
70
+ signed :field2, 4, "Field2"
71
+ signed :field3, 4, "Field3"
72
+ end
73
+
74
+ tf = TestFormat6.new(:field1 => -10, :field2 => -4, :field3 => -7)
75
+ tf.to_s.bytes.to_a.should == [0b11110110, 0b11001001]
76
+ end
77
+
78
+ it "should calculate checksum correctly" do
79
+ class TestFormat7 < RubyBits::Structure
80
+ unsigned :field1, 8, "Field1"
81
+ unsigned :field2, 4, "Field2"
82
+ unsigned :field3, 4, "Flag"
83
+ unsigned :field4, 16, "Field3"
84
+ unsigned :checksum, 8, "Checksum (sum of all previous fields)"
85
+
86
+ checksum :checksum do |bytes|
87
+ bytes[0..-2].inject{|sum, byte| sum += byte} & 255
88
+ end
89
+ end
90
+
91
+ tf = TestFormat7.new(:field1 => 0x77, :field2 => 0x06, :field3 => 0x0F, :field4 => 0x726b)
92
+ tf.to_s.bytes.to_a.should == "work".bytes.to_a << ((0x77 + 0x6F + 0x72 + 0x6B) & 255)
93
+ end
94
+
95
+ it "should calculate checksum when accessed" do
96
+ class TestFormat8 < RubyBits::Structure
97
+ unsigned :field1, 8, "Field1"
98
+ unsigned :field2, 4, "Field2"
99
+ unsigned :field3, 4, "Flag"
100
+ unsigned :field4, 16, "Field3"
101
+ unsigned :checksum, 8, "Checksum (sum of all previous fields)"
102
+
103
+ checksum :checksum do |bytes|
104
+ bytes[0..-2].inject{|sum, byte| sum += byte} & 255
105
+ end
106
+ end
107
+
108
+ tf = TestFormat8.new(:field1 => 0x77, :field2 => 0x06, :field3 => 0x0F, :field4 => 0x726b)
109
+ tf.checksum.should == (0x77 + 0x6F + 0x72 + 0x6B) & 255
110
+ end
111
+
112
+ it "should allow variable length fields" do
113
+ class TestFormat9 < RubyBits::Structure
114
+ unsigned :field1, 8, "Field1"
115
+ unsigned :field2, 4, "Field2"
116
+ unsigned :field3, 4, "Flag"
117
+ variable :field4, "text"
118
+ unsigned :checksum, 8, "Checksum (sum of all previous fields)"
119
+
120
+ checksum :checksum do |bytes|
121
+ bytes[0..-2].inject{|sum, byte| sum += byte} & 255
122
+ end
123
+ end
124
+
125
+ tf = TestFormat9.new(:field1 => 0x77, :field2 => 0x06, :field3 => 0x0F, :field4 => "hello")
126
+ checksum = (0x77 + 0x6F + "hello".bytes.to_a.reduce(:+)) & 255
127
+ tf.checksum.should == checksum
128
+
129
+ tf.to_s.bytes.to_a.should == [0x77, 0x6F] + "hello".bytes.to_a << checksum
130
+ end
131
+
132
+ it "should allow variable length fields that are not byte aligned" do
133
+ class TestFormat10 < RubyBits::Structure
134
+ unsigned :field1, 8, "Field1"
135
+ unsigned :field2, 4, "Field2"
136
+ unsigned :field3, 4, "Flag"
137
+ unsigned :field4, 6, "Not byte aligned"
138
+ variable :text, "text"
139
+ unsigned :checksum, 8, "Checksum (sum of all previous fields)"
140
+
141
+ checksum :checksum do |bytes|
142
+ bytes[0..-2].inject{|sum, byte| sum += byte} & 255
143
+ end
144
+ end
145
+
146
+ string = [119, 111, 201, 133, 137, 140]
147
+ tf = TestFormat10.new(:field1 => 0x77, :field2 => 0x06, :field3 => 0x0F, :field4 => 0x32, :text => "abc")
148
+ checksum = string.reduce(:+) & 255
149
+ tf.checksum.should == checksum
150
+
151
+ tf.to_s.bytes.to_a.should == [119, 111, 201, 133, 137, 141, 36]
152
+ end
153
+
154
+ it "should allow variable length fields whose lengths are specified by another field" do
155
+ class TestFormat11 < RubyBits::Structure
156
+ unsigned :field1, 8, "Field1"
157
+ unsigned :field2, 4, "Field2"
158
+ unsigned :field3, 4, "Flag"
159
+ variable :text, "text", :length => :field2
160
+ unsigned :checksum, 8, "Checksum (sum of all previous fields)"
161
+
162
+ checksum :checksum do |bytes|
163
+ bytes[0..-2].inject{|sum, byte| sum += byte} & 255
164
+ end
165
+ end
166
+
167
+ tf = TestFormat11.new(:field1 => 0x77, :field2 => 0x04, :field3 => 0x0F, :text => "abc")
168
+ checksum = (0x77 + 0x4F + "abc".bytes.to_a.reduce(:+)) & 255
169
+ tf.checksum.should == checksum
170
+
171
+ tf.to_s.bytes.to_a.should == [0x77, 0x4F, 0x61, 0x62, 0x63, 0, checksum]
172
+ end
173
+
174
+ it "should fail when setting an invalid value for a field" do
175
+ class TestFormat12 < RubyBits::Structure
176
+ unsigned :field1, 8, "Field1"
177
+ unsigned :field2, 4, "Field2"
178
+ unsigned :field3, 16, "Flag"
179
+ variable :text, "text"
180
+ end
181
+
182
+ expect {TestFormat12.new(:field1 => 257, :field2 => 0x4, :field3 => 500)}.to raise_error(RubyBits::FieldValueException)
183
+ tf = TestFormat12.new
184
+ expect {tf.field2 = 0x44}.to raise_error(RubyBits::FieldValueException)
185
+ expect {tf.field3 = 0x44122}.to raise_error(RubyBits::FieldValueException)
186
+ expect {tf.text = 55}.to raise_error(RubyBits::FieldValueException)
187
+ end
188
+ end
189
+
190
+ describe "parsing" do
191
+ it "should correctly determine a valid message" do
192
+ class TestFormat13 < RubyBits::Structure
193
+ unsigned :field1, 8, "Field1"
194
+ unsigned :field2, 4, "Field2"
195
+ unsigned :field3, 4, "Field3"
196
+ signed :field4, 8, "Field4"
197
+ signed :field5, 8, "Field5"
198
+ unsigned :field6, 16, "Short field"
199
+ end
200
+ TestFormat13.valid_message?([0x34, 0x41, 0b11001001, 0x24, 0x44, 0x55].pack("c*")).should == true
201
+ TestFormat13.valid_message?([0x34, 0x41, 0b11001001, 0x24, 0x44, 0x55, 0x44].pack("c*")).should == true
202
+ TestFormat13.valid_message?([0x11, 0x11, 0x44, 0x24, 0x11].pack("c*")).should == false
203
+ TestFormat13.valid_message?("").should == false
204
+
205
+ tf, string = TestFormat13.from_string([0x34, 0x41, 0b11001001, 0b00110011, 0x55, 0x11].pack("c*") + "ab")
206
+ tf.field1.should == 0x34
207
+ tf.field2.should == 0x04
208
+ tf.field3.should == 0x01
209
+ tf.field4.should == -55
210
+ tf.field5.should == 0b00110011
211
+ tf.field6.should == 0x5511
212
+ string.should == "ab"
213
+ end
214
+ it "should correctly determine a valid message with variable length fields" do
215
+ class TestFormat14 < RubyBits::Structure
216
+ unsigned :field1, 8, "Field1"
217
+ unsigned :size, 4, "Length"
218
+ unsigned :field3, 4, "Field3"
219
+ variable :text, "text", :length => :size, :unit => :byte
220
+ end
221
+ TestFormat14.valid_message?([0x44, 0x3F].pack("cc") + "abc").should == true
222
+ TestFormat14.valid_message?([0x33, 0x1C].pack("cc") + "ab").should == true
223
+ TestFormat14.valid_message?([0x11, 0x5C].pack("cc") + "abc").should == false
224
+
225
+ tf, string = TestFormat14.from_string([0x34, 0x3F].pack("c*") + "abcdefg")
226
+ tf.field1.should == 0x34
227
+ tf.size.should == 0x03
228
+ tf.field3.should == 0x0F
229
+ tf.text.should == "abc"
230
+ string.should == "defg"
231
+ end
232
+ it "should correctly determine a valid message with variable length fields and checksum" do
233
+ class TestFormat15 < RubyBits::Structure
234
+ unsigned :field1, 8, "Field1"
235
+ unsigned :size, 4, "size"
236
+ variable :text, "text", :length => :size, :unit => :byte
237
+ unsigned :checksum, 8, "checksum"
238
+
239
+ checksum :checksum do |bytes|
240
+ bytes.reduce(:+) & 255
241
+ end
242
+ end
243
+
244
+ TestFormat15.valid_message?([0x5C, 0x36, 0x16, 0x26, 0x3F, 0xE0].pack("c*")).should == true
245
+ TestFormat15.valid_message?([0x5C, 0x36, 0x16, 0x26, 0x3F, 0xD0].pack("c*")).should == false
246
+ TestFormat15.valid_message?([0x5C, 0x36, 0x16, 0x26, 0x30, 0xE0].pack("c*")).should == false
247
+
248
+ tf, string = TestFormat15.from_string([0x5C, 0x36, 0x16, 0x26, 0x3F, 0xE0].pack("c*") + "BC")
249
+ tf.field1.should == 0x5C
250
+ tf.size.should == 3
251
+ tf.text.should == "abc"
252
+ tf.checksum.should == 254
253
+ string.should == "BC"
254
+ end
255
+ it "should parse fix-width format" do
256
+ class TestFormat16 < RubyBits::Structure
257
+ unsigned :field1, 8, "Field1"
258
+ unsigned :field2, 4, "Field2"
259
+ unsigned :field3, 4, "Field3"
260
+ signed :field4, 8, "Field4"
261
+ end
262
+
263
+ messages, string = TestFormat16.parse([0x34, 0x41, 0b11001001, 0x55, 0xCF, 0b00110110].pack("c*") + "ab")
264
+ messages[0].field1.should == 0x34
265
+ messages[0].field2.should == 0x04
266
+ messages[0].field3.should == 0x01
267
+ messages[0].field4.should == -55
268
+
269
+ messages[1].field1.should == 0x55
270
+ messages[1].field2.should == 0xC
271
+ messages[1].field3.should == 0xF
272
+ messages[1].field4.should == 0b00110110
273
+
274
+ string.should == "ab"
275
+ end
276
+ it "should parse variable-width format" do
277
+ class TestFormat17 < RubyBits::Structure
278
+ unsigned :field1, 8, "Field1"
279
+ unsigned :size, 4, "Length"
280
+ unsigned :field3, 4, "Field3"
281
+ variable :text, "text", :length => :size, :unit => :byte
282
+ end
283
+
284
+ messages, string = TestFormat17.parse([0x44, 0x24].pack("c*") + "hi" + [0x55, 0x62].pack("c*") + "hello! friend")
285
+ messages[0].field1.should == 0x44
286
+ messages[0].size.should == 0x02
287
+ messages[0].field3.should == 0x04
288
+ messages[0].text.should == "hi"
289
+
290
+ messages[1].field1.should == 0x55
291
+ messages[1].size.should == 0x6
292
+ messages[1].field3.should == 0x2
293
+ messages[1].text.should == "hello!"
294
+
295
+ string.should == " friend"
296
+ end
297
+
298
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'rubybits'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,165 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubybits
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Micah Wylde
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-11-27 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 1
30
+ - 0
31
+ version: 2.1.0
32
+ type: :development
33
+ prerelease: false
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: yard
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ - 6
45
+ - 0
46
+ version: 0.6.0
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: bluecloth
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 2
59
+ - 0
60
+ - 9
61
+ version: 2.0.9
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: bundler
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ~>
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 1
74
+ - 0
75
+ - 0
76
+ version: 1.0.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ name: jeweler
82
+ requirement: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ segments:
88
+ - 1
89
+ - 5
90
+ - 1
91
+ version: 1.5.1
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ name: rcov
97
+ requirement: &id006 !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ segments:
103
+ - 0
104
+ version: "0"
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: *id006
108
+ description: RubyBits simplifies the task of parsing and generating binary strings in particular formats.
109
+ email: mwylde@wesleyan.edu
110
+ executables: []
111
+
112
+ extensions: []
113
+
114
+ extra_rdoc_files:
115
+ - LICENSE.txt
116
+ - README.md
117
+ files:
118
+ - .document
119
+ - .rspec
120
+ - Gemfile
121
+ - Gemfile.lock
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - VERSION
126
+ - lib/rubybits.rb
127
+ - rubybits.gemspec
128
+ - spec/rubybits_spec.rb
129
+ - spec/spec_helper.rb
130
+ has_rdoc: true
131
+ homepage: http://github.com/mwylde/rubybits
132
+ licenses:
133
+ - MIT
134
+ post_install_message:
135
+ rdoc_options: []
136
+
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ hash: 1583675054934852324
145
+ segments:
146
+ - 0
147
+ version: "0"
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ segments:
154
+ - 0
155
+ version: "0"
156
+ requirements: []
157
+
158
+ rubyforge_project:
159
+ rubygems_version: 1.3.7
160
+ signing_key:
161
+ specification_version: 3
162
+ summary: A library that makes dealing with bit strings and binary formats easier, inspired by BitStruct
163
+ test_files:
164
+ - spec/rubybits_spec.rb
165
+ - spec/spec_helper.rb