1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 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 182 183 184 185 186 187 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 231 232 233 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 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 336 337 338 339 340 341 342 343 344
|
require 'bindata/base'
require 'bindata/dsl'
module BinData
# An Array is a list of data objects of the same type.
#
# require 'bindata'
#
# data = "\x03\x04\x05\x06\x07\x08\x09"
#
# obj = BinData::Array.new(type: :int8, initial_length: 6)
# obj.read(data) #=> [3, 4, 5, 6, 7, 8]
#
# obj = BinData::Array.new(type: :int8,
# read_until: -> { index == 1 })
# obj.read(data) #=> [3, 4]
#
# obj = BinData::Array.new(type: :int8,
# read_until: -> { element >= 6 })
# obj.read(data) #=> [3, 4, 5, 6]
#
# obj = BinData::Array.new(type: :int8,
# read_until: -> { array[index] + array[index - 1] == 13 })
# obj.read(data) #=> [3, 4, 5, 6, 7]
#
# obj = BinData::Array.new(type: :int8, read_until: :eof)
# obj.read(data) #=> [3, 4, 5, 6, 7, 8, 9]
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# <tt>:type</tt>:: The symbol representing the data type of the
# array elements. If the type is to have params
# passed to it, then it should be provided as
# <tt>[type_symbol, hash_params]</tt>.
# <tt>:initial_length</tt>:: The initial length of the array.
# <tt>:read_until</tt>:: While reading, elements are read until this
# condition is true. This is typically used to
# read an array until a sentinel value is found.
# The variables +index+, +element+ and +array+
# are made available to any lambda assigned to
# this parameter. If the value of this parameter
# is the symbol :eof, then the array will read
# as much data from the stream as possible.
#
# Each data object in an array has the variable +index+ made available
# to any lambda evaluated as a parameter of that data object.
class Array < BinData::Base
extend DSLMixin
include Enumerable
dsl_parser :array
arg_processor :array
mandatory_parameter :type
optional_parameters :initial_length, :read_until
mutually_exclusive_parameters :initial_length, :read_until
def initialize_shared_instance
@element_prototype = get_parameter(:type)
if get_parameter(:read_until) == :eof
extend ReadUntilEOFPlugin
elsif has_parameter?(:read_until)
extend ReadUntilPlugin
elsif has_parameter?(:initial_length)
extend InitialLengthPlugin
end
super
end
def initialize_instance
@element_list = nil
end
def clear?
@element_list.nil? || elements.all?(&:clear?)
end
def assign(array)
return if self.equal?(array) # prevent self assignment
raise ArgumentError, "can't set a nil value for #{debug_name}" if array.nil?
@element_list = []
concat(array)
end
def snapshot
elements.collect(&:snapshot)
end
def find_index(obj)
elements.index(obj)
end
alias index find_index
# Returns the first index of +obj+ in self.
#
# Uses equal? for the comparator.
def find_index_of(obj)
elements.index { |el| el.equal?(obj) }
end
def push(*args)
insert(-1, *args)
self
end
alias << push
def unshift(*args)
insert(0, *args)
self
end
def concat(array)
insert(-1, *array.to_ary)
self
end
def insert(index, *objs)
extend_array(index - 1)
abs_index = (index >= 0) ? index : index + 1 + length
# insert elements before...
new_elements = objs.map { new_element }
elements.insert(index, *new_elements)
# ...assigning values
objs.each_with_index do |obj, i|
self[abs_index + i] = obj
end
self
end
# Returns the element at +index+.
def [](arg1, arg2 = nil)
if arg1.respond_to?(:to_int) && arg2.nil?
slice_index(arg1.to_int)
elsif arg1.respond_to?(:to_int) && arg2.respond_to?(:to_int)
slice_start_length(arg1.to_int, arg2.to_int)
elsif arg1.is_a?(Range) && arg2.nil?
slice_range(arg1)
else
raise TypeError, "can't convert #{arg1} into Integer" unless arg1.respond_to?(:to_int)
raise TypeError, "can't convert #{arg2} into Integer" unless arg2.respond_to?(:to_int)
end
end
alias slice []
def slice_index(index)
extend_array(index)
at(index)
end
def slice_start_length(start, length)
elements[start, length]
end
def slice_range(range)
elements[range]
end
private :slice_index, :slice_start_length, :slice_range
# Returns the element at +index+. Unlike +slice+, if +index+ is out
# of range the array will not be automatically extended.
def at(index)
elements[index]
end
# Sets the element at +index+.
def []=(index, value)
extend_array(index)
elements[index].assign(value)
end
# Returns the first element, or the first +n+ elements, of the array.
# If the array is empty, the first form returns nil, and the second
# form returns an empty array.
def first(n = nil)
if n.nil? && empty?
# explicitly return nil as arrays grow automatically
nil
elsif n.nil?
self[0]
else
self[0, n]
end
end
# Returns the last element, or the last +n+ elements, of the array.
# If the array is empty, the first form returns nil, and the second
# form returns an empty array.
def last(n = nil)
if n.nil?
self[-1]
else
n = length if n > length
self[-n, n]
end
end
def length
elements.length
end
alias size length
def empty?
length.zero?
end
# Allow this object to be used in array context.
def to_ary
collect { |el| el }
end
def each
elements.each { |el| yield el }
end
def debug_name_of(child) #:nodoc:
index = find_index_of(child)
"#{debug_name}[#{index}]"
end
def offset_of(child) #:nodoc:
index = find_index_of(child)
sum = sum_num_bytes_below_index(index)
child.bit_aligned? ? sum.floor : sum.ceil
end
def do_write(io) #:nodoc:
elements.each { |el| el.do_write(io) }
end
def do_num_bytes #:nodoc:
sum_num_bytes_for_all_elements
end
#---------------
private
def extend_array(max_index)
max_length = max_index + 1
while elements.length < max_length
append_new_element
end
end
def elements
@element_list ||= []
end
def append_new_element
element = new_element
elements << element
element
end
def new_element
@element_prototype.instantiate(nil, self)
end
def sum_num_bytes_for_all_elements
sum_num_bytes_below_index(length)
end
def sum_num_bytes_below_index(index)
(0...index).inject(0) do |sum, i|
nbytes = elements[i].do_num_bytes
if nbytes.is_a?(Integer)
sum.ceil + nbytes
else
sum + nbytes
end
end
end
end
class ArrayArgProcessor < BaseArgProcessor
def sanitize_parameters!(obj_class, params) #:nodoc:
# ensure one of :initial_length and :read_until exists
unless params.has_at_least_one_of?(:initial_length, :read_until)
params[:initial_length] = 0
end
params.warn_replacement_parameter(:length, :initial_length)
params.warn_replacement_parameter(:read_length, :initial_length)
params.must_be_integer(:initial_length)
params.merge!(obj_class.dsl_params)
params.sanitize_object_prototype(:type)
end
end
# Logic for the :read_until parameter
module ReadUntilPlugin
def do_read(io)
loop do
element = append_new_element
element.do_read(io)
variables = { index: self.length - 1, element: self.last, array: self }
break if eval_parameter(:read_until, variables)
end
end
end
# Logic for the read_until: :eof parameter
module ReadUntilEOFPlugin
def do_read(io)
loop do
element = append_new_element
begin
element.do_read(io)
rescue EOFError, IOError
elements.pop
break
end
end
end
end
# Logic for the :initial_length parameter
module InitialLengthPlugin
def do_read(io)
elements.each { |el| el.do_read(io) }
end
def elements
if @element_list.nil?
@element_list = []
eval_parameter(:initial_length).times do
@element_list << new_element
end
end
@element_list
end
end
end
|