require 'bindata/base'
require 'bindata/dsl'
module BinData
# BinData declarations are evaluated in a single pass.
# However, some binary formats require multi pass processing. A common
# reason is seeking backwards in the input stream.
#
# DelayedIO supports multi pass processing. It works by ignoring the normal
# #read or #write calls. The user must explicitly call the #read_now! or
# #write_now! methods to process an additional pass. This additional pass
# must specify the abs_offset of the I/O operation.
#
# require 'bindata'
#
# obj = BinData::DelayedIO.new(read_abs_offset: 3, type: :uint16be)
# obj.read("\x00\x00\x00\x11\x12")
# obj #=> 0
#
# obj.read_now!
# obj #=> 0x1112
#
# - OR -
#
# obj.read("\x00\x00\x00\x11\x12") { obj.read_now! } #=> 0x1122
#
# obj.to_binary_s { obj.write_now! } #=> "\x00\x00\x00\x11\x12"
#
# You can use the +auto_call_delayed_io+ keyword to cause #read and #write to
# automatically perform the extra passes.
#
# class ReversePascalString < BinData::Record
# auto_call_delayed_io
#
# delayed_io :str, read_abs_offset: 0 do
# string read_length: :len
# end
# count_bytes_remaining :total_size
# skip to_abs_offset: -> { total_size - 1 }
# uint8 :len, value: -> { str.length }
# end
#
# s = ReversePascalString.read("hello\x05")
# s.to_binary_s #=> "hello\x05"
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# :read_abs_offset:: The abs_offset to start reading at.
# :type:: The single type inside the delayed io. Use
# a struct if multiple fields are required.
class DelayedIO < BinData::Base
extend DSLMixin
dsl_parser :delayed_io
arg_processor :delayed_io
mandatory_parameters :read_abs_offset, :type
def initialize_instance
@type = get_parameter(:type).instantiate(nil, self)
@abs_offset = nil
@read_io = nil
@write_io = nil
end
def clear?
@type.clear?
end
def assign(val)
@type.assign(val)
end
def snapshot
@type.snapshot
end
def num_bytes
@type.num_bytes
end
def respond_to?(symbol, include_private = false) #:nodoc:
@type.respond_to?(symbol, include_private) || super
end
def method_missing(symbol, *args, &block) #:nodoc:
@type.__send__(symbol, *args, &block)
end
def abs_offset
@abs_offset || eval_parameter(:read_abs_offset)
end
# Sets the +abs_offset+ to use when writing this object.
def abs_offset=(offset)
@abs_offset = offset
end
def rel_offset
abs_offset
end
def do_read(io) #:nodoc:
@read_io = io
end
def do_write(io) #:nodoc:
@write_io = io
end
def do_num_bytes #:nodoc:
0
end
def eval_parameter_with_delayed_io(key, overrides = nil)
result = eval_parameter_without_delayed_io(key, overrides)
# Delay processing :onlyif until we do the actual read/write
result = true if key == :onlyif && ! result
result
end
alias_method :eval_parameter_without_delayed_io, :eval_parameter
alias_method :eval_parameter, :eval_parameter_with_delayed_io
def include_obj?
! has_parameter?(:onlyif) || eval_parameter_without_delayed_io(:onlyif)
end
# DelayedIO objects aren't read when #read is called.
# The reading is delayed until this method is called.
def read_now!
return unless include_obj?
raise IOError, "read from where?" unless @read_io
@read_io.seekbytes(abs_offset - @read_io.offset)
start_read do
@type.do_read(@read_io)
end
end
# DelayedIO objects aren't written when #write is called.
# The writing is delayed until this method is called.
def write_now!
return unless include_obj?
raise IOError, "write to where?" unless @write_io
@write_io.seekbytes(abs_offset - @write_io.offset)
@type.do_write(@write_io)
end
end
class DelayedIoArgProcessor < BaseArgProcessor
include MultiFieldArgSeparator
def sanitize_parameters!(obj_class, params)
params.merge!(obj_class.dsl_params)
params.must_be_integer(:read_abs_offset)
params.sanitize_object_prototype(:type)
end
end
# Add +auto_call_delayed_io+ keyword to BinData::Base.
class Base
class << self
# The +auto_call_delayed_io+ keyword sets a data object tree to perform
# multi pass I/O automatically.
def auto_call_delayed_io
return if DelayedIO.method_defined? :initialize_instance_without_record_io
include AutoCallDelayedIO
DelayedIO.send(:alias_method, :initialize_instance_without_record_io, :initialize_instance)
DelayedIO.send(:define_method, :initialize_instance) do
if @parent && !defined? @delayed_io_recorded
@delayed_io_recorded = true
list = top_level_get(:delayed_ios)
list << self if list
end
initialize_instance_without_record_io
end
end
end
module AutoCallDelayedIO
def initialize_shared_instance
top_level_set(:delayed_ios, [])
super
end
def read(io)
super(io) { top_level_get(:delayed_ios).each(&:read_now!) }
end
def write(io, *_)
super(io) { top_level_get(:delayed_ios).each(&:write_now!) }
end
def num_bytes
to_binary_s.size
end
end
end
end