Class: PacketGen::Types::Fields Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/packetgen/types/fields.rb

Overview

This class is abstract.

Set of fields

This class is a base class to define headers or anything else with a binary format containing multiple fields.

Basics

A Fields subclass is generaly composed of multiple binary fields. These fields have each a given type. All types from PacketGen::Types module are supported, and all Fields subclasses may also be used as field type.

To define a new subclass, it has to inherit from Fields. And some class methods have to be used to declare attributes/fields:

class MyBinaryStructure < PacketGen::Types::Fields
  # define a first Int8 attribute, with default value: 1
  define_field :attr1, PacketGen::Types::Int8, default: 1
  #define a second attribute, of kind Int32
  define_field :attr2, PacketGen::Types::Int32
end

These defintions create 4 methods: #attr1, #attr1=, #attr2 and #attr2=. All these methods take and/or return Integers.

Fields may also be accessed through #[] ans #[]=. These methods give access to type object:

mybs = MyBinaryStructure.new
mybs.attr1     # => Integer
mybs[:attr1]   # => PacketGen::Types::Int8

#initialize accepts an option hash to populate attributes. Keys are attribute name symbols, and values are those expected by writer accessor.

#read is able to populate object from a binary string.

#to_s returns binary string from object.

Add Fields

Fields.define_field adds a field to Fields subclass. A lot of field types may be defined: integer types, string types (to handle a stream of bytes). More complex field types may be defined using others Fields subclasses:

# define a 16-bit little-endian integer field, named type
define_field :type, PacketGen::Types::Int16le
# define a string field
define_field :body, PacketGen::Types::String
# define afield using a complex type (Fields subclass)
define_field :mac_addr, PacketGen::Eth::MacAddr

This example creates six methods on our Fields subclass: #type, #type=, #body, #body=, #mac_addr and #mac_addr=.

Fields.define_field has many options (third optional Hash argument):

  • :default gives default field value. It may be a simple value (an Integer for an Int field, for example) or a lambda,

  • :builder to give a builder/constructor lambda to create field. The lambda takes one argument: Fields subclass object owning field,

  • :optional to define this field as optional. This option takes a lambda parameter used to say if this field is present or not,

  • :enum to define Hash enumeration for an Enum type.

For example:

# 32-bit integer field defaulting to 1
define_field :type, PacketGen::Types::Int32, default: 1
# 16-bit integer field, created with a random value. Each instance of this
# object will have a different value.
define_field :id, PacketGen::Types::Int16, default: ->{ rand(65535) }
# a size field
define_field :body_size, PacketGen::Types::Int16
# String field which length is taken from body_size field
define_field :body, PacketGen::Types::String, builder: ->(obj, type) { type.new('', length_from: obj[:body_size]) }
# 16-bit enumeration type. As :default not specified, default to first value of enum
define_field :type_class, PacketGen::Types::Int16Enum, enum: { 'class1' => 1, 'class2' => 2}
# optional field. Only present if another field has a certain value
define_field :opt1, PacketGen::Types::Int16, optional: ->(h) { h.type == 42 }

Fields.define_field_before and Fields.define_field_after are also defined to relatively create a field from anoher one (for example, when adding a field in a subclass).

Generating bit fields

Fields.define_bit_fields_on creates a bit field on a previuously declared integer field. For example, frag field in IP header:

define_field :frag, Types::Int16, default: 0
define_bit_fields_on :frag, :flag_rsv, :flag_df, :flag_mf, :fragment_offset, 13

This example generates methods:

  • #frag and #frag= to access frag field as a 16-bit integer,

  • #flag_rsv?, #flag_rsv=, #flag_df?, #flag_df=, #flag_mf? and #flag_mf= to access Boolean RSV, MF and DF flags from frag field,

  • #fragment_offset and #fragment_offset= to access 13-bit integer fragment offset subfield from frag field.

Author:

  • Sylvain Daubert

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Fields

Create a new header object



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
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
# File 'lib/packetgen/types/fields.rb', line 355

def initialize(options={})
  @fields = {}
  @optional_fields = {}

  field_defs = self.class.class_eval { @field_defs }
  self.class.fields.each do |field|
    ary = field_defs[field]
    type, default, builder, optional, enum, field_options = ary
    default = default.to_proc.call(self) if default.is_a?(Proc)
    @fields[field] = if builder
                       builder.call(self, type)
                     elsif enum
                       type.new(enum)
                     elsif !field_options.empty?
                       type.new(field_options)
                     else
                       type.new
                     end

    value = options[field] || default
    if value.class <= type
      @fields[field] = value
    elsif type < Types::Enum
      case value
      when ::String
        @fields[field].value = value
      else
        @fields[field].read(value)
      end
    elsif type < Types::Int
      @fields[field].read(value)
    elsif type <= Types::String
      @fields[field].read(value)
    elsif @fields[field].respond_to? :from_human
      @fields[field].from_human(value)
    end

    @optional_fields[field] = optional if optional
  end
  self.class.class_eval { @bit_fields }.each do |_, hsh|
    hsh.each_key do |bit_field|
      self.send "#{bit_field}=", options[bit_field] if options[bit_field]
    end
  end
