rubybits 0.1.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/.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