require 'bindata/base'
require 'bindata/dsl'
module BinData
# A Buffer is conceptually a substream within a data stream. It has a
# defined size and it will always read or write the exact number of bytes to
# fill the buffer. Short reads will skip over unused bytes and short writes
# will pad the substream with "\0" bytes.
#
# require 'bindata'
#
# obj = BinData::Buffer.new(length: 5, type: [:string, {value: "abc"}])
# obj.to_binary_s #=> "abc\000\000"
#
#
# class MyBuffer < BinData::Buffer
# default_parameter length: 8
#
# endian :little
#
# uint16 :num1
# uint16 :num2
# # padding occurs here
# end
#
# obj = MyBuffer.read("\001\000\002\000\000\000\000\000")
# obj.num1 #=> 1
# obj.num1 #=> 2
# obj.raw_num_bytes #=> 4
# obj.num_bytes #=> 8
#
#
# class StringTable < BinData::Record
# endian :little
#
# uint16 :table_size_in_bytes
# buffer :strings, length: :table_size_in_bytes do
# array read_until: :eof do
# uint8 :len
# string :str, length: :len
# end
# end
# end
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# :length:: The number of bytes in the buffer.
# :type:: The single type inside the buffer. Use a struct if
# multiple fields are required.
class Buffer < BinData::Base
extend DSLMixin
dsl_parser :buffer
arg_processor :buffer
mandatory_parameters :length, :type
def initialize_instance
@type = get_parameter(:type).instantiate(nil, self)
end
# The number of bytes used, ignoring the padding imposed by the buffer.
def raw_num_bytes
@type.num_bytes
end
def clear?
@type.clear?
end
def assign(val)
@type.assign(val)
end
def snapshot
@type.snapshot
end
def respond_to_missing?(symbol, include_all = false) # :nodoc:
@type.respond_to?(symbol, include_all) || super
end
def method_missing(symbol, *args, &block) # :nodoc:
@type.__send__(symbol, *args, &block)
end
def do_read(io) # :nodoc:
buf_len = eval_parameter(:length)
io.transform(BufferIO.new(buf_len)) do |transformed_io, _|
@type.do_read(transformed_io)
end
end
def do_write(io) # :nodoc:
buf_len = eval_parameter(:length)
io.transform(BufferIO.new(buf_len)) do |transformed_io, _|
@type.do_write(transformed_io)
end
end
def do_num_bytes # :nodoc:
eval_parameter(:length)
end
# Transforms the IO stream to restrict access inside
# a buffer of specified length.
class BufferIO < IO::Transform
def initialize(length)
super()
@bytes_remaining = length
end
def before_transform
@buf_start = offset
@buf_end = @buf_start + @bytes_remaining
end
def num_bytes_remaining
[@bytes_remaining, super].min
rescue IOError
@bytes_remaining
end
def skip(n)
nbytes = buffer_limited_n(n)
@bytes_remaining -= nbytes
chain_skip(nbytes)
end
def seek_abs(n)
if n < @buf_start || n >= @buf_end
raise IOError, "can not seek to abs_offset outside of buffer"
end
@bytes_remaining -= (n - offset)
chain_seek_abs(n)
end
def read(n)
nbytes = buffer_limited_n(n)
@bytes_remaining -= nbytes
chain_read(nbytes)
end
def write(data)
nbytes = buffer_limited_n(data.size)
@bytes_remaining -= nbytes
if nbytes < data.size
data = data[0, nbytes]
end
chain_write(data)
end
def after_read_transform
read(nil)
end
def after_write_transform
write("\x00" * @bytes_remaining)
end
def buffer_limited_n(n)
if n.nil?
@bytes_remaining
elsif n.positive?
limit = @bytes_remaining
n > limit ? limit : n
# uncomment if we decide to allow backwards skipping
# elsif n.negative?
# limit = @bytes_remaining + @buf_start - @buf_end
# n < limit ? limit : n
else
0
end
end
end
end
class BufferArgProcessor < BaseArgProcessor
include MultiFieldArgSeparator
def sanitize_parameters!(obj_class, params)
params.merge!(obj_class.dsl_params)
params.must_be_integer(:length)
params.sanitize_object_prototype(:type)
end
end
end