Class: Binstruct

Inherits:
Object show all
Defined in:
lib/binstruct.rb

Overview

There are two main classes of methods for a Binstruct, but because of the implementation they are all actually instance methods. The “construction” methods are endian, bitfield, substruct and all the builders for field subtypes. Each class in the Fields module has a builder, so UnsignedField is just unsigned. Those methods are created at runtime, so they can’t be seen by rdoc, but they all look like:

unsigned, buffer_name, :accessor_sym, length_in_bits, "Description"

Inside a parse block, the buffer name used for the field builders should match the block parameter, unless you are manually messing with the buffer. The same applies to fields created inside the blocks for substructs and bitfields.

The other methods are ‘proper’ instance methods, to work with the structure once it is created and filled with data. Each field creates a getter and setter method matching its symbol, and also a direct access via [:field_sym] that returns the field itself, which gives you access to the field’s own instance methods like set_raw (see Fields).

Direct Known Subclasses

Bitfield

Defined Under Namespace

Classes: Bitfield

Constant Summary collapse

VERSION =
"1.0.0"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buffer = nil, &blk) ⇒ Binstruct

————END CONSTRUCTION——————



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/binstruct.rb', line 128

def initialize(buffer=nil, &blk)
    @fields=[]
    @hash_references={}
    @endian_flip_hack=false
    @groups=Hash.new {|h,k| h[k]=[]}
    buffer||=""
    @bitbuf=buffer.unpack('B*').join
    if block_given?
        instance_exec(@bitbuf, &blk)
    elsif self.class.init_block
        instance_exec(@bitbuf, &self.class.init_block)
    else
        # do nothing, user probably just wants a blank struct to manually add fields.
    end
    endian :big unless @endian
    @groups.each {|group, contents|
        unless contents.flatten.all? {|sym| @hash_references.keys.any? {|othersym| othersym==sym}}
            raise RuntimeError, "Binstruct: Construction: group #{group} contains invalid field name(s)"
        end
    }
    # This is not ideal for structures that aren't byte aligned, but raising an exception 
    # would be less flexible.
    buffer.replace @bitbuf.scan(/.{8}/).map {|e| e.to_i(2).chr}.join unless buffer.nil?
end

Class Attribute Details

.init_blockObject (readonly)

Returns the value of attribute init_block.



108
109
110
# File 'lib/binstruct.rb', line 108

def init_block
  @init_block
end

Instance Attribute Details

#endian(sym) ⇒ Object

Set the endianness for the whole structure. Default is :big, options are :big or :little. Fields created after this method is invoked in a construction block will be created with the new endianness. You can also set the endianness after construction with somestruct.endian=:little .



34
35
36
# File 'lib/binstruct.rb', line 34

def endian
  @endian
end

#fieldsObject

Returns the value of attribute fields.



26
27
28
# File 'lib/binstruct.rb', line 26

def fields
  @fields
end

#groupsObject (readonly)

Returns the value of attribute groups.



25
26
27
# File 'lib/binstruct.rb', line 25

def groups
  @groups
end

Class Method Details

.parse(&blk) ⇒ Object

There are two ways to create a Binstruct subclass, one is by calling parse inside the structure definition:

class Foo < Binstruct
    parse {|buffer_as_binary|
        #definitions here
    }
end

and the other is by just supplying a block to new:

quick_struct=Binstruct.new {|b| string, b, :foo, 32, "Some String"}

Otherwise, Binstruct.new will just create a blank structure (this can be useful if you want to fill in the fields at runtime).



122
123
124
# File 'lib/binstruct.rb', line 122

def self.parse( &blk )
    @init_block=blk
end

Instance Method Details

#[](sym) ⇒ Object

return an object, specified by symbol. May be a field or a substruct. not designed for bitfields, since they’re supposed to be invisible containers.



158
159
160
# File 'lib/binstruct.rb', line 158

def []( sym )
    @hash_references[sym]
end

#bitfield(bitbuf, len, &blk) ⇒ Object

In little endian structures, byte-swaps 16, 32 or 64 bits and presents them for carving into fields. Only really neccessary for non-byte aligned field sets in little endian structures. The bitfield itself is invisible but the fields are created with the usual accessors.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/binstruct.rb', line 47

def bitfield(bitbuf, len, &blk)
    if @endian==:little
        unless len==16||len==32||len==64
            raise RuntimeError, "Binstruct: Bitfield: Don't know how to endian swap #{len} bits. :("
        end
        instr=bitbuf.slice!(0,len).scan(/.{8}/).reverse.join
    else
        instr=bitbuf.slice!(0,len)
    end
    new=Bitfield.new([instr].pack('B*'), &blk)
    if @endian==:little
        # This is so we know to flip the bytes back in #to_s
        new.instance_variable_set :@endian_flip_hack, true
    end
    @fields << new
    # Add direct references and accessor methods to the containing Binstruct
    new.fields.each {|f| 
        unless f.is_a? Fields::Field
            raise RuntimeError, "Binstruct: Construction: Illegal content #{f.class} in bitfield - use only Fields"
        end
        @hash_references[f.name]=f
        meta_def f.name do f.get_value end
        meta_def (f.name.to_s + '=').to_sym do |val| f.set_value(val) end
    }
