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:
#
# :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
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)
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(&: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)
elements.insert(index, *to_storage_formats(objs))
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 to_storage_formats(els)
els.collect { |el| new_element(el) }
end
def elements
@element_list ||= []
end
def append_new_element
element = new_element
elements << element
element
end
def new_element(value = nil)
@element_prototype.instantiate(value, 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