Class: BufferCursor

Inherits:
Object
  • Object
show all
Defined in:
lib/buffer_cursor.rb

Overview

A cursor to walk through data structures to read fields. The cursor can move forwards, backwards, is seekable, and supports peeking without moving the cursor. The BinData module is used for interpreting bytes as desired.

Defined Under Namespace

Classes: StackEntry

Constant Summary collapse

VERSION =
"0.9.0"
@@global_tracing =
false

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buffer, position) ⇒ BufferCursor

Initialize a cursor within a buffer at the given position.



40
41
42
43
44
45
46
47
# File 'lib/buffer_cursor.rb', line 40

def initialize(buffer, position)
  @buffer = buffer
  @stack = [ StackEntry.new(self, position) ]

  trace false
  trace_with :print_trace
  trace_to STDOUT
end

Class Method Details

.trace!(arg = true) ⇒ Object

Enable tracing for all BufferCursor objects globally.



35
36
37
# File 'lib/buffer_cursor.rb', line 35

def self.trace!(arg=true)
  @@global_tracing = arg
end

Instance Method Details

#adjust(relative_position) ⇒ Object

Adjust the current cursor to a new relative position.



149
150
151
152
# File 'lib/buffer_cursor.rb', line 149

def adjust(relative_position)
  current.position += relative_position
  self
end

#backwardObject

Set the direction of the cursor to “backward”.



133
134
135
# File 'lib/buffer_cursor.rb', line 133

def backward
  direction(:backward)
end

#currentObject

The current cursor object; the top of the stack.



97
98
99
# File 'lib/buffer_cursor.rb', line 97

def current
  @stack.last
end

#direction(direction_arg = nil) ⇒ Object

Return the direction of the current cursor.



118
119
120
121
122
123
124
125
# File 'lib/buffer_cursor.rb', line 118

def direction(direction_arg=nil)
  if direction_arg.nil?
    return current.direction
  end

  current.direction = direction_arg
  self
end

#each_byte_as_uint8(length) ⇒ Object

Iterate through length bytes returning each as an unsigned 8-bit integer.



203
204
205
206
207
208
209
210
211
212
213
# File 'lib/buffer_cursor.rb', line 203

def each_byte_as_uint8(length)
  unless block_given?
    return enum_for(:each_byte_as_uint8, length)
  end

  read_and_advance(length).bytes.each do |byte|
    yield byte
  end

  nil
end

#forwardObject

Set the direction of the cursor to “forward”.



128
129
130
# File 'lib/buffer_cursor.rb', line 128

def forward
  direction(:forward)
end

#get_bit_array(num_bits) ⇒ Object

Read an array of 1-bit integers.



295
296
297
298
299
300
# File 'lib/buffer_cursor.rb', line 295

def get_bit_array(num_bits)
  size = (num_bits + 7) / 8
  data = read_and_advance(size)
  bit_array = BinData::Array.new(:type => :bit1, :initial_length => size * 8)
  bit_array.read(data).to_ary
end

#get_bytes(length) ⇒ Object

Return raw bytes.



198
199
200
# File 'lib/buffer_cursor.rb', line 198

def get_bytes(length)
  read_and_advance(length)
end

#get_hex(length) ⇒ Object

Return raw bytes as hex.



216
217
218
# File 'lib/buffer_cursor.rb', line 216

def get_hex(length)
  read_and_advance(length).bytes.map { |c| "%02x" % c }.join
end

#get_sint16(position = nil) ⇒ Object

Read a big-endian signed 16-bit integer.



235
236
237
238
239
# File 'lib/buffer_cursor.rb', line 235

def get_sint16(position=nil)
  seek(position)
  data = read_and_advance(2)
  BinData::Int16be.read(data)
end

#get_uint16(position = nil) ⇒ Object

Read a big-endian unsigned 16-bit integer.



228
229
230
231
232
# File 'lib/buffer_cursor.rb', line 228

def get_uint16(position=nil)
  seek(position)
  data = read_and_advance(2)
  BinData::Uint16be.read(data)
end

#get_uint24(position = nil) ⇒ Object

Read a big-endian unsigned 24-bit integer.



242
243
244
245
246
# File 'lib/buffer_cursor.rb', line 242

def get_uint24(position=nil)
  seek(position)
  data = read_and_advance(3)
  BinData::Uint24be.read(data)
end

#get_uint32(position = nil) ⇒ Object

Read a big-endian unsigned 32-bit integer.



249
250
251
252
253
# File 'lib/buffer_cursor.rb', line 249

def get_uint32(position=nil)
  seek(position)
  data = read_and_advance(4)
  BinData::Uint32be.read(data)
end

#get_uint48(position = nil) ⇒ Object

Read a big-endian unsigned 48-bit integer.



256
257
258
259
260
# File 'lib/buffer_cursor.rb', line 256

def get_uint48(position=nil)
  seek(position)
  data = read_and_advance(6)
  BinData::Uint48be.read(data)
end

#get_uint64(position = nil) ⇒ Object

Read a big-endian unsigned 64-bit integer.



263
264
265
266
267
# File 'lib/buffer_cursor.rb', line 263

