File: array.rb

package info (click to toggle)
ruby-bindata 2.4.14-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 600 kB
  • sloc: ruby: 8,566; makefile: 4
file content (344 lines) | stat: -rw-r--r-- 8,934 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
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:
  #
  # <tt>:type</tt>::           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
  #                            <tt>[type_symbol, hash_params]</tt>.
  # <tt>:initial_length</tt>:: The initial length of the array.
  # <tt>:read_until</tt>::     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)
      return if self.equal?(array)  # prevent self assignment
      raise ArgumentError, "can't set a nil value for #{debug_name}" if array.nil?

      @element_list = []
      concat(array)
    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)
      abs_index = (index >= 0) ? index : index + 1 + length

      # insert elements before...
      new_elements = objs.map { new_element }
      elements.insert(index, *new_elements)

      # ...assigning values
      objs.each_with_index do |obj, i|
        self[abs_index + i] = obj
      end

      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 elements
      @element_list ||= []
    end

    def append_new_element
      element = new_element
      elements << element
      element
    end

    def new_element
      @element_prototype.instantiate(nil, 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