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 => true)
# 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.
#
# 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 this class
register(self.name, self)
# These are the parameters used by this class.
mandatory_parameter :type
optional_parameters :initial_length, :read_until, :read_until_eof
mutually_exclusive_parameters :initial_length, :read_until, :read_until_eof
class << self
# Returns a sanitized +params+ that is of the form expected
# by #initialize.
def sanitize_parameters(sanitizer, params)
params = params.dup
unless params.has_key?(:initial_length) or params.has_key?(:read_until) or params.has_key?(:read_until_eof)
# ensure one of :initial_length, :read_until, or :read_until_eof exists
params[:initial_length] = 0
end
if params.has_key?(:read_length)
warn ":read_length is not used with arrays. You probably want to change this to :initial_length"
end
if params.has_key?(:type)
type, el_params = params[:type]
params[:type] = sanitizer.sanitize(type, el_params)
end
super(sanitizer, params)
end
end
# Creates a new Array
def initialize(params = {}, env = nil)
super(params, env)
klass, el_params = param(:type)
@element_list = nil
@element_klass = klass
@element_params = el_params
end
# Returns if the element at position +index+ is clear?. If +index+
# is not given, then returns whether all fields are clear.
def clear?(index = nil)
if @element_list.nil?
true
elsif index.nil?
elements.each { |f| return false if not f.clear? }
true
else
(index < elements.length) ? elements[index].clear? : 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 @element_list.nil?
# do nothing as the array is already clear
elsif index.nil?
@element_list = nil
elsif index < elements.length
elements[index].clear
end
end
# Returns whether this data object contains a single value. Single
# value data objects respond to #value and #value=.
def single_value?
return false
end
# To be called after calling #do_read.
def done_read
elements.each { |f| f.done_read }
end
# Appends a new element to the end of the array. If the array contains
# single_values then the +value+ may be provided to the call.
# Returns the appended object, or value in the case of single_values.
def append(value = nil)
# TODO: deprecate #append as it can be replaced with #push
append_new_element
self[-1] = value unless value.nil?
self.last
end
# Pushes the given object(s) on to the end of this array.
# This expression returns the array itself, so several appends may
# be chained together.
def push(*args)
args.each do |arg|
if @element_klass == arg.class
# TODO: need to modify arg.env to add_variable(:index) and
# to link arg.env to self.env
elements.push(arg)
else
append(arg)
end
end
self
end
# Returns the element at +index+. If the element is a single_value
# then the value of the element is returned instead.
def [](*args)
if args.length == 1 and ::Integer === args[0]
# extend array automatically
while args[0] >= elements.length
append_new_element
end
end
data = elements[*args]
if data.respond_to?(:each)
data.collect { |el| (el && el.single_value?) ? el.value : el }
else
(data && data.single_value?) ? data.value : data
end
end
alias_method :slice, :[]
# Sets the element at +index+. If the element is a single_value
# then the value of the element is set instead.
def []=(index, value)
# extend array automatically
while index >= elements.length
append_new_element
end
obj = elements[index]
unless obj.single_value?
# TODO: allow setting objects, not just values
raise NoMethodError, "undefined method `[]=' for #{self}", caller
end
obj.value = value
end
# Iterate over each element in the array. If the elements are
# single_values then the values of the elements are iterated instead.
def each
elements.each do |el|
yield(el.single_value? ? el.value : el)
end
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?
if elements.empty?
# explicitly return nil as arrays grow automatically
nil
else
self[0]
end
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
snapshot
end
#---------------
private
# Reads the values for all fields in this object from +io+.
def _do_read(io)
if has_param?(:initial_length)
elements.each { |f| f.do_read(io) }
elsif has_param?(:read_until)
@element_list = nil
loop do
element = append_new_element
element.do_read(io)
variables = { :index => self.length - 1, :element => self.last,
:array => self }
finished = eval_param(:read_until, variables)
break if finished
end
else # :read_until_eof
loop do
element = append_new_element
begin
element.do_read(io)
rescue EOFError
finished = true
remove_last_element
end
variables = { :index => self.length - 1, :element => self.last,
:array => self }
break if finished
end
end
end
# Writes the values for all fields in this object to +io+.
def _do_write(io)
elements.each { |f| f.do_write(io) }
end
# Returns the number of bytes it will take to write the element at
# +index+. If +index+, then returns the number of bytes required
# to write all fields.
def _do_num_bytes(index)
if index.nil?
(elements.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
else
elements[index].do_num_bytes
end
end
# Returns a snapshot of the data in this array.
def _snapshot
elements.collect { |e| e.snapshot }
end
# Returns the list of all elements in the array. The elements
# will be instantiated on the first call to this method.
def elements
if @element_list.nil?
@element_list = []
if has_param?(:initial_length)
# create the desired number of instances
eval_param(:initial_length).times do
append_new_element
end
end
end
@element_list
end
# Creates a new element and appends it to the end of @element_list.
# Returns the newly created element
def append_new_element
# ensure @element_list is initialised
elements()
env = create_env
env.add_variable(:index, @element_list.length)
element = @element_klass.new(@element_params, env)
@element_list << element
element
end
# Pops the last element off the end of @element_list.
# Returns the popped element.
# This is important for the :read_until_eof option to properly close
# the do_read io handle.
def remove_last_element
elements()
@element_list.pop
end
end
end