File: skip.rb

package info (click to toggle)
ruby-bindata 2.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 652 kB
  • sloc: ruby: 8,896; makefile: 4
file content (222 lines) | stat: -rw-r--r-- 5,868 bytes parent folder | download
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
require 'bindata/base_primitive'
require 'bindata/dsl'

module BinData
  # Skip will skip over bytes from the input stream.  If the stream is not
  # seekable, then the bytes are consumed and discarded.
  #
  # When writing, skip will write the appropriate number of zero bytes.
  #
  #   require 'bindata'
  #
  #   class A < BinData::Record
  #     skip length: 5
  #     string :a, read_length: 5
  #   end
  #
  #   obj = A.read("abcdefghij")
  #   obj.a #=> "fghij"
  #
  #
  #   class B < BinData::Record
  #     skip do
  #       string read_length: 2, assert: 'ef'
  #     end
  #     string :s, read_length: 5
  #   end
  #
  #   obj = B.read("abcdefghij")
  #   obj.s #=> "efghi"
  #
  #
  # == Parameters
  #
  # Skip objects accept all the params that BinData::BasePrimitive
  # does, as well as the following:
  #
  # <tt>:length</tt>::        The number of bytes to skip.
  # <tt>:to_abs_offset</tt>:: Skips to the given absolute offset.
  # <tt>:until_valid</tt>::   Skips until a given byte pattern is matched.
  #                           This parameter contains a type that will raise
  #                           a BinData::ValidityError unless an acceptable byte
  #                           sequence is found.  The type is represented by a
  #                           Symbol, or if the type is to have params
  #                           passed to it, then it should be provided as
  #                           <tt>[type_symbol, hash_params]</tt>.
  #
  class Skip < BinData::BasePrimitive
    extend DSLMixin

    dsl_parser    :skip
    arg_processor :skip

    optional_parameters :length, :to_abs_offset, :until_valid
    mutually_exclusive_parameters :length, :to_abs_offset, :until_valid

    def initialize_shared_instance
      extend SkipLengthPlugin      if has_parameter?(:length)
      extend SkipToAbsOffsetPlugin if has_parameter?(:to_abs_offset)
      extend SkipUntilValidPlugin  if has_parameter?(:until_valid)
      super
    end

    #---------------
    private

    def value_to_binary_string(_)
      len = skip_length
      if len.negative?
        raise ArgumentError,
              "#{debug_name} attempted to seek backwards by #{len.abs} bytes"
      end

      "\000" * skip_length
    end

    def read_and_return_value(io)
      len = skip_length
      if len.negative?
        raise ArgumentError,
              "#{debug_name} attempted to seek backwards by #{len.abs} bytes"
      end

      io.skipbytes(len)
      ""
    end

    def sensible_default
      ""
    end

    # Logic for the :length parameter
    module SkipLengthPlugin
      def skip_length
        eval_parameter(:length)
      end
    end

    # Logic for the :to_abs_offset parameter
    module SkipToAbsOffsetPlugin
      def skip_length
        eval_parameter(:to_abs_offset) - abs_offset
      end
    end

    # Logic for the :until_valid parameter
    module SkipUntilValidPlugin
      def skip_length
        @skip_length ||= 0
      end

      def read_and_return_value(io)
        prototype = get_parameter(:until_valid)
        validator = prototype.instantiate(nil, self)
        fs = fast_search_for_obj(validator)

        io.transform(ReadaheadIO.new) do |transformed_io, raw_io|
          pos = 0
          loop do
            seek_to_pos(pos, raw_io)
            validator.clear
            validator.do_read(transformed_io)
            break
          rescue ValidityError
            pos += 1

            if fs
              seek_to_pos(pos, raw_io)
              pos += next_search_index(raw_io, fs)
            end
          end

          seek_to_pos(pos, raw_io)
          @skip_length = pos
        end
      end

      def seek_to_pos(pos, io)
        io.rollback
        io.skip(pos)
      end

      # A fast search has a pattern string at a specific offset.
      FastSearch = ::Struct.new('FastSearch', :pattern, :offset)

      def fast_search_for(obj)
        if obj.respond_to?(:asserted_binary_s)
          FastSearch.new(obj.asserted_binary_s, obj.rel_offset)
        else
          nil
        end
      end

      # If a search object has an +asserted_value+ field then we
      # perform a faster search for a valid object.
      def fast_search_for_obj(obj)
        if BinData::Struct === obj
          obj.each_pair(true) do |_, field|
            fs = fast_search_for(field)
            return fs if fs
          end
        elsif BinData::BasePrimitive === obj
          return fast_search_for(obj)
        end

        nil
      end

      SEARCH_SIZE = 100_000

      def next_search_index(io, fs)
        buffer = binary_string("")

        # start searching at fast_search offset
        pos = fs.offset
        io.skip(fs.offset)

        loop do
          data = io.read(SEARCH_SIZE)
          raise EOFError, "no match" if data.nil?

          buffer << data
          index = buffer.index(fs.pattern)
          if index
            return pos + index - fs.offset
          end

          # advance buffer
          searched = buffer.slice!(0..-fs.pattern.size)
          pos += searched.size
        end
      end

      class ReadaheadIO < BinData::IO::Transform
        def before_transform
          if !seekable?
            raise IOError, "readahead is not supported on unseekable streams"
          end

          @mark = offset
        end

        def rollback
          seek_abs(@mark)
        end
      end
    end
  end

  class SkipArgProcessor < BaseArgProcessor
    def sanitize_parameters!(obj_class, params)
      params.merge!(obj_class.dsl_params)

      unless params.has_at_least_one_of?(:length, :to_abs_offset, :until_valid)
        raise ArgumentError,
              "#{obj_class} requires :length, :to_abs_offset or :until_valid"
      end

      params.must_be_integer(:to_abs_offset, :length)
      params.sanitize_object_prototype(:until_valid)
    end
  end
end