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