end

#deep_each(&blk) ⇒ Object

yield all fields in the structure, entering nested substructs as necessary



179
180
181
182
183
184
185
186
187
# File 'lib/binstruct.rb', line 179

def deep_each( &blk ) #:yields: a Field
    @fields.each {|atom|
        if atom.is_a? Binstruct
            atom.deep_each &blk unless atom.fields.empty?
        else
            yield atom
        end
    }
end

#each(&blk) ⇒ Object

yield each object to the block. This is a little messy, because substructs are not Fields::Field types. For Bitfields, just silently yield each component, not the container field. The upshot of all this is that the caller needs to be prepared for a Field or a Binstruct in the block. This is the ‘shallow’ each.



167
168
169
170
171
172
173
174
175
176
# File 'lib/binstruct.rb', line 167

def each( &blk ) #:yields: a Field or a Bitstruct
    @fields.each {|atom|
        if atom.is_a? Bitfield
            atom.fields.each {|f| yield f}
        else
            yield atom
        end
    }

end

#flattenObject

Flattens all fields in nested structures into an array, preserving order. In some cases (eg little endian structures with bitfields) this will mean that struct.flatten.join will not be the same as struct.to_s.



210
211
212
213
214
# File 'lib/binstruct.rb', line 210

def flatten
    a=[]
    self.deep_each {|f| a << f}
    a
end

#group(groupname, *fieldsyms) ⇒ Object

Groups a list of fields under :groupname. Designed for use in Metafuzz. somestruct.groups will return the hash of {:group_sym=>[field1, field2...]}



103
104
105
# File 'lib/binstruct.rb', line 103

def group( groupname, *fieldsyms )
    @groups[groupname] << fieldsyms
end

#inspectObject

Returns an array of terse field descriptions which show the index, field class, name and length in bits, plus a hexdumped snippet of the contents.



246
247
248
249
# File 'lib/binstruct.rb', line 246

def inspect
    # This could possibly be more readable...
    self.flatten.map {|field| "<IDX:#{self.flatten.index(field)}><#{field.class.to_s.match(/::(.+)Field/)[1]}><#{field.name}><#{field.length}><#{field.to_s[0..12].each_byte.to_a.map {|b| "%.2x" % b}.join(' ') + (field.to_s.length>12?"...":"")}>"}
end

#lengthObject

Packed length in bytes.



240
241
242
# File 'lib/binstruct.rb', line 240

def length
    self.to_s.length
end

#replace(oldthing, newthing) ⇒ Object

Searches a Binstruct, recursing through nested structures as neccessary, and replaces a given object with a new object. Note that this replaces the object that ==oldthing, so a reference to it is needed first.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/binstruct.rb', line 192

def replace(oldthing, newthing)
    k,v=@hash_references.select {|k,v| v==oldthing}.flatten
    @hash_references[k]=newthing
    @fields.map! {|atom|
        if atom==oldthing
            newthing
        else
            if atom.is_a? Binstruct
                atom.replace(oldthing,newthing)
            end
            atom
        end
    }
end

#substruct(strbuf, name, len, klass) ⇒ Object

Creates a nested structure, and a direct accesor for it that returns the structure itself, so accessors like main.sub1.sub2.some_val are possible. When iterating over the Binstruct contents, see #each, which will pass substructs to the block and #deep_each which recursively enters substructs and passes only Fields.



79
80
81
82
83
84
85
86
# File 'lib/binstruct.rb', line 79

def substruct(strbuf, name, len, klass)
    new=klass.new(strbuf)
    @fields << new
    @hash_references[name]=new
    meta_def name do new end
    # More informative than the NoMethodError they would normally get.
    meta_def (name.to_s + '=').to_sym do raise NoMethodError, "Binstruct: Illegal call of '=' on a substruct." end
end

#to_sObject

pack current struct as a string - for Fields, it will use the bitstring, for anything else (including Bitfields and Binstructs) it will use to_s.unpack('B*'). Via a filthy internal hack, bitfields get byte-swapped back in here. Finally, once the bitstring is assembled, it is right-padded with 0 and packed as a string.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/binstruct.rb', line 222

def to_s
    bits=@fields.inject("") {|str,field| 
        field.kind_of?(Fields::Field) ?  str << field.bitstring : str << field.to_s.unpack('B*').join
    } 
    unless bits.length % 8 == 0
        #puts "Warning, structure not byte aligned, right padding with 0"
    end
    return "" if bits.empty?
    bytearray=bits.scan(/.{1,8}/)
    # If not byte aligned, right pad with 0
    bytearray.last << '0'*(8-bytearray.last.length) if bytearray.last.length < 8
    # This only happens for Binstructs that have the endian_flip_hack ivar
    # set, so only inside the to_s of a Bitfield when little endian.
    bytearray=bytearray.reverse if @endian_flip_hack
    bytearray.map {|byte| "" << byte.to_i(2)}.join
end