def get_uint64(position=nil)
  seek(position)
  data = read_and_advance(8)
  BinData::Uint64be.read(data)
end

#get_uint8(position = nil) ⇒ Object

Read an unsigned 8-bit integer.



221
222
223
224
225
# File 'lib/buffer_cursor.rb', line 221

def get_uint8(position=nil)
  seek(position)
  data = read_and_advance(1)
  BinData::Uint8.read(data)
end

#get_uint_array_by_size(size, count) ⇒ Object

Read an array of count unsigned integers given their size in bytes.



290
291
292
# File 'lib/buffer_cursor.rb', line 290

def get_uint_array_by_size(size, count)
  (0...count).to_a.inject([]) { |a, n| a << get_uint_by_size(size); a }
end

#get_uint_by_size(size) ⇒ Object

Read a big-endian unsigned integer given its size in bytes.



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/buffer_cursor.rb', line 270

def get_uint_by_size(size)
  case size
  when 1
    get_uint8
  when 2
    get_uint16
  when 3
    get_uint24
  when 4
    get_uint32
  when 6
    get_uint48
  when 8
    get_uint64
  else
    raise "Integer size #{size} not implemented"
  end
end

#name(name_arg = nil) ⇒ Object

Set the field name.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/buffer_cursor.rb', line 102

def name(name_arg=nil)
  if name_arg.nil?
    return current.name
  end

  unless block_given?
    raise "No block given"
  end

  current.name.push name_arg
  ret = yield(self)
  current.name.pop
  ret
end

#peek(position = nil) ⇒ Object

Execute a block and restore the cursor to the previous position after the block returns. Return the block’s return value after restoring the cursor. Optionally seek to provided position before executing block.



171
172
173
174
175
176
177
# File 'lib/buffer_cursor.rb', line 171

def peek(position=nil)
  raise "No block given" unless block_given?
  push(position)
  result = yield(self)
  pop
  result
end

#popObject

Restore the last cursor position.



162
163
164
165
166
# File 'lib/buffer_cursor.rb', line 162

def pop
  raise "No cursors to pop" unless @stack.size > 1
  @stack.pop
  self
end

#positionObject

Return the position of the current cursor.



138
139
140
# File 'lib/buffer_cursor.rb', line 138

def position
  current.position
end

Print a trace output for this cursor. The method is passed a cursor object, position, raw byte buffer, and array of names.



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/buffer_cursor.rb', line 56

def print_trace(cursor, position, bytes, name)
  slice_size = 16
  bytes.each_slice(slice_size).each_with_index do |slice_bytes, slice_count|
    @trace_io.puts "%06i %s %-32s  %s" % [
      position + (slice_count * slice_size),
      direction == :backward ? "" : "",
      slice_bytes.map { |n| "%02x" % n }.join,
      slice_count == 0 ? name.join(".") : "",
    ]
  end
end

#push(position = nil) ⇒ Object

Save the current cursor position and start a new (nested, stacked) cursor.



155
156
157
158
159
# File 'lib/buffer_cursor.rb', line 155

def push(position=nil)
  @stack.push current.dup
  seek(position)
  self
end

#read_and_advance(length) ⇒ Object

Read a number of bytes forwards or backwards from the current cursor position and adjust the cursor position by that amount.



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/buffer_cursor.rb', line 181

def read_and_advance(length)
  data = nil
  cursor_start = current.position
  case current.direction
  when :forward
    data = @buffer.slice(current.position, length)
    adjust(length)
  when :backward
    adjust(-length)
    data = @buffer.slice(current.position, length)
  end

  record_trace(cursor_start, data.bytes, current.name)
  data
end

#record_trace(position, bytes, name) ⇒ Object

Generate a trace record from the current cursor.



92
93
94
# File 'lib/buffer_cursor.rb', line 92

def record_trace(position, bytes, name)
  @trace_proc.call(self, position, bytes, name) if tracing_enabled?
end

#seek(position) ⇒ Object

Move the current cursor to a new absolute position.



143
144
145
146
# File 'lib/buffer_cursor.rb', line 143

def seek(position)
  current.position = position if position
  self
end

#trace(arg = true) ⇒ Object



49
50
51
52
# File 'lib/buffer_cursor.rb', line 49

def trace(arg=true)
  @instance_tracing = arg
  self
end

#trace_to(file) ⇒ Object



68
69
70
71
# File 'lib/buffer_cursor.rb', line 68

def trace_to(file)
  @trace_io = file
  self
end

#trace_with(arg = nil) ⇒ Object

Set a Proc or method on self to trace with.



74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/buffer_cursor.rb', line 74

def trace_with(arg=nil)
  if arg.nil?
    @trace_proc = nil
  elsif arg.class == Proc
    @trace_proc = arg
  elsif arg.class == Symbol
    @trace_proc = lambda { |cursor, position, bytes, name| self.send(arg, cursor, position, bytes, name) }
  else
    raise "Don't know how to trace with #{arg}"
  end
  self
end

#tracing_enabled?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'lib/buffer_cursor.rb', line 87

def tracing_enabled?
  (@@global_tracing or @instance_tracing) && @trace_proc
end