jbangert-bindata 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|