require 'bindata/base'
require 'bindata/sanitize'
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)
# obj.snapshot #=> [3, 4, 5, 6, 7, 8]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { index == 1 })
# obj.read(data)
# obj.snapshot #=> [3, 4]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { element >= 6 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { array[index] + array[index - 1] == 13 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7]
#
# obj = BinData::Array.new(:type => :int8, :read_until => :eof)
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# :type:: 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
# [type_symbol, hash_params].
# :initial_length:: The initial length of the array.
# :read_until:: 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
include Enumerable
register(self.name, self)
mandatory_parameter :type
optional_parameters :initial_length, :read_until
mutually_exclusive_parameters :initial_length, :read_until
class << self
def sanitize_parameters!(sanitizer, params)
unless params.has_key?(:initial_length) or params.has_key?(:read_until)
# ensure one of :initial_length and :read_until exists
params[:initial_length] = 0
end
warn_replacement_parameter(params, :read_length, :initial_length)
if params.has_key?(:type)
type, el_params = params[:type]
klass = sanitizer.lookup_class(type)
sanitized_params = sanitizer.sanitized_params(klass, el_params)
params[:type] = [klass, sanitized_params]
end
super(sanitizer, params)
end
end
def initialize(params = {}, parent = nil)
super(params, parent)
el_class, el_params = get_parameter(:type)
@element_list = nil
@element_class = el_class
@element_params = el_params
end
# Returns if the element at position +index+ is clear?. If +index+
# is not given, then returns whether all elements are clear.
def clear?(index = nil)
if index.nil?
@element_list.nil? or elements.inject(true) { |all_clear, f| all_clear and f.clear? }
elsif index < elements.length
warn "'obj.clear?(n)' is deprecated. Replacing with 'obj[n].clear?'"
elements[index].clear?
else
true
end
end
# Clears the element at position +index+. If +index+ is not given, then
# the internal state of the array is reset to that of a newly created
# object.
def clear(index = nil)
if index.nil?
@element_list = nil
elsif index < elements.length
warn "'obj.clear(n)' is deprecated. Replacing with 'obj[n].clear'"
elements[index].clear
end
end
# Returns the first index of +obj+ in self.
#
# a = BinData::String.new; a.value = "a"
# b = BinData::String.new; b.value = "b"
# c = BinData::String.new; c.value = "c"
#
# arr = BinData::Array.new(:type => :string)
# arr.push(a, b, c)
#
# arr.find_index("b") #=> 1
# arr.find_index(c) #=> 2
#
def find_index(obj)
elements.find_index(obj)
end
alias_method :index, :find_index
# Returns the first index of +obj+ in self.
#
# Uses equal? for the comparator.
def find_index_of(obj)
elements.find_index { |el| el.equal?(obj) }
end
def push(*args)
insert(-1, *args)
self
end
alias_method :<<, :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)
elements.insert(index, *to_storage_formats(objs))
self
end
def append(value = nil)
warn "#append is deprecated, use push or slice instead"
if value.nil?
slice(length)
else
push(value)
end
self.last
end
# Returns the element at +index+.
def [](arg1, arg2 = nil)
if arg1.respond_to?(:to_int) and arg2.nil?
slice_index(arg1.to_int)
elsif arg1.respond_to?(:to_int) and arg2.respond_to?(:to_int)
slice_start_length(arg1.to_int, arg2.to_int)
elsif arg1.is_a?(Range) and 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_method :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? and 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
# The number of elements in this array.
def length
elements.length
end
alias_method :size, :length
# Returns true if self array contains no elements.
def empty?
length.zero?
end
# Allow this object to be used in array context.
def to_ary
collect { |el| el }
end
# Iterate over each element in the array.
def each
elements.each { |el| yield el }
end
def debug_name_of(child)
index = find_index_of(child)
"#{debug_name}[#{index}]"
end
def offset_of(child)
index = find_index_of(child)
sum = sum_num_bytes_below_index(index)
child_offset = (::Integer === child.do_num_bytes) ? sum.ceil : sum.floor
offset + child_offset
end
#---------------
private
def extend_array(max_index)
max_length = max_index + 1
while elements.length < max_length
append_new_element
end
end
def to_storage_formats(els)
els.collect { |el| to_storage_format(el) }
end
def to_storage_format(obj)
element = new_element
element.assign(obj)
element
end
def _do_read(io)
if has_parameter?(:initial_length)
elements.each { |f| f.do_read(io) }
elsif has_parameter?(:read_until)
read_until(io)
end
end
def read_until(io)
if get_parameter(:read_until) == :eof
read_until_eof(io)
else
read_until_condition(io)
end
end
def read_until_eof(io)
finished = false
while not finished
element = append_new_element
begin
element.do_read(io)
rescue
elements.pop
finished = true
end
end
end
def read_until_condition(io)
finished = false
while not finished
element = append_new_element
element.do_read(io)
variables = { :index => self.length - 1, :element => self.last,
:array => self }
finished = eval_parameter(:read_until, variables)
end
end
def _done_read
elements.each { |f| f.done_read }
end
def _do_write(io)
elements.each { |f| f.do_write(io) }
end
def _do_num_bytes(index)
if index.nil?
sum_num_bytes_for_all_elements.ceil
elsif index < elements.length
warn "'obj.num_bytes(n)' is deprecated. Replacing with 'obj[n].num_bytes'"
elements[index].do_num_bytes
else
0
end
end
def _assign(array)
raise ArgumentError, "can't set a nil value for #{debug_name}" if array.nil?
@element_list = to_storage_formats(array.to_ary)
end
def _snapshot
elements.collect { |e| e.snapshot }
end
def elements
if @element_list.nil?
@element_list = []
if has_parameter?(:initial_length)
eval_parameter(:initial_length).times do
@element_list << new_element
end
end
end
@element_list
end
def append_new_element
element = new_element
elements << element
element
end
def new_element
@element_class.new(@element_params, self)
end
def sum_num_bytes_for_all_elements
sum_num_bytes_below_index(length)
end
def sum_num_bytes_below_index(index)
sum = 0
(0...index).each do |i|
nbytes = elements[i].do_num_bytes
sum = ((::Integer === nbytes) ? sum.ceil : sum) + nbytes
end
sum
end
end
end