Class: Arpie::Binary
- Inherits:
-
Object
- Object
- Arpie::Binary
- Extended by:
- Arpie
- Defined in:
- lib/arpie/binary.rb,
lib/arpie/binary/fixed_type.rb
Overview
A Binary is a helper to convert arbitary bitlevel binary data to and from ruby Struct-lookalikes.
Here’s an example:
class Test < Arpie::Binary
describe "I am a test"
field :a, :uint8
field :b, :bytes, :sizeof => :nint16
end
This will allow you to parse binary blocks consisting of:
-
a byte
-
a network-order int (16 bit)
-
a string which length’ is the given int
Rinse and repeat.
For all available data types, please look into the source file of this class at the bottom.
Writing new types is easy enough, (see BinaryType).
On confusing names:
Arpie uses the term binary
to refer to on-the-wire data bit/byte binary format. A Binary
(uppercase) is a object describing the format. If you’re reading binary
, think “raw data”; if you’re reading Binary
or object
, think Arpie::Binary.
Another warning:
Do not use Kernel
methods as field names. It’ll confuse method_missing. Example:
field :test, :uint8
=> in `test': wrong number of arguments (ArgumentError)
In fact, this is the reason while Binary will not let you define fields with with names like existing instance methods.
Defined Under Namespace
Constant Summary
Constants included from Arpie
Class Method Summary collapse
- .__anonymous ⇒ Object
- .__anonymous=(x) ⇒ Object
- .add_hook(hook, handler) ⇒ Object
- .binary_size(opts = {}) ⇒ Object
- .call_hooks(hook, *va) ⇒ Object
- .call_virtual(on_object, name) ⇒ Object
-
.describe(text = nil) ⇒ Object
You can use this to provide a short description of this Binary.
- .describe_all_types ⇒ Object
-
.f(*va, &b) ⇒ Object
Alias for
field
. -
.field(name, type = nil, opts = {}, &block) ⇒ Object
Specify that this Binary has a field of type
type
. -
.field?(field) ⇒ Boolean
Returns true if this Binary has the named
field
. -
.from(binary, opts = {}) ⇒ Object
Parse the given
binary
, which is a string, and return an instance of this class. - .get_field(field) ⇒ Object
-
.get_type_handler(type) ⇒ Object
Returns the BinaryType handler class for
type
, which can be a symbol (:uint8), or a Arpie::Binary. -
.post_from(&handler) ⇒ Object
Add a hook that gets called after converting a Binary to wire format.
-
.post_to(&handler) ⇒ Object
Add a hook that gets called after converting a binary to Binary representation.
-
.pre_from(&handler) ⇒ Object
Add a hook that gets called before converting a Binary to wire format.
-
.pre_to(&handler) ⇒ Object
Add a hook that gets called before converting a binary to Binary representation.
-
.register_type(handler, *type_aliases) ⇒ Object
This registers a new type with this binary.
-
.s(*va, &b) ⇒ Object
An alias for
static
. -
.static(content, name = nil) ⇒ Object
Create a static field.
-
.to(object, opts = {}) ⇒ Object
Recursively convert the given Binary object to wire format.
-
.v(*va, &b) ⇒ Object
Alias for
virtual
. -
.virtual(name, type, opts = {}, &handler) ⇒ Object
Set up a new virtual field.
-
.virtual?(virtual) ⇒ Boolean
Returns true if this Binary has the named
virtual
.
Instance Method Summary collapse
-
#initialize ⇒ Binary
constructor
:startdoc:.
-
#inspect ⇒ Object
:nodoc:.
- #method_missing(m, *a) ⇒ Object
- #to(opts = {}) ⇒ Object
Methods included from Arpie
Constructor Details
#initialize ⇒ Binary
:startdoc:
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/arpie/binary.rb', line 63 def initialize @fields = {} @@fields[self.class] ||= [] # set up our own class handlers, create anon classes, set up default values @@fields[self.class].each {|field| if field.inline_handler @fields[field.name] = field.inline_handler.new elsif field.type.is_a?(Class) @fields[field.name] = field.type.new elsif field.opts[:default] @fields[field.name] = field.opts[:default] end } if block_given? yield self end end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(m, *a) ⇒ Object
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/arpie/binary.rb', line 100 def method_missing m, *a m.to_s =~ /^(.+?)(=?)$/ or super at = $1.to_sym if self.class.field?(at) if $2 == "=" a.size == 1 or raise ArgumentError if !a[0].is_a?(Class) && inline = self.class.get_field(at)[3] a[0], __nil = inline.from(a[0], {}) end @fields[at] = a[0] else @fields[at] end elsif self.class.virtual?(at) if $2 == "=" raise ArgumentError else Binary.call_virtual(self, at) end else super end end |
Class Method Details
.__anonymous ⇒ Object
55 56 57 |
# File 'lib/arpie/binary.rb', line 55 def self.__anonymous @@anonymous[self] end |
.__anonymous=(x) ⇒ Object
58 59 60 |
# File 'lib/arpie/binary.rb', line 58 def self.__anonymous= x @@anonymous[self] = x end |
.add_hook(hook, handler) ⇒ Object
314 315 316 317 318 |
# File 'lib/arpie/binary.rb', line 314 def self.add_hook(hook, handler) @@hooks[self] ||= {} @@hooks[self][hook] ||= [] @@hooks[self][hook] << handler end |
.binary_size(opts = {}) ⇒ Object
302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/arpie/binary.rb', line 302 def self.binary_size opts = {} @@fields[self] ||= [] total = @@fields[self].inject(0) {|sum, field| klass = get_type_handler(field.type) sz = klass.binary_size(field.opts) sz or raise "cannot binary_size dynamic Binary definitions" sum + sz } total end |
.call_hooks(hook, *va) ⇒ Object
320 321 322 323 324 325 326 327 |
# File 'lib/arpie/binary.rb', line 320 def self.call_hooks(hook, *va) @@hooks[self] ||= {} @@hooks[self][hook] ||= [] @@hooks[self][hook].each {|handler| va = *handler.call(*va) } va end |
.call_virtual(on_object, name) ⇒ Object
126 127 128 129 130 |
# File 'lib/arpie/binary.rb', line 126 def self.call_virtual(on_object, name) @@virtuals[on_object.class].select {|virtual| virtual.name == name }[0].handler.call(on_object) end |
.describe(text = nil) ⇒ Object
You can use this to provide a short description of this Binary. It will be shown when calling Binary.inspect. When called without a parameter, (non-recursively) print out a pretty description of this data type as it would appear on the wire.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/arpie/binary.rb', line 188 def self.describe text = nil unless text.nil? @@description[self] = text else ret = [] ret << "%-10s %s" % ["Binary:", @@description[self]] ret << "" sprf = "%-10s %-25s %-15s %-15s %-15s %s" sprf_of = "%68s %s" if @@virtuals[self] && @@virtuals[self].size > 0 ret << sprf % ["Virtuals:", "NAME", "TYPE", "WIDTH", "", "DESCRIPTION"] @@virtuals[self].each {|virtual| width = self.get_type_handler(virtual.type).binary_size(virtual.opts) ret << sprf % [ "", virtual.name, virtual.type, width, "", virtual.opts[:description] ] } ret << "" end if @@fields[self] && @@fields[self].size > 0 ret << sprf % %w{Fields: NAME TYPE WIDTH OF DESCRIPTION} @@fields[self].each {|field| width = self.get_type_handler(field.type).binary_size(field.opts) ret << sprf % [ "", field.name, field.type, (field.opts[:length] || field.opts[:sizeof] || width), field.opts[:of] ? field.opts[:of].inspect : "", field.opts[:description] ] ret << sprf_of % [ "", field.opts[:of_opts].inspect ] if field.opts[:of_opts] } end ret.join("\n") end end |
.describe_all_types ⇒ Object
174 175 176 177 178 179 180 181 182 |
# File 'lib/arpie/binary.rb', line 174 def self.describe_all_types ret = [] strf = "%-15s %-8s %s" ret << strf % %w{TYPE WIDTH HANDLER} @@types.sort{|a,b| a[0].to_s <=> b[0].to_s}.each {|type, handler| ret << strf % [type.inspect, (handler.binary_size({}) rescue nil), handler.inspect] } ret.join("\n") end |
.f(*va, &b) ⇒ Object
Alias for field
.
280 281 282 |
# File 'lib/arpie/binary.rb', line 280 def self.f *va, &b field *va, &b end |
.field(name, type = nil, opts = {}, &block) ⇒ Object
Specify that this Binary has a field of type type
. See the class documentation for usage.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/arpie/binary.rb', line 234 def self.field name, type = nil, opts = {}, &block @@fields[self] ||= [] handler = get_type_handler(type) raise ArgumentError, "#{name.inspect} already exists as a virtual" if virtual?(name) raise ArgumentError, "#{name.inspect} already exists as a field" if field?(name) raise ArgumentError, "#{name.inspect} already exists as a instance method" if instance_methods.index(name.to_s) raise ArgumentError, "#{name.inspect}: cannot inline classes" if block_given? and type.class === Arpie::Binary @@fields[self].each {|field| raise ArgumentError, "#{name.inspect}: :optional fields cannot be followed by required fields" if field[:opts].include?(:optional) } unless opts[:optional] type.nil? && !block_given? and raise ArgumentError, "You need to specify an inline handler if no type is given." inline_handler = nil if block_given? inline_handler = Class.new(Arpie::Binary) inline_handler.__anonymous = [name, type, opts] inline_handler.instance_eval(&block) end if type.nil? type, inline_handler = inline_handler, nil end if handler.respond_to?(:required_opts) missing_required = handler.required_opts.keys - opts.keys raise ArgumentError, "#{self}: #{name.inspect} as type #{type.inspect} " + "requires options: #{missing_required.inspect}" if missing_required.size > 0 handler.required_opts.each {|k,v| v.nil? and next v.call(opts[k]) or raise ArgumentError, "#{self}: Invalid value given for opt key #{k.inspect}." } end opts[:description] ||= opts[:desc] if opts[:desc] opts.delete(:desc) @@fields[self] << Field.new(name.to_sym, type, opts, inline_handler) end |
.field?(field) ⇒ Boolean
Returns true if this Binary has the named field
.
140 141 142 143 144 145 |
# File 'lib/arpie/binary.rb', line 140 def self.field? field @@fields[self] or return false @@fields[self].select {|_field| _field.name == field }.size > 0 end |
.from(binary, opts = {}) ⇒ Object
Parse the given binary
, which is a string, and return an instance of this class. Will raise Arpie::EIncomplete when not enough data is available in binary
to construct a complete Binary.
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/arpie/binary.rb', line 332 def self.from binary, opts = {} @@fields[self] ||= [] binary = * self.call_hooks(:pre_from, binary) consumed_bytes = 0 obj = new @@fields[self].each {|field| # name, klass, kopts, inline_handler| field.opts[:binary] = binary field.opts[:object] = obj handler = get_type_handler(field.type) attrib, consumed = binary, nil attrib, consumed = handler.from(binary[consumed_bytes .. -1], field.opts) rescue case $! when EIncomplete if field.opts[:optional] attrib, consumed = field.opts[:default], handler.binary_size(field.opts) else raise $!, "#{$!.to_s}, #{self}#from needs more data for " + "#{field.name.inspect}. (data: #{binary[consumed_bytes .. -1].inspect})" end when StreamError bogon! binary[consumed_bytes .. -1], "#{self}#from: #{field.name.inspect}: #{$!.to_s}" else raise end consumed_bytes += consumed obj.send((field.name.to_s + "=").to_sym, attrib) field.opts.delete(:binary) field.opts.delete(:object) } binary, obj, consumed_bytes = self.call_hooks(:post_from, binary, obj, consumed_bytes) [obj, consumed_bytes] end |
.get_field(field) ⇒ Object
147 148 149 150 151 152 153 |
# File 'lib/arpie/binary.rb', line 147 def self.get_field field @@fields[self] or raise ArgumentError, "No such field: #{field.inspect}" @@fields[self].each {|_field| _field.name == field and return _field } raise ArgumentError, "No such field: #{field.inspect}" end |
.get_type_handler(type) ⇒ Object
Returns the BinaryType handler class for type
, which can be a symbol (:uint8), or a Arpie::Binary.
165 166 167 168 169 170 171 172 |
# File 'lib/arpie/binary.rb', line 165 def self.get_type_handler type if type.class === Arpie::Binary type else @@types[type] or raise ArgumentError, "#{self}: No such field type: #{type.inspect}" end end |
.post_from(&handler) ⇒ Object
Add a hook that gets called after converting a Binary to wire format. Arguments to the handler: binary
, object
Note that all handlers need to return their arguemts as they were passed, as they will replace the original values.
435 436 437 |
# File 'lib/arpie/binary.rb', line 435 def self.post_from &handler self.add_hook(:post_from, handler) end |
.post_to(&handler) ⇒ Object
Add a hook that gets called after converting a binary to Binary representation. Arguments to the handler: object
, binary
, consumed_bytes
. Note that all handlers need to return their arguemts as they were passed, as they will replace the original values.
419 420 421 |
# File 'lib/arpie/binary.rb', line 419 def self.post_to &handler self.add_hook(:post_to, handler) end |
.pre_from(&handler) ⇒ Object
Add a hook that gets called before converting a Binary to wire format. Arguments to the handler: object
Note that all handlers need to return their arguemts as they were passed, as they will replace the original values.
427 428 429 |
# File 'lib/arpie/binary.rb', line 427 def self.pre_from &handler self.add_hook(:pre_from, handler) end |
.pre_to(&handler) ⇒ Object
Add a hook that gets called before converting a binary to Binary representation. Arguments to the handler: binary
Note that all handlers need to return their arguemts as they were passed, as they will replace the original values.
411 412 413 |
# File 'lib/arpie/binary.rb', line 411 def self.pre_to &handler self.add_hook(:pre_to, handler) end |
.register_type(handler, *type_aliases) ⇒ Object
This registers a new type with this binary.
133 134 135 136 137 |
# File 'lib/arpie/binary.rb', line 133 def self.register_type handler, *type_aliases type_aliases.each do |type| @@types[type] = handler end end |
.s(*va, &b) ⇒ Object
An alias for static
.
36 37 38 |
# File 'lib/arpie/binary/fixed_type.rb', line 36 def self.s *va, &b static *va, &b end |
.static(content, name = nil) ⇒ Object
Create a static field. This is an alias for:
field :name, :fixed, :value => content
If no name is specified, it is assumed that the user will never want to access it and a suitable one is autogenerated.
30 31 32 33 |
# File 'lib/arpie/binary/fixed_type.rb', line 30 def self.static content, name = nil name ||= "static-%x" % content.hash field name, :fixed, :value => content end |
.to(object, opts = {}) ⇒ Object
Recursively convert the given Binary object to wire format.
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/arpie/binary.rb', line 372 def self.to object, opts = {} object.nil? and raise ArgumentError, "cannot #to nil" @@fields[self] ||= [] r = [] object = * self.call_hooks(:pre_to, object) @@fields[self].each {|field| # name, klass, kopts, inline_handler| field.opts[:object] = object handler = get_type_handler(field.type) val = object.send(field.name) if field.inline_handler val = val.to end # r << (val.respond_to?(:to) ? val.to(opts) : handler.to(val, kopts)) rescue case $! r << handler.to(val, field.opts) rescue case $! when StreamError raise $!, "#{self}#from: #{field.name.inspect}: #{$!.to_s}" else raise end field.opts.delete(:object) } r = r.join('') _obj, r = self.call_hooks(:post_to, object, r) r end |
.v(*va, &b) ⇒ Object
Alias for virtual
.
298 299 300 |
# File 'lib/arpie/binary.rb', line 298 def self.v *va, &b virtual *va, &b end |
.virtual(name, type, opts = {}, &handler) ⇒ Object
Set up a new virtual field
285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/arpie/binary.rb', line 285 def self.virtual name, type, opts = {}, &handler raise ArgumentError, "You need to pass a block with virtuals" unless block_given? raise ArgumentError, "#{name.inspect} already exists as a virtual" if virtual?(name) raise ArgumentError, "#{name.inspect} already exists as a field" if field?(name) raise ArgumentError, "#{name.inspect} already exists as a instance method" if instance_methods.index(name.to_s) @@virtuals[self] ||= [] opts[:description] ||= opts[:desc] opts.delete(:desc) @@virtuals[self] << Virtual.new(name.to_sym, type, opts, handler) end |
.virtual?(virtual) ⇒ Boolean
Returns true if this Binary has the named virtual
.
156 157 158 159 160 161 |
# File 'lib/arpie/binary.rb', line 156 def self.virtual? virtual @@virtuals[self] or return false @@virtuals[self].select {|_virtual| _virtual.name == virtual }.size > 0 end |
Instance Method Details
#inspect ⇒ Object
:nodoc:
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/arpie/binary.rb', line 84 def inspect #:nodoc: desc = " " + @@description[self.class].inspect if @@description[self.class] # Anonymous is special klass = self.class.respond_to?(:__anonymous) && self.class.__anonymous ? "Anon#{self.class.__anonymous.inspect}" : self.class.to_s fields = [] @@fields[self.class].each {|field| fields << "%s=>%s" % [field.name.inspect, @fields[field.name].inspect] } fields = '{' + fields.join(", ") + '}' "#<#{klass}#{desc} #{fields}>" end |
#to(opts = {}) ⇒ Object
402 403 404 |
# File 'lib/arpie/binary.rb', line 402 def to opts = {} self.class.to(self, opts) end |