end

Class Method Details

.define_bit_fields_on(attr, *args) ⇒ void

This method returns an undefined value.

Define a bitfield on given attribute

class MyHeader < PacketGen::Types::Fields
  define_field :flags, Types::Int16
  # define a bit field on :flag attribute:
  # flag1, flag2 and flag3 are 1-bit fields
  # type and stype are 3-bit fields. reserved is a 6-bit field
  define_bit_fields_on :flags, :flag1, :flag2, :flag3, :type, 3, :stype, 3, :reserved, 7
end

A bitfield of size 1 bit defines 2 methods:

  • #field? which returns a Boolean,

  • #field= which takes and returns a Boolean.

A bitfield of more bits defines 2 methods:

  • #field which returns an Integer,

  • #field= which takes and returns an Integer.

Raises:

  • (ArgumentError)


278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/packetgen/types/fields.rb', line 278

def define_bit_fields_on(attr, *args)
  attr_def = @field_defs[attr]
  raise ArgumentError, "unknown #{attr} field" if attr_def.nil?

  type = attr_def.first
  unless type < Types::Int
    raise TypeError, "#{attr} is not a PacketGen::Types::Int"
  end

  total_size = type.new.width * 8
  idx = total_size - 1

  field = args.shift
  while field
    next unless field.is_a? Symbol

    size = if args.first.is_a? Integer
             args.shift
           else
             1
           end
    unless field == :_
      shift = idx - (size - 1)
      field_mask = (2**size - 1) << shift
      clear_mask = (2**total_size - 1) & (~field_mask & (2**total_size - 1))

      if size == 1
        class_eval "        def \#{field}?\n          val = (self[:\#{attr}].to_i & \#{field_mask}) >> \#{shift}\n          val != 0\n        end\n        def \#{field}=(v)\n          val = v ? 1 : 0\n          self[:\#{attr}].value = self[:\#{attr}].to_i & \#{clear_mask}\n          self[:\#{attr}].value |= val << \#{shift}\n        end\n        METHODS\n      else\n        class_eval <<-METHODS\n        def \#{field}\n          (self[:\#{attr}].to_i & \#{field_mask}) >> \#{shift}\n        end\n        def \#{field}=(v)\n          self[:\#{attr}].value = self[:\#{attr}].to_i & \#{clear_mask}\n          self[:\#{attr}].value |= (v & \#{2**size - 1}) << \#{shift}\n        end\n        METHODS\n      end\n\n      @bit_fields[attr] = {} if @bit_fields[attr].nil?\n      @bit_fields[attr][field] = size\n    end\n\n    idx -= size\n    field = args.shift\n  end\nend\n"

.define_field(name, type, options = {}) ⇒ void

This method returns an undefined value.

Define a field in class

class BinaryStruct < PacketGen::Types::Fields
  # 8-bit value
  define_field :value1, Types::Int8
  # 16-bit value
  define_field :value2, Types::Int16
  # specific class, may use a specific constructor
  define_field :value3, MyClass, builder: ->(obj, type) { type.new(obj) }
end

bs = BinaryStruct.new
bs[value1]   # => Types::Int8
bs.value1    # => Integer

Options Hash (options):

  • :default (Object)

    default value. May be a proc. This lambda take one argument: the caller object.

  • :builder (Lambda)

    lambda to construct this field. Parameters to this lambda is the caller object and the field type class.

  • :optional (Lambda)

    define this field as optional. Given lambda is used to known if this field is present or not. Parameter to this lambda is the being defined Field object.

  • :enum (Hash)

    mandatory option for an Enum type. Define enumeration: hash’s keys are String, and values are Integer.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/packetgen/types/fields.rb', line 155

def define_field(name, type, options={})
  define = []
  if type < Types::Enum
    define << "def #{name}; self[:#{name}].to_i; end"
    define << "def #{name}=(val) self[:#{name}].value = val; end"
  elsif type < Types::Int
    define << "def #{name}; self[:#{name}].to_i; end"
    define << "def #{name}=(val) self[:#{name}].read val; end"
  elsif type.instance_methods.include?(:to_human) &&
        type.instance_methods.include?(:from_human)
    define << "def #{name}; self[:#{name}].to_human; end"
    define << "def #{name}=(val) self[:#{name}].from_human val; end"
  else
    define << "def #{name}; self[:#{name}]; end\n"
    define << "def #{name}=(val) self[:#{name}].read val; end"
  end

  define.delete(1) if type.instance_methods.include? "#{name}=".to_sym
  define.delete(0) if type.instance_methods.include? name
  class_eval define.join("\n")
  @field_defs[name] = [type, options.delete(:default),
                       options.delete(:builder),
                       options.delete(:optional),
                       options.delete(:enum),
                       options]
  fields << name
