jbangert-bindata 1.5.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/.gitignore +1 -0
- data/BSDL +22 -0
- data/COPYING +52 -0
- data/ChangeLog.rdoc +204 -0
- data/Gemfile +2 -0
- data/INSTALL +11 -0
- data/NEWS.rdoc +164 -0
- data/README.md +54 -0
- data/Rakefile +13 -0
- data/bindata.gemspec +31 -0
- data/doc/manual.haml +407 -0
- data/doc/manual.md +1649 -0
- data/examples/NBT.txt +149 -0
- data/examples/gzip.rb +161 -0
- data/examples/ip_address.rb +22 -0
- data/examples/list.rb +124 -0
- data/examples/nbt.rb +178 -0
- data/lib/bindata.rb +33 -0
- data/lib/bindata/alignment.rb +83 -0
- data/lib/bindata/array.rb +335 -0
- data/lib/bindata/base.rb +388 -0
- data/lib/bindata/base_primitive.rb +214 -0
- data/lib/bindata/bits.rb +87 -0
- data/lib/bindata/choice.rb +216 -0
- data/lib/bindata/count_bytes_remaining.rb +35 -0
- data/lib/bindata/deprecated.rb +50 -0
- data/lib/bindata/dsl.rb +312 -0
- data/lib/bindata/float.rb +80 -0
- data/lib/bindata/int.rb +184 -0
- data/lib/bindata/io.rb +274 -0
- data/lib/bindata/lazy.rb +105 -0
- data/lib/bindata/offset.rb +91 -0
- data/lib/bindata/params.rb +135 -0
- data/lib/bindata/primitive.rb +135 -0
- data/lib/bindata/record.rb +110 -0
- data/lib/bindata/registry.rb +92 -0
- data/lib/bindata/rest.rb +35 -0
- data/lib/bindata/sanitize.rb +290 -0
- data/lib/bindata/skip.rb +48 -0
- data/lib/bindata/string.rb +145 -0
- data/lib/bindata/stringz.rb +96 -0
- data/lib/bindata/struct.rb +388 -0
- data/lib/bindata/trace.rb +94 -0
- data/lib/bindata/version.rb +3 -0
- data/setup.rb +1585 -0
- data/spec/alignment_spec.rb +61 -0
- data/spec/array_spec.rb +331 -0
- data/spec/base_primitive_spec.rb +238 -0
- data/spec/base_spec.rb +376 -0
- data/spec/bits_spec.rb +163 -0
- data/spec/choice_spec.rb +263 -0
- data/spec/count_bytes_remaining_spec.rb +38 -0
- data/spec/deprecated_spec.rb +31 -0
- data/spec/example.rb +21 -0
- data/spec/float_spec.rb +37 -0
- data/spec/int_spec.rb +216 -0
- data/spec/io_spec.rb +352 -0
- data/spec/lazy_spec.rb +217 -0
- data/spec/primitive_spec.rb +202 -0
- data/spec/record_spec.rb +530 -0
- data/spec/registry_spec.rb +108 -0
- data/spec/rest_spec.rb +26 -0
- data/spec/skip_spec.rb +27 -0
- data/spec/spec_common.rb +58 -0
- data/spec/string_spec.rb +300 -0
- data/spec/stringz_spec.rb +118 -0
- data/spec/struct_spec.rb +350 -0
- data/spec/system_spec.rb +380 -0
- data/tasks/manual.rake +36 -0
- data/tasks/rspec.rake +17 -0
- metadata +208 -0
data/lib/bindata/lazy.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
module BinData
|
2
|
+
# A LazyEvaluator is bound to a data object. The evaluator will evaluate
|
3
|
+
# lambdas in the context of this data object. These lambdas
|
4
|
+
# are those that are passed to data objects as parameters, e.g.:
|
5
|
+
#
|
6
|
+
# BinData::String.new(:value => lambda { %w{a test message}.join(" ") })
|
7
|
+
#
|
8
|
+
# As a shortcut, :foo is the equivalent of lambda { foo }.
|
9
|
+
#
|
10
|
+
# When evaluating lambdas, unknown methods are resolved in the context of the
|
11
|
+
# parent of the bound data object. Resolution is attempted firstly as keys
|
12
|
+
# in #parameters, and secondly as methods in this parent. This
|
13
|
+
# resolution propagates up the chain of parent data objects.
|
14
|
+
#
|
15
|
+
# An evaluation will recurse until it returns a result that is not
|
16
|
+
# a lambda or a symbol.
|
17
|
+
#
|
18
|
+
# This resolution process makes the lambda easier to read as we just write
|
19
|
+
# <tt>field</tt> instead of <tt>obj.field</tt>.
|
20
|
+
class LazyEvaluator
|
21
|
+
|
22
|
+
# Creates a new evaluator. All lazy evaluation is performed in the
|
23
|
+
# context of +obj+.
|
24
|
+
def initialize(obj)
|
25
|
+
@obj = obj
|
26
|
+
end
|
27
|
+
|
28
|
+
def lazy_eval(val, overrides = nil)
|
29
|
+
@overrides = overrides if overrides
|
30
|
+
if val.is_a? Symbol
|
31
|
+
__send__(val)
|
32
|
+
elsif val.respond_to? :arity
|
33
|
+
instance_exec(&val)
|
34
|
+
else
|
35
|
+
val
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a LazyEvaluator for the parent of this data object.
|
40
|
+
def parent
|
41
|
+
if @obj.parent
|
42
|
+
@obj.parent.lazy_evaluator
|
43
|
+
else
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the index of this data object inside it's nearest container
|
49
|
+
# array.
|
50
|
+
def index
|
51
|
+
return @overrides[:index] if @overrides and @overrides.has_key?(:index)
|
52
|
+
|
53
|
+
child = @obj
|
54
|
+
parent = @obj.parent
|
55
|
+
while parent
|
56
|
+
if parent.respond_to?(:find_index_of)
|
57
|
+
return parent.find_index_of(child)
|
58
|
+
end
|
59
|
+
child = parent
|
60
|
+
parent = parent.parent
|
61
|
+
end
|
62
|
+
raise NoMethodError, "no index found"
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing(symbol, *args)
|
66
|
+
return @overrides[symbol] if defined? @overrides and @overrides.has_key?(symbol)
|
67
|
+
|
68
|
+
if @obj.parent
|
69
|
+
eval_symbol_in_parent_context(symbol, args)
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#---------------
|
76
|
+
private
|
77
|
+
|
78
|
+
def eval_symbol_in_parent_context(symbol, args)
|
79
|
+
result = resolve_symbol_in_parent_context(symbol, args)
|
80
|
+
recursively_eval(result, args)
|
81
|
+
end
|
82
|
+
|
83
|
+
def resolve_symbol_in_parent_context(symbol, args)
|
84
|
+
obj_parent = @obj.parent
|
85
|
+
|
86
|
+
if obj_parent.has_parameter?(symbol)
|
87
|
+
obj_parent.get_parameter(symbol)
|
88
|
+
elsif obj_parent.safe_respond_to?(symbol)
|
89
|
+
obj_parent.__send__(symbol, *args)
|
90
|
+
else
|
91
|
+
symbol
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def recursively_eval(val, args)
|
96
|
+
if val.is_a?(Symbol)
|
97
|
+
parent.__send__(val, *args)
|
98
|
+
elsif val.respond_to?(:arity)
|
99
|
+
parent.instance_exec(&val)
|
100
|
+
else
|
101
|
+
val
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module BinData
|
2
|
+
# == Parameters
|
3
|
+
#
|
4
|
+
# Parameters may be provided at initialisation to control the behaviour of
|
5
|
+
# an object. These parameters are:
|
6
|
+
#
|
7
|
+
# [<tt>:check_offset</tt>] Raise an error if the current IO offset doesn't
|
8
|
+
# meet this criteria. A boolean return indicates
|
9
|
+
# success or failure. Any other return is compared
|
10
|
+
# to the current offset. The variable +offset+
|
11
|
+
# is made available to any lambda assigned to
|
12
|
+
# this parameter. This parameter is only checked
|
13
|
+
# before reading.
|
14
|
+
# [<tt>:adjust_offset</tt>] Ensures that the current IO offset is at this
|
15
|
+
# position before reading. This is like
|
16
|
+
# <tt>:check_offset</tt>, except that it will
|
17
|
+
# adjust the IO offset instead of raising an error.
|
18
|
+
module CheckOrAdjustOffsetMixin
|
19
|
+
|
20
|
+
def self.included(base) #:nodoc:
|
21
|
+
base.optional_parameters :check_offset, :adjust_offset
|
22
|
+
base.mutually_exclusive_parameters :check_offset, :adjust_offset
|
23
|
+
end
|
24
|
+
|
25
|
+
# Ideally these two methods should be protected,
|
26
|
+
# but Ruby 1.9.2 requires them to be public.
|
27
|
+
# see http://redmine.ruby-lang.org/issues/show/2375
|
28
|
+
|
29
|
+
def do_read_with_check_offset(io) #:nodoc:
|
30
|
+
check_offset(io)
|
31
|
+
do_read_without_check_offset(io)
|
32
|
+
end
|
33
|
+
|
34
|
+
def do_read_with_adjust_offset(io) #:nodoc:
|
35
|
+
adjust_offset(io)
|
36
|
+
do_read_without_adjust_offset(io)
|
37
|
+
end
|
38
|
+
|
39
|
+
#---------------
|
40
|
+
private
|
41
|
+
|
42
|
+
# To be called from BinData::Base#initialize.
|
43
|
+
#
|
44
|
+
# Monkey patches #do_read to check or adjust the stream offset
|
45
|
+
# as appropriate.
|
46
|
+
def add_methods_for_check_or_adjust_offset
|
47
|
+
if has_parameter?(:check_offset)
|
48
|
+
class << self
|
49
|
+
alias_method :do_read_without_check_offset, :do_read
|
50
|
+
alias_method :do_read, :do_read_with_check_offset
|
51
|
+
end
|
52
|
+
end
|
53
|
+
if has_parameter?(:adjust_offset)
|
54
|
+
class << self
|
55
|
+
alias_method :do_read_without_adjust_offset, :do_read
|
56
|
+
alias_method :do_read, :do_read_with_adjust_offset
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def check_offset(io)
|
62
|
+
actual_offset = io.offset
|
63
|
+
expected = eval_parameter(:check_offset, :offset => actual_offset)
|
64
|
+
|
65
|
+
if not expected
|
66
|
+
raise ValidityError, "offset not as expected for #{debug_name}"
|
67
|
+
elsif actual_offset != expected and expected != true
|
68
|
+
raise ValidityError,
|
69
|
+
"offset is '#{actual_offset}' but " +
|
70
|
+
"expected '#{expected}' for #{debug_name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def adjust_offset(io)
|
75
|
+
actual_offset = io.offset
|
76
|
+
expected = eval_parameter(:adjust_offset)
|
77
|
+
if actual_offset != expected
|
78
|
+
begin
|
79
|
+
seek = expected - actual_offset
|
80
|
+
io.seekbytes(seek)
|
81
|
+
warn "adjusting stream position by #{seek} bytes" if $VERBOSE
|
82
|
+
rescue
|
83
|
+
raise ValidityError,
|
84
|
+
"offset is '#{actual_offset}' but couldn't seek to " +
|
85
|
+
"expected '#{expected}' for #{debug_name}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'bindata/lazy'
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
module AcceptedParametersMixin
|
5
|
+
def self.included(base) #:nodoc:
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
# Class methods to mix in to BinData::Base
|
10
|
+
module ClassMethods
|
11
|
+
# Mandatory parameters must be present when instantiating a data object.
|
12
|
+
def mandatory_parameters(*args)
|
13
|
+
accepted_parameters.mandatory(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Optional parameters may be present when instantiating a data object.
|
17
|
+
def optional_parameters(*args)
|
18
|
+
accepted_parameters.optional(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Default parameters can be overridden when instantiating a data object.
|
22
|
+
def default_parameters(*args)
|
23
|
+
accepted_parameters.default(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Mutually exclusive parameters may not all be present when
|
27
|
+
# instantiating a data object.
|
28
|
+
def mutually_exclusive_parameters(*args)
|
29
|
+
accepted_parameters.mutually_exclusive(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :mandatory_parameter, :mandatory_parameters
|
33
|
+
alias_method :optional_parameter, :optional_parameters
|
34
|
+
alias_method :default_parameter, :default_parameters
|
35
|
+
|
36
|
+
def accepted_parameters #:nodoc:
|
37
|
+
unless defined? @accepted_parameters
|
38
|
+
ancestor_params = superclass.respond_to?(:accepted_parameters) ?
|
39
|
+
superclass.accepted_parameters : nil
|
40
|
+
@accepted_parameters = AcceptedParameters.new(ancestor_params)
|
41
|
+
end
|
42
|
+
@accepted_parameters
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# BinData objects accept parameters when initializing. AcceptedParameters
|
47
|
+
# allow a BinData class to declaratively identify accepted parameters as
|
48
|
+
# mandatory, optional, default or mutually exclusive.
|
49
|
+
class AcceptedParameters
|
50
|
+
|
51
|
+
def initialize(ancestor_parameters = nil)
|
52
|
+
if ancestor_parameters
|
53
|
+
@mandatory = ancestor_parameters.mandatory.dup
|
54
|
+
@optional = ancestor_parameters.optional.dup
|
55
|
+
@default = ancestor_parameters.default.dup
|
56
|
+
@mutually_exclusive = ancestor_parameters.mutually_exclusive.dup
|
57
|
+
else
|
58
|
+
@mandatory = []
|
59
|
+
@optional = []
|
60
|
+
@default = Hash.new
|
61
|
+
@mutually_exclusive = []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def mandatory(*args)
|
66
|
+
if not args.empty?
|
67
|
+
@mandatory.concat(to_syms(args))
|
68
|
+
@mandatory.uniq!
|
69
|
+
end
|
70
|
+
@mandatory
|
71
|
+
end
|
72
|
+
|
73
|
+
def optional(*args)
|
74
|
+
if not args.empty?
|
75
|
+
@optional.concat(to_syms(args))
|
76
|
+
@optional.uniq!
|
77
|
+
end
|
78
|
+
@optional
|
79
|
+
end
|
80
|
+
|
81
|
+
def default(args = nil)
|
82
|
+
if args
|
83
|
+
to_syms(args.keys) # call for side effect of validating names
|
84
|
+
args.each_pair do |param, value|
|
85
|
+
@default[param.to_sym] = value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
@default
|
89
|
+
end
|
90
|
+
|
91
|
+
def mutually_exclusive(*args)
|
92
|
+
arg1, arg2 = args
|
93
|
+
if arg1 != nil && arg2 != nil
|
94
|
+
@mutually_exclusive.push([arg1.to_sym, arg2.to_sym])
|
95
|
+
@mutually_exclusive.uniq!
|
96
|
+
end
|
97
|
+
@mutually_exclusive
|
98
|
+
end
|
99
|
+
|
100
|
+
def all
|
101
|
+
(@mandatory + @optional + @default.keys).uniq
|
102
|
+
end
|
103
|
+
|
104
|
+
#---------------
|
105
|
+
private
|
106
|
+
|
107
|
+
def to_syms(args)
|
108
|
+
syms = args.collect { |el| el.to_sym }
|
109
|
+
ensure_valid_names(syms)
|
110
|
+
syms
|
111
|
+
end
|
112
|
+
|
113
|
+
def ensure_valid_names(names)
|
114
|
+
invalid_names = self.class.invalid_parameter_names
|
115
|
+
names.each do |name|
|
116
|
+
if invalid_names.include?(name)
|
117
|
+
raise NameError.new("Rename parameter '#{name}' " +
|
118
|
+
"as it shadows an existing method.", name)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.invalid_parameter_names
|
124
|
+
unless defined? @invalid_names
|
125
|
+
all_names = LazyEvaluator.instance_methods(true) + Kernel.methods
|
126
|
+
allowed_names = ["name", "type", :name, :type] # ruby 1.8 vs 1.9
|
127
|
+
invalid_names = (all_names - allowed_names).uniq
|
128
|
+
@invalid_names = Hash[*invalid_names.collect { |key| [key.to_sym, true] }.flatten]
|
129
|
+
end
|
130
|
+
@invalid_names
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'bindata/base_primitive'
|
2
|
+
require 'bindata/dsl'
|
3
|
+
require 'bindata/struct'
|
4
|
+
|
5
|
+
module BinData
|
6
|
+
# A Primitive is a declarative way to define a new BinData data type.
|
7
|
+
# The data type must contain a primitive value only, i.e numbers or strings.
|
8
|
+
# For new data types that contain multiple values see BinData::Record.
|
9
|
+
#
|
10
|
+
# To define a new data type, set fields as if for Record and add a
|
11
|
+
# #get and #set method to extract / convert the data between the fields
|
12
|
+
# and the #value of the object.
|
13
|
+
#
|
14
|
+
# require 'bindata'
|
15
|
+
#
|
16
|
+
# class PascalString < BinData::Primitive
|
17
|
+
# uint8 :len, :value => lambda { data.length }
|
18
|
+
# string :data, :read_length => :len
|
19
|
+
#
|
20
|
+
# def get
|
21
|
+
# self.data
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# def set(v)
|
25
|
+
# self.data = v
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# ps = PascalString.new(:initial_value => "hello")
|
30
|
+
# ps.to_binary_s #=> "\005hello"
|
31
|
+
# ps.read("\003abcde")
|
32
|
+
# ps #=> "abc"
|
33
|
+
#
|
34
|
+
# # Unsigned 24 bit big endian integer
|
35
|
+
# class Uint24be < BinData::Primitive
|
36
|
+
# uint8 :byte1
|
37
|
+
# uint8 :byte2
|
38
|
+
# uint8 :byte3
|
39
|
+
#
|
40
|
+
# def get
|
41
|
+
# (self.byte1 << 16) | (self.byte2 << 8) | self.byte3
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def set(v)
|
45
|
+
# v = 0 if v < 0
|
46
|
+
# v = 0xffffff if v > 0xffffff
|
47
|
+
#
|
48
|
+
# self.byte1 = (v >> 16) & 0xff
|
49
|
+
# self.byte2 = (v >> 8) & 0xff
|
50
|
+
# self.byte3 = v & 0xff
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# u24 = Uint24be.new
|
55
|
+
# u24.read("\x12\x34\x56")
|
56
|
+
# "0x%x" % u24 #=> 0x123456
|
57
|
+
#
|
58
|
+
# == Parameters
|
59
|
+
#
|
60
|
+
# Primitive objects accept all the parameters that BinData::BasePrimitive do.
|
61
|
+
#
|
62
|
+
class Primitive < BasePrimitive
|
63
|
+
include DSLMixin
|
64
|
+
|
65
|
+
unregister_self
|
66
|
+
dsl_parser :primitive
|
67
|
+
|
68
|
+
class << self
|
69
|
+
def sanitize_parameters!(params) #:nodoc:
|
70
|
+
params[:struct_params] = params.create_sanitized_params(dsl_params, BinData::Struct)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
mandatory_parameter :struct_params
|
75
|
+
|
76
|
+
def initialize_instance
|
77
|
+
@struct = BinData::Struct.new(get_parameter(:struct_params), self)
|
78
|
+
end
|
79
|
+
|
80
|
+
def respond_to?(symbol, include_private = false) #:nodoc:
|
81
|
+
@struct.respond_to?(symbol, include_private) || super
|
82
|
+
end
|
83
|
+
|
84
|
+
def method_missing(symbol, *args, &block) #:nodoc:
|
85
|
+
if @struct.respond_to?(symbol)
|
86
|
+
@struct.__send__(symbol, *args, &block)
|
87
|
+
else
|
88
|
+
super
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def debug_name_of(child) #:nodoc:
|
93
|
+
debug_name + "-internal-"
|
94
|
+
end
|
95
|
+
|
96
|
+
def do_write(io)
|
97
|
+
set(_value)
|
98
|
+
@struct.do_write(io)
|
99
|
+
end
|
100
|
+
|
101
|
+
def do_num_bytes
|
102
|
+
set(_value)
|
103
|
+
@struct.do_num_bytes
|
104
|
+
end
|
105
|
+
|
106
|
+
#---------------
|
107
|
+
private
|
108
|
+
|
109
|
+
def sensible_default
|
110
|
+
get
|
111
|
+
end
|
112
|
+
|
113
|
+
def read_and_return_value(io)
|
114
|
+
@struct.do_read(io)
|
115
|
+
get
|
116
|
+
end
|
117
|
+
|
118
|
+
###########################################################################
|
119
|
+
# To be implemented by subclasses
|
120
|
+
|
121
|
+
# Extracts the value for this data object from the fields of the
|
122
|
+
# internal struct.
|
123
|
+
def get
|
124
|
+
raise NotImplementedError
|
125
|
+
end
|
126
|
+
|
127
|
+
# Sets the fields of the internal struct to represent +v+.
|
128
|
+
def set(v)
|
129
|
+
raise NotImplementedError
|
130
|
+
end
|
131
|
+
|
132
|
+
# To be implemented by subclasses
|
133
|
+
###########################################################################
|
134
|
+
end
|
135
|
+
end
|