lib/bindata/skip.rb
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