end

.define_field_after(other, name, type, options = {}) ⇒ void

This method returns an undefined value.

Define a field, after another one

Raises:

  • (ArgumentError)

See Also:



210
211
212
213
214
215
216
217
218
219
# File 'lib/packetgen/types/fields.rb', line 210

def define_field_after(other, name, type, options={})
  define_field name, type, options
  return if other.nil?

  fields.delete name
  idx = fields.index(other)
  raise ArgumentError, "unknown #{other} field" if idx.nil?

  fields[idx + 1, 0] = name
end

.define_field_before(other, name, type, options = {}) ⇒ void

This method returns an undefined value.

Define a field, before another one

Raises:

  • (ArgumentError)

See Also:



191
192
193
194
195
196
197
198
199
200
# File 'lib/packetgen/types/fields.rb', line 191

def define_field_before(other, name, type, options={})
  define_field name, type, options
  return if other.nil?

  fields.delete name
  idx = fields.index(other)
  raise ArgumentError, "unknown #{other} field" if idx.nil?

  fields[idx, 0] = name
end

.delete_field(name) ⇒ void

Deprecated.

Use remove_field instead.

This method returns an undefined value.

Delete a previously defined field

Since:

  • 2.8.4 deprecated



237
238
239
240
# File 'lib/packetgen/types/fields.rb', line 237

def delete_field(name)
  Deprecation.deprecated(self, __method__, 'remove_field', klass_method: true)
  remove_field name
end

.fieldsArray<Symbol>

Get field names



124
125
126
# File 'lib/packetgen/types/fields.rb', line 124

def fields
  @ordered_fields
end

.inherited(klass) ⇒ void

This method returns an undefined value.

On inheritage, create @field_defs class variable



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/packetgen/types/fields.rb', line 108

def inherited(klass)
  field_defs = {}
  @field_defs.each do |k, v|
    field_defs[k] = v.clone
  end
  ordered = @ordered_fields.clone
  bf = @bit_fields.clone
  klass.class_eval do
    @ordered_fields = ordered
    @field_defs = field_defs
    @bit_fields = bf
  end
end

.remove_bit_fields_on(attr) ⇒ void

This method returns an undefined value.

Remove all bit fields defined on attr

Since:

  • 2.8.4



341
342
343
344
345
346
347
348
349
# File 'lib/packetgen/types/fields.rb', line 341

def remove_bit_fields_on(attr)
  fields = @bit_fields.delete(attr)
  return if fields.nil?

  fields.each do |field, size|
    undef_method "#{field}="
    undef_method(size == 1 ? "#{field}?" : "#{field}")
  end
end

.remove_field(name) ⇒ void

This method returns an undefined value.

Remove a previously defined field

Since:

  • 2.8.4



225
226
227
228
229
230
# File 'lib/packetgen/types/fields.rb', line 225

def remove_field(name)
  fields.delete name
  @field_defs.delete name
  undef_method name if method_defined?(name)
  undef_method "#{name}=" if method_defined?("#{name}=")
end

.update_field(field, options) ⇒ void

This method returns an undefined value.

Update a previously defined field

Raises:

  • (ArgumentError)

    unknown field

See Also:

Since:

  • 2.8.4



249
250
251
252
253
254
255
256
257
# File 'lib/packetgen/types/fields.rb', line 249

def update_field(field, options)
  raise ArgumentError, "unkown #{field} field for #{self}" unless @field_defs.key?(field)

  @field_defs[field][1] = options.delete(:default) if options.key?(:default)
  @field_defs[field][2] = options.delete(:builder) if options.key?(:builder)
  @field_defs[field][3] = options.delete(:optional) if options.key?(:optional)
  @field_defs[field][4] = options.delete(:enum) if options.key?(:enum)
  @field_defs[field][5].merge!(options)
end

Instance Method Details

#[](field) ⇒ Object

Get field object



404
405
406
# File 'lib/packetgen/types/fields.rb', line 404

def [](field)
  @fields[field]
end

#[]=(field, obj) ⇒ Object

Set field object



412
413
414
# File 'lib/packetgen/types/fields.rb', line 412

def []=(field, obj)
  @fields[field] = obj
end

#bits_on(field) ⇒ Hash?

Get bit fields definition for given field.

Since:

  • 2.8.3



574
575
576
# File 'lib/packetgen/types/fields.rb', line 574

def bits_on(field)
  self.class.class_eval { @bit_fields }[field]
end

#body=(value) ⇒ void

Deprecated.

This method returns an undefined value.

Used to set body as value of body object.

Raises:

  • (BodyError)

    no body on given object

  • (ArgumentError)

    cannot cram body in :body field



530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/packetgen/types/fields.rb', line 530

def body=(value)
  Deprecation.deprecated(self.class, __method__)
  raise BodyError, 'no body field' unless @fields.key? :body

  case body
  when ::String
    self[:body].read value
  when Int, Fields
    self[:body] = value
  when NilClass
    self[:body] = Types::String.new.read('')
  else
    raise ArgumentError, "Can't cram a #{body.class} in a :body field"
  end
end

#fieldsArray<Symbol>

Get all field names



418
419
420
# File 'lib/packetgen/types/fields.rb', line 418

def fields
  self.class.fields
end

#force_binary(str) ⇒ String

Deprecated.

Will be a private method

Force str to binary encoding



550
551
552
# File 'lib/packetgen/types/fields.rb', line 550

def force_binary(str)
  PacketGen.force_binary(str)
end

#inspect {|attr| ... } ⇒ String

Common inspect method for headers.

A block may be given to differently format some attributes. This may be used by subclasses to handle specific fields.

Yield Parameters:

  • attr (Symbol)

    attribute to inspect

Yield Returns:

  • (String, nil)

    the string to print for attr, or nil to let inspect generate it



493
494
495
496
497
498
499
500
501
502
503
# File 'lib/packetgen/types/fields.rb', line 493

def inspect
  str = Inspect.dashed_line(self.class, 1)
  fields.each do |attr|
    next if attr == :body
    next unless present?(attr)

    result = yield(attr)if block_given?
    str << (result || Inspect.inspect_attribute(attr, self[attr], 1))
  end
  str
end

#is_optional?(field) ⇒ Boolean

Deprecated.

Use #optional? instead.



434
435
436
437
# File 'lib/packetgen/types/fields.rb', line 434

def is_optional?(field)
  Deprecation.deprecated(self.class, __method__, 'optional?', klass_method: true)
  optional? field
end

#is_present?(field) ⇒ Boolean

Deprecated.

Use #present? instead.

Say if an optional field is present



450
451
452
453
# File 'lib/packetgen/types/fields.rb', line 450

def is_present?(field)
  Deprecation.deprecated(self.class, __method__, 'present?', klass_method: true)
  present? field
end

#offset_of(field) ⇒ Integer

Get offset of given field in PacketGen::Types::Fields structure.

Raises:

  • (ArgumentError)

    unknown field



558
559
560
561
562
563
564
565
566
567
568
# File 'lib/packetgen/types/fields.rb', line 558

def offset_of(field)
  raise ArgumentError, "#{field} is an unknown field of #{self.class}" unless @fields.include?(field)

  offset = 0
  fields.each do |f|
    break offset if f == field
    next unless present?(f)

    offset += self[f].sz
  end
end

#optional?(field) ⇒ Boolean

Say if this field is optional



429
430
431
# File 'lib/packetgen/types/fields.rb', line 429

def optional?(field)
  @optional_fields.key? field
end

#optional_fieldsObject

Get all optional field name



423
424
425
# File 'lib/packetgen/types/fields.rb', line 423

def optional_fields
  @optional_fields.keys
end

#present?(field) ⇒ Boolean

Say if an optional field is present



441
442
443
444
445
# File 'lib/packetgen/types/fields.rb', line 441

def present?(field)
  return true unless optional?(field)

  @optional_fields[field].call(self)
end

#read(str) ⇒ Fields

Populate object from a binary string



458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/packetgen/types/fields.rb', line 458

def read(str)
  return self if str.nil?

  force_binary str
  start = 0
  fields.each do |field|
    next unless present?(field)

    obj = nil
    if self[field].respond_to? :width
      width = self[field].width
      obj = self[field].read str[start, width]
      start += width
    elsif self[field].respond_to? :sz
      obj = self[field].read str[start..-1]
      size = self[field].sz
      start += size
    else
      obj = self[field].read str[start..-1]
      start = str.size
    end
    self[field] = obj unless obj == self[field]
  end

  self
end

#sznteger

Size of object as binary string



514
515
516
# File 'lib/packetgen/types/fields.rb', line 514

def sz
  to_s.size
end

#to_hHash

Return object as a hash



520
521
522
# File 'lib/packetgen/types/fields.rb', line 520

def to_h
  Hash[fields.map { |f| [f, @fields[f].to_human] }]
end

#to_sString

Return object as a binary string



507
508
509
510
# File 'lib/packetgen/types/fields.rb', line 507

def to_s
  fields.select { |f| present?(f) }
        .map! { |f| force_binary @fields[f].to_s }.join
end