require 'bindata/base' module BinData # An Array is a list of data objects of the same type. # # require 'bindata' # require 'stringio' # # a = BinData::Array.new(:type => :int8, :initial_length => 5) # io = StringIO.new("\x03\x04\x05\x06\x07") # a.read(io) # a.snapshot #=> [3, 4, 5, 6, 7] # # == 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 < 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 # Creates a new Array def initialize(params = {}, env = nil) super(cleaned_params(params), env) ensure_mutual_exclusion(:initial_length, :read_until) type, el_params = param(:type) klass = klass_lookup(type) raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil? @element_list = nil @element_klass = klass @element_params = el_params || {} 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 else elements[index].clear end 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 elements[index].clear? end end # 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) } else # :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 end end # To be called after calling #do_read. def done_read elements.each { |f| f.done_read } end # Writes the values for all fields in this object to +io+. def _write(io) elements.each { |f| f.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 _num_bytes(index) if index.nil? elements.inject(0) { |sum, f| sum + f.num_bytes } else elements[index].num_bytes end end # Returns a snapshot of the data in this array. def snapshot elements.collect { |e| e.snapshot } end # An array has no fields. def field_names [] 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? self.length.zero? ? nil : self[0] else array = [] [n, self.length].min.times do |i| array.push(self[i]) end array 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.length.zero? ? nil : self[self.length - 1] else array = [] start = self.length - [n, self.length].min start.upto(self.length - 1) do |i| array.push(self[i]) end array end 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) append_new_element self[self.length - 1] = value unless value.nil? self.last end # Returns the element at +index+. If the element is a single_value # then the value of the element is returned instead. def [](index) obj = elements[index] obj.single_value? ? obj.value : obj end # Sets the element at +index+. If the element is a single_value # then the value of the element is set instead. def []=(index, value) obj = elements[index] unless obj.single_value? 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 # The number of elements in this array. def length elements.length end alias_method :size, :length #--------------- private # 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 # Returns a hash of cleaned +params+. Cleaning means that param # values are converted to a desired format. def cleaned_params(params) unless params.has_key?(:initial_length) or params.has_key?(:read_until) # ensure one of :initial_length and :read_until exists new_params = params.dup new_params[:initial_length] = 0 params = new_params end params end end end