# BinData::Struct.new(name: :my_struct, fields: ...)
# array = BinData::Array.new(type: :my_struct)
#
module RegisterNamePlugin
def self.included(base) # :nodoc:
# The registered name may be provided explicitly.
base.optional_parameter :name
end
def initialize_shared_instance
if has_parameter?(:name)
RegisteredClasses.register(get_parameter(:name), self)
end
super
end
end
end
bindata-2.5.1/lib/bindata/buffer.rb 0000644 0000041 0000041 00000011100 15000064511 017100 0 ustar www-data www-data 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
bindata-2.5.1/lib/bindata/io.rb 0000644 0000041 0000041 00000030413 15000064511 016246 0 ustar www-data www-data require 'stringio'
module BinData
# A wrapper around an IO object. The wrapper provides a consistent
# interface for BinData objects to use when accessing the IO.
module IO
# Creates a StringIO around +str+.
def self.create_string_io(str = "")
bin_str = str.dup.force_encoding(Encoding::BINARY)
StringIO.new(bin_str).tap(&:binmode)
end
# Create a new IO Read wrapper around +io+. +io+ must provide #read,
# #pos if reading the current stream position and #seek if setting the
# current stream position. If +io+ is a string it will be automatically
# wrapped in an StringIO object.
#
# The IO can handle bitstreams in either big or little endian format.
#
# M byte1 L M byte2 L
# S 76543210 S S fedcba98 S
# B B B B
#
# In big endian format:
# readbits(6), readbits(5) #=> [765432, 10fed]
#
# In little endian format:
# readbits(6), readbits(5) #=> [543210, a9876]
#
class Read
def initialize(io)
if self.class === io
raise ArgumentError, "io must not be a #{self.class}"
end
# wrap strings in a StringIO
if io.respond_to?(:to_str)
io = BinData::IO.create_string_io(io.to_str)
end
@io = RawIO.new(io)
# bits when reading
@rnbits = 0
@rval = 0
@rendian = nil
end
# Allow transforming data in the input stream.
# See +BinData::Buffer+ as an example.
#
# +io+ must be an instance of +Transform+.
#
# yields +self+ and +io+ to the given block
def transform(io)
reset_read_bits
saved = @io
@io = io.prepend_to_chain(@io)
yield(self, io)
io.after_read_transform
ensure
@io = saved
end
# The number of bytes remaining in the io steam.
def num_bytes_remaining
@io.num_bytes_remaining
end
# Seek +n+ bytes from the current position in the io stream.
def skipbytes(n)
reset_read_bits
@io.skip(n)
end
# Seek to an absolute offset within the io stream.
def seek_to_abs_offset(n)
reset_read_bits
@io.seek_abs(n)
end
# Reads exactly +n+ bytes from +io+.
#
# If the data read is nil an EOFError is raised.
#
# If the data read is too short an IOError is raised.
def readbytes(n)
reset_read_bits
read(n)
end
# Reads all remaining bytes from the stream.
def read_all_bytes
reset_read_bits
read
end
# Reads exactly +nbits+ bits from the stream. +endian+ specifies whether
# the bits are stored in +:big+ or +:little+ endian format.
def readbits(nbits, endian)
if @rendian != endian
# don't mix bits of differing endian
reset_read_bits
@rendian = endian
end
if endian == :big
read_big_endian_bits(nbits)
else
read_little_endian_bits(nbits)
end
end
# Discards any read bits so the stream becomes aligned at the
# next byte boundary.
def reset_read_bits
@rnbits = 0
@rval = 0
end
#---------------
private
def read(n = nil)
str = @io.read(n)
if n
raise EOFError, "End of file reached" if str.nil?
raise IOError, "data truncated" if str.size < n
end
str
end
def read_big_endian_bits(nbits)
while @rnbits < nbits
accumulate_big_endian_bits
end
val = (@rval >> (@rnbits - nbits)) & mask(nbits)
@rnbits -= nbits
@rval &= mask(@rnbits)
val
end
def accumulate_big_endian_bits
byte = read(1).unpack1('C') & 0xff
@rval = (@rval << 8) | byte
@rnbits += 8
end
def read_little_endian_bits(nbits)
while @rnbits < nbits
accumulate_little_endian_bits
end
val = @rval & mask(nbits)
@rnbits -= nbits
@rval >>= nbits
val
end
def accumulate_little_endian_bits
byte = read(1).unpack1('C') & 0xff
@rval = @rval | (byte << @rnbits)
@rnbits += 8
end
def mask(nbits)
(1 << nbits) - 1
end
end
# Create a new IO Write wrapper around +io+. +io+ must provide #write.
# If +io+ is a string it will be automatically wrapped in an StringIO
# object.
#
# The IO can handle bitstreams in either big or little endian format.
#
# See IO::Read for more information.
class Write
def initialize(io)
if self.class === io
raise ArgumentError, "io must not be a #{self.class}"
end
# wrap strings in a StringIO
if io.respond_to?(:to_str)
io = BinData::IO.create_string_io(io.to_str)
end
@io = RawIO.new(io)
@wnbits = 0
@wval = 0
@wendian = nil
end
# Allow transforming data in the output stream.
# See +BinData::Buffer+ as an example.
#
# +io+ must be an instance of +Transform+.
#
# yields +self+ and +io+ to the given block
def transform(io)
flushbits
saved = @io
@io = io.prepend_to_chain(@io)
yield(self, io)
io.after_write_transform
ensure
@io = saved
end
# Seek to an absolute offset within the io stream.
def seek_to_abs_offset(n)
raise IOError, "stream is unseekable" unless @io.seekable?
flushbits
@io.seek_abs(n)
end
# Writes the given string of bytes to the io stream.
def writebytes(str)
flushbits
write(str)
end
# Writes +nbits+ bits from +val+ to the stream. +endian+ specifies whether
# the bits are to be stored in +:big+ or +:little+ endian format.
def writebits(val, nbits, endian)
if @wendian != endian
# don't mix bits of differing endian
flushbits
@wendian = endian
end
clamped_val = val & mask(nbits)
if endian == :big
write_big_endian_bits(clamped_val, nbits)
else
write_little_endian_bits(clamped_val, nbits)
end
end
# To be called after all +writebits+ have been applied.
def flushbits
raise "Internal state error nbits = #{@wnbits}" if @wnbits >= 8
if @wnbits > 0
writebits(0, 8 - @wnbits, @wendian)
end
end
alias flush flushbits
#---------------
private
def write(data)
@io.write(data)
end
def write_big_endian_bits(val, nbits)
while nbits > 0
bits_req = 8 - @wnbits
if nbits >= bits_req
msb_bits = (val >> (nbits - bits_req)) & mask(bits_req)
nbits -= bits_req
val &= mask(nbits)
@wval = (@wval << bits_req) | msb_bits
write(@wval.chr)
@wval = 0
@wnbits = 0
else
@wval = (@wval << nbits) | val
@wnbits += nbits
nbits = 0
end
end
end
def write_little_endian_bits(val, nbits)
while nbits > 0
bits_req = 8 - @wnbits
if nbits >= bits_req
lsb_bits = val & mask(bits_req)
nbits -= bits_req
val >>= bits_req
@wval = @wval | (lsb_bits << @wnbits)
write(@wval.chr)
@wval = 0
@wnbits = 0
else
@wval = @wval | (val << @wnbits)
@wnbits += nbits
nbits = 0
end
end
end
def mask(nbits)
(1 << nbits) - 1
end
end
# API used to access the raw data stream.
class RawIO
def initialize(io)
@io = io
@pos = 0
if is_seekable?(io)
@initial_pos = io.pos
else
singleton_class.prepend(UnSeekableIO)
end
end
def is_seekable?(io)
io.pos
rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE, Errno::EINVAL
nil
end
def seekable?
true
end
def num_bytes_remaining
start_mark = @io.pos
@io.seek(0, ::IO::SEEK_END)
end_mark = @io.pos
@io.seek(start_mark, ::IO::SEEK_SET)
end_mark - start_mark
end
def offset
@pos
end
def skip(n)
raise IOError, "can not skip backwards" if n.negative?
@io.seek(n, ::IO::SEEK_CUR)
@pos += n
end
def seek_abs(n)
@io.seek(n + @initial_pos, ::IO::SEEK_SET)
@pos = n
end
def read(n)
@io.read(n).tap { |data| @pos += (data&.size || 0) }
end
def write(data)
@io.write(data)
end
end
# An IO stream may be transformed before processing.
# e.g. encoding, compression, buffered.
#
# Multiple transforms can be chained together.
#
# To create a new transform layer, subclass +Transform+.
# Override the public methods +#read+ and +#write+ at a minimum.
# Additionally the hook, +#before_transform+, +#after_read_transform+
# and +#after_write_transform+ are available as well.
#
# IMPORTANT! If your transform changes the size of the underlying
# data stream (e.g. compression), then call
# +::transform_changes_stream_length!+ in your subclass.
class Transform
class << self
# Indicates that this transform changes the length of the
# underlying data. e.g. performs compression or error correction
def transform_changes_stream_length!
prepend(UnSeekableIO)
end
end
def initialize
@chain_io = nil
end
# Initialises this transform.
#
# Called before any IO operations.
def before_transform; end
# Flushes the input stream.
#
# Called after the final read operation.
def after_read_transform; end
# Flushes the output stream.
#
# Called after the final write operation.
def after_write_transform; end
# Prepends this transform to the given +chain+.
#
# Returns self (the new head of chain).
def prepend_to_chain(chain)
@chain_io = chain
before_transform
self
end
# Is the IO seekable?
def seekable?
@chain_io.seekable?
end
# How many bytes are available for reading?
def num_bytes_remaining
chain_num_bytes_remaining
end
# The current offset within the stream.
def offset
chain_offset
end
# Skips forward +n+ bytes in the input stream.
def skip(n)
chain_skip(n)
end
# Seeks to the given absolute position.
def seek_abs(n)
chain_seek_abs(n)
end
# Reads +n+ bytes from the stream.
def read(n)
chain_read(n)
end
# Writes +data+ to the stream.
def write(data)
chain_write(data)
end
#-------------
private
def create_empty_binary_string
String.new.force_encoding(Encoding::BINARY)
end
def chain_seekable?
@chain_io.seekable?
end
def chain_num_bytes_remaining
@chain_io.num_bytes_remaining
end
def chain_offset
@chain_io.offset
end
def chain_skip(n)
@chain_io.skip(n)
end
def chain_seek_abs(n)
@chain_io.seek_abs(n)
end
def chain_read(n)
@chain_io.read(n)
end
def chain_write(data)
@chain_io.write(data)
end
end
# A module to be prepended to +RawIO+ or +Transform+ when the data
# stream is not seekable. This is either due to underlying stream
# being unseekable or the transform changes the number of bytes.
module UnSeekableIO
def seekable?
false
end
def num_bytes_remaining
raise IOError, "stream is unseekable"
end
def skip(n)
raise IOError, "can not skip backwards" if n.negative?
# skip over data in 8k blocks
while n > 0
bytes_to_read = [n, 8192].min
read(bytes_to_read)
n -= bytes_to_read
end
end
def seek_abs(n)
skip(n - offset)
end
end
end
end
bindata-2.5.1/lib/bindata/uint8_array.rb 0000644 0000041 0000041 00000003524 15000064511 020107 0 ustar www-data www-data require 'bindata/base_primitive'
module BinData
# Uint8Array is a specialised type of array that only contains
# bytes (Uint8). It is a faster and more memory efficient version
# of `BinData::Array.new(:type => :uint8)`.
#
# require 'bindata'
#
# obj = BinData::Uint8Array.new(initial_length: 5)
# obj.read("abcdefg") #=> [97, 98, 99, 100, 101]
# obj[2] #=> 99
# obj.collect { |x| x.chr }.join #=> "abcde"
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# :initial_length:: The initial length of the array.
# :read_until:: May only have a value of `:eof`. This parameter
# instructs the array to read as much data from
# the stream as possible.
class Uint8Array < BinData::BasePrimitive
optional_parameters :initial_length, :read_until
mutually_exclusive_parameters :initial_length, :read_until
arg_processor :uint8_array
#---------------
private
def value_to_binary_string(val)
val.pack("C*")
end
def read_and_return_value(io)
if has_parameter?(:initial_length)
data = io.readbytes(eval_parameter(:initial_length))
else
data = io.read_all_bytes
end
data.unpack("C*")
end
def sensible_default
[]
end
end
class Uint8ArrayArgProcessor < 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
msg = "Parameter :read_until must have a value of :eof"
params.sanitize(:read_until) { |val| raise ArgumentError, msg unless val == :eof }
end
end
end
bindata-2.5.1/lib/bindata/base.rb 0000644 0000041 0000041 00000021022 15000064511 016545 0 ustar www-data www-data require 'bindata/framework'
require 'bindata/io'
require 'bindata/lazy'
require 'bindata/name'
require 'bindata/params'
require 'bindata/registry'
require 'bindata/sanitize'
module BinData
# This is the abstract base class for all data objects.
class Base
extend AcceptedParametersPlugin
include Framework
include RegisterNamePlugin
class << self
# Instantiates this class and reads from +io+, returning the newly
# created data object. +args+ will be used when instantiating.
def read(io, *args, &block)
obj = new(*args)
obj.read(io, &block)
obj
end
# The arg processor for this class.
def arg_processor(name = nil)
@arg_processor ||= nil
if name
@arg_processor = "#{name}_arg_processor".gsub(/(?:^|_)(.)/) { $1.upcase }.to_sym
elsif @arg_processor.is_a? Symbol
@arg_processor = BinData.const_get(@arg_processor).new
elsif @arg_processor.nil?
@arg_processor = superclass.arg_processor
else
@arg_processor
end
end
# The name of this class as used by Records, Arrays etc.
def bindata_name
RegisteredClasses.underscore_name(name)
end
# Call this method if this class is abstract and not to be used.
def unregister_self
RegisteredClasses.unregister(name)
end
# Registers all subclasses of this class for use
def register_subclasses # :nodoc:
singleton_class.send(:undef_method, :inherited)
define_singleton_method(:inherited) do |subclass|
RegisteredClasses.register(subclass.name, subclass)
register_subclasses
end
end
private :unregister_self, :register_subclasses
end
# Register all subclasses of this class.
register_subclasses
# Set the initial arg processor.
arg_processor :base
# Creates a new data object.
#
# Args are optional, but if present, must be in the following order.
#
# +value+ is a value that is +assign+ed immediately after initialization.
#
# +parameters+ is a hash containing symbol keys. Some parameters may
# reference callable objects (methods or procs).
#
# +parent+ is the parent data object (e.g. struct, array, choice) this
# object resides under.
#
def initialize(*args)
value, @params, @parent = extract_args(args)
initialize_shared_instance
initialize_instance
assign(value) if value
end
attr_accessor :parent
protected :parent=
# Creates a new data object based on this instance.
#
# This implements the prototype design pattern.
#
# All parameters will be be duplicated. Use this method
# when creating multiple objects with the same parameters.
def new(value = nil, parent = nil)
obj = clone
obj.parent = parent if parent
obj.initialize_instance
obj.assign(value) if value
obj
end
# Returns the result of evaluating the parameter identified by +key+.
#
# +overrides+ is an optional +parameters+ like hash that allow the
# parameters given at object construction to be overridden.
#
# Returns nil if +key+ does not refer to any parameter.
def eval_parameter(key, overrides = nil)
value = get_parameter(key)
if value.is_a?(Symbol) || value.respond_to?(:arity)
lazy_evaluator.lazy_eval(value, overrides)
else
value
end
end
# Returns a lazy evaluator for this object.
def lazy_evaluator # :nodoc:
@lazy_evaluator ||= LazyEvaluator.new(self)
end
# Returns the parameter referenced by +key+.
# Use this method if you are sure the parameter is not to be evaluated.
# You most likely want #eval_parameter.
def get_parameter(key)
@params[key]
end
# Returns whether +key+ exists in the +parameters+ hash.
def has_parameter?(key)
@params.has_parameter?(key)
end
# Resets the internal state to that of a newly created object.
def clear
initialize_instance
end
# Reads data into this data object.
def read(io, &block)
io = BinData::IO::Read.new(io) unless BinData::IO::Read === io
start_read do
clear
do_read(io)
end
block.call(self) if block_given?
self
end
# Writes the value for this data object to +io+.
def write(io, &block)
io = BinData::IO::Write.new(io) unless BinData::IO::Write === io
do_write(io)
io.flush
block.call(self) if block_given?
self
end
# Returns the number of bytes it will take to write this data object.
def num_bytes
do_num_bytes.ceil
end
# Returns the string representation of this data object.
def to_binary_s(&block)
io = BinData::IO.create_string_io
write(io, &block)
io.string
end
# Returns the hexadecimal string representation of this data object.
def to_hex(&block)
to_binary_s(&block).unpack1('H*')
end
# Return a human readable representation of this data object.
def inspect
snapshot.inspect
end
# Return a string representing this data object.
def to_s
snapshot.to_s
end
# Work with Ruby's pretty-printer library.
def pretty_print(pp) # :nodoc:
pp.pp(snapshot)
end
# Override and delegate =~ as it is defined in Object.
def =~(other)
snapshot =~ other
end
# Returns a user friendly name of this object for debugging purposes.
def debug_name
@parent ? @parent.debug_name_of(self) : 'obj'
end
# Returns the offset (in bytes) of this object with respect to its most
# distant ancestor.
def abs_offset
@parent ? @parent.abs_offset + @parent.offset_of(self) : 0
end
# Returns the offset (in bytes) of this object with respect to its parent.
def rel_offset
@parent ? @parent.offset_of(self) : 0
end
def ==(other) # :nodoc:
# double dispatch
other == snapshot
end
# A version of +respond_to?+ used by the lazy evaluator. It doesn't
# reinvoke the evaluator so as to avoid infinite evaluation loops.
def safe_respond_to?(symbol, include_private = false) # :nodoc:
base_respond_to?(symbol, include_private)
end
alias base_respond_to? respond_to?
#---------------
private
def extract_args(args)
self.class.arg_processor.extract_args(self.class, args)
end
def start_read
top_level_set(:in_read, true)
yield
ensure
top_level_set(:in_read, false)
end
# Is this object tree currently being read? Used by BasePrimitive.
def reading?
top_level_get(:in_read)
end
def top_level_set(sym, value)
top_level.instance_variable_set("@tl_#{sym}", value)
end
def top_level_get(sym)
tl = top_level
tl.instance_variable_defined?("@tl_#{sym}") &&
tl.instance_variable_get("@tl_#{sym}")
end
def top_level
if parent.nil?
tl = self
else
tl = parent
tl = tl.parent while tl.parent
end
tl
end
def binary_string(str)
str.to_s.dup.force_encoding(Encoding::BINARY)
end
end
# ArgProcessors process the arguments passed to BinData::Base.new into
# the form required to initialise the BinData object.
#
# Any passed parameters are sanitized so the BinData object doesn't
# need to perform error checking on the parameters.
class BaseArgProcessor
@@empty_hash = Hash.new.freeze
# Takes the arguments passed to BinData::Base.new and
# extracts [value, sanitized_parameters, parent].
def extract_args(obj_class, obj_args)
value, params, parent = separate_args(obj_class, obj_args)
sanitized_params = SanitizedParameters.sanitize(params, obj_class)
[value, sanitized_params, parent]
end
# Separates the arguments passed to BinData::Base.new into
# [value, parameters, parent]. Called by #extract_args.
def separate_args(_obj_class, obj_args)
args = obj_args.dup
value = parameters = parent = nil
if args.length > 1 && args.last.is_a?(BinData::Base)
parent = args.pop
end
if args.length > 0 && args.last.is_a?(Hash)
parameters = args.pop
end
if args.length > 0
value = args.pop
end
parameters ||= @@empty_hash
[value, parameters, parent]
end
# Performs sanity checks on the given parameters.
# This method converts the parameters to the form expected
# by the data object.
def sanitize_parameters!(obj_class, obj_params); end
end
end
bindata-2.5.1/lib/bindata/bits.rb 0000644 0000041 0000041 00000010725 15000064511 016604 0 ustar www-data www-data require 'thread'
require 'bindata/base_primitive'
module BinData
# Defines a number of classes that contain a bit based integer.
# The integer is defined by endian and number of bits.
module BitField # :nodoc: all
@@mutex = Mutex.new
class << self
def define_class(name, nbits, endian, signed = :unsigned)
@@mutex.synchronize do
unless BinData.const_defined?(name)
new_class = Class.new(BinData::BasePrimitive)
BitField.define_methods(new_class, nbits, endian.to_sym, signed.to_sym)
RegisteredClasses.register(name, new_class)
BinData.const_set(name, new_class)
end
end
BinData.const_get(name)
end
def define_methods(bit_class, nbits, endian, signed)
bit_class.module_eval <<-END
#{create_params_code(nbits)}
def assign(val)
#{create_nbits_code(nbits)}
#{create_clamp_code(nbits, signed)}
super(val)
end
def do_write(io)
#{create_nbits_code(nbits)}
val = _value
#{create_int2uint_code(nbits, signed)}
io.writebits(val, #{nbits}, :#{endian})
end
def do_num_bytes
#{create_nbits_code(nbits)}
#{create_do_num_bytes_code(nbits)}
end
def bit_aligned?
true
end
#---------------
private
def read_and_return_value(io)
#{create_nbits_code(nbits)}
val = io.readbits(#{nbits}, :#{endian})
#{create_uint2int_code(nbits, signed)}
val
end
def sensible_default
0
end
END
end
def create_params_code(nbits)
if nbits == :nbits
"mandatory_parameter :nbits"
else
""
end
end
def create_nbits_code(nbits)
if nbits == :nbits
"nbits = eval_parameter(:nbits)"
else
""
end
end
def create_do_num_bytes_code(nbits)
if nbits == :nbits
"nbits / 8.0"
else
nbits / 8.0
end
end
def create_clamp_code(nbits, signed)
if nbits == :nbits
create_dynamic_clamp_code(signed)
else
create_fixed_clamp_code(nbits, signed)
end
end
def create_dynamic_clamp_code(signed)
if signed == :signed
max = "(1 << (nbits - 1)) - 1"
min = "-((#{max}) + 1)"
else
max = "(1 << nbits) - 1"
min = "0"
end
"val = val.clamp(#{min}, #{max})"
end
def create_fixed_clamp_code(nbits, signed)
if nbits == 1 && signed == :signed
raise "signed bitfield must have more than one bit"
end
if signed == :signed
max = "(1 << (#{nbits} - 1)) - 1"
min = "-((#{max}) + 1)"
else
min = "0"
max = "(1 << #{nbits}) - 1"
end
clamp = "(val = val.clamp(#{min}, #{max}))"
if nbits == 1
# allow single bits to be used as booleans
clamp = "(val == true) ? 1 : (not val) ? 0 : #{clamp}"
end
"val = #{clamp}"
end
def create_int2uint_code(nbits, signed)
if signed != :signed
""
elsif nbits == :nbits
"val &= (1 << nbits) - 1"
else
"val &= #{(1 << nbits) - 1}"
end
end
def create_uint2int_code(nbits, signed)
if signed != :signed
""
elsif nbits == :nbits
"val -= (1 << nbits) if (val >= (1 << (nbits - 1)))"
else
"val -= #{1 << nbits} if (val >= #{1 << (nbits - 1)})"
end
end
end
end
# Create classes for dynamic bitfields
{
'Bit' => :big,
'BitLe' => :little,
'Sbit' => [:big, :signed],
'SbitLe' => [:little, :signed]
}.each_pair { |name, args| BitField.define_class(name, :nbits, *args) }
# Create classes on demand
module BitFieldFactory
def const_missing(name)
mappings = {
/^Bit(\d+)$/ => :big,
/^Bit(\d+)le$/ => :little,
/^Sbit(\d+)$/ => [:big, :signed],
/^Sbit(\d+)le$/ => [:little, :signed]
}
mappings.each_pair do |regex, args|
if regex =~ name.to_s
nbits = $1.to_i
return BitField.define_class(name, nbits, *args)
end
end
super(name)
end
end
BinData.extend BitFieldFactory
end
bindata-2.5.1/lib/bindata/count_bytes_remaining.rb 0000644 0000041 0000041 00000001335 15000064511 022227 0 ustar www-data www-data require 'bindata/base_primitive'
module BinData
# Counts the number of bytes remaining in the input stream from the current
# position to the end of the stream. This only makes sense for seekable
# streams.
#
# require 'bindata'
#
# class A < BinData::Record
# count_bytes_remaining :bytes_remaining
# string :all_data, read_length: :bytes_remaining
# end
#
# obj = A.read("abcdefghij")
# obj.all_data #=> "abcdefghij"
#
class CountBytesRemaining < BinData::BasePrimitive
#---------------
private
def value_to_binary_string(val)
""
end
def read_and_return_value(io)
io.num_bytes_remaining
end
def sensible_default
0
end
end
end
bindata-2.5.1/lib/bindata/alignment.rb 0000644 0000041 0000041 00000003356 15000064511 017623 0 ustar www-data www-data require 'bindata/base_primitive'
module BinData
# Resets the stream alignment to the next byte. This is
# only useful when using bit-based primitives.
#
# class MyRec < BinData::Record
# bit4 :a
# resume_byte_alignment
# bit4 :b
# end
#
# MyRec.read("\x12\x34") #=> {"a" => 1, "b" => 3}
#
class ResumeByteAlignment < BinData::Base
def clear?; true; end
def assign(val); end
def snapshot; nil; end
def do_num_bytes; 0; end
def do_read(io)
io.readbytes(0)
end
def do_write(io)
io.writebytes("")
end
end
# A monkey patch to force byte-aligned primitives to
# become bit-aligned. This allows them to be used at
# non byte based boundaries.
#
# class BitString < BinData::String
# bit_aligned
# end
#
# class MyRecord < BinData::Record
# bit4 :preamble
# bit_string :str, length: 2
# end
#
module BitAligned
class BitAlignedIO
def initialize(io)
@io = io
end
def binary_string(str)
str.to_s.dup.force_encoding(Encoding::BINARY)
end
def readbytes(n)
n.times.inject(binary_string("")) do |bytes, _|
bytes + @io.readbits(8, :big).chr
end
end
def writebytes(str)
str.each_byte { |v| @io.writebits(v, 8, :big) }
end
end
def bit_aligned?
true
end
def do_read(io)
super(BitAlignedIO.new(io))
end
def do_num_bytes
super.to_f
end
def do_write(io)
super(BitAlignedIO.new(io))
end
end
def BasePrimitive.bit_aligned
include BitAligned
end
def Primitive.bit_aligned
fail "'bit_aligned' is not supported for BinData::Primitives"
end
end
bindata-2.5.1/lib/bindata/base_primitive.rb 0000644 0000041 0000041 00000016054 15000064511 020646 0 ustar www-data www-data require 'bindata/base'
module BinData
# A BinData::BasePrimitive object is a container for a value that has a
# particular binary representation. A value corresponds to a primitive type
# such as as integer, float or string. Only one value can be contained by
# this object. This value can be read from or written to an IO stream.
#
# require 'bindata'
#
# obj = BinData::Uint8.new(initial_value: 42)
# obj #=> 42
# obj.assign(5)
# obj #=> 5
# obj.clear
# obj #=> 42
#
# obj = BinData::Uint8.new(value: 42)
# obj #=> 42
# obj.assign(5)
# obj #=> 42
#
# obj = BinData::Uint8.new(assert: 3)
# obj.read("\005") #=> BinData::ValidityError: value is '5' but expected '3'
#
# obj = BinData::Uint8.new(assert: -> { value < 5 })
# obj.read("\007") #=> BinData::ValidityError: value not as expected
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params include those for BinData::Base as well as:
#
# [:initial_value] This is the initial value to use before one is
# either #read or explicitly set with #value=.
# [:value] The object will always have this value.
# Calls to #value= are ignored when
# using this param. While reading, #value
# will return the value of the data read from the
# IO, not the result of the :value param.
# [:assert] Raise an error unless the value read or assigned
# meets this criteria. The variable +value+ is
# made available to any lambda assigned to this
# parameter. A boolean return indicates success
# or failure. Any other return is compared to
# the value just read in.
# [:asserted_value] Equivalent to :assert and :value.
#
class BasePrimitive < BinData::Base
unregister_self
optional_parameters :initial_value, :value, :assert, :asserted_value
mutually_exclusive_parameters :initial_value, :value
mutually_exclusive_parameters :asserted_value, :value, :assert
def initialize_shared_instance
extend InitialValuePlugin if has_parameter?(:initial_value)
extend ValuePlugin if has_parameter?(:value)
extend AssertPlugin if has_parameter?(:assert)
extend AssertedValuePlugin if has_parameter?(:asserted_value)
super
end
def initialize_instance
@value = nil
end
def clear? # :nodoc:
@value.nil?
end
def assign(val)
raise ArgumentError, "can't set a nil value for #{debug_name}" if val.nil?
raw_val = val.respond_to?(:snapshot) ? val.snapshot : val
@value = raw_val.dup
end
def snapshot
_value
end
def value
snapshot
end
def value=(val)
assign(val)
end
def respond_to_missing?(symbol, include_all = false) # :nodoc:
child = snapshot
child.respond_to?(symbol, include_all) || super
end
def method_missing(symbol, *args, &block) # :nodoc:
child = snapshot
if child.respond_to?(symbol)
self.class.class_eval <<-END, __FILE__, __LINE__ + 1
def #{symbol}(*args, &block) # def clamp(*args, &block)
snapshot.#{symbol}(*args, &block) # snapshot.clamp(*args, &block)
end # end
END
child.__send__(symbol, *args, &block)
else
super
end
end
def <=>(other)
snapshot <=> other
end
def eql?(other)
# double dispatch
other.eql?(snapshot)
end
def hash
snapshot.hash
end
def do_read(io) # :nodoc:
@value = read_and_return_value(io)
end
def do_write(io) # :nodoc:
io.writebytes(value_to_binary_string(_value))
end
def do_num_bytes # :nodoc:
value_to_binary_string(_value).length
end
#---------------
private
# The unmodified value of this data object. Note that #snapshot calls this
# method. This indirection is so that #snapshot can be overridden in
# subclasses to modify the presentation value.
def _value
@value != nil ? @value : sensible_default
end
# Logic for the :value parameter
module ValuePlugin
def assign(val)
# Ignored
end
def _value
reading? ? @value : eval_parameter(:value)
end
end
# Logic for the :initial_value parameter
module InitialValuePlugin
def _value
@value != nil ? @value : eval_parameter(:initial_value)
end
end
# Logic for the :assert parameter
module AssertPlugin
def assign(val)
super(val)
assert!
end
def do_read(io) # :nodoc:
super(io)
assert!
end
def assert!
current_value = snapshot
expected = eval_parameter(:assert, value: current_value)
msg =
if !expected
"value '#{current_value}' not as expected"
elsif expected != true && current_value != expected
"value is '#{current_value}' but expected '#{expected}'"
else
nil
end
raise ValidityError, "#{msg} for #{debug_name}" if msg
end
end
# Logic for the :asserted_value parameter
module AssertedValuePlugin
def assign(val)
assert_value(val)
super(val)
end
def _value
reading? ? @value : eval_parameter(:asserted_value)
end
# The asserted value as a binary string.
#
# Rationale: while reading, +#to_binary_s+ will use the
# value read in, rather than the +:asserted_value+.
# This feature is used by Skip.
def asserted_binary_s
value_to_binary_string(eval_parameter(:asserted_value))
end
def do_read(io) # :nodoc:
super(io)
assert!
end
def assert!
assert_value(snapshot)
end
def assert_value(current_value)
expected = eval_parameter(:asserted_value, value: current_value)
if current_value != expected
raise ValidityError,
"value is '#{current_value}' but " \
"expected '#{expected}' for #{debug_name}"
end
end
end
###########################################################################
# To be implemented by subclasses
# Return the string representation that +val+ will take when written.
def value_to_binary_string(val)
raise NotImplementedError
end
# Read a number of bytes from +io+ and return the value they represent.
def read_and_return_value(io)
raise NotImplementedError
end
# Return a sensible default for this data.
def sensible_default
raise NotImplementedError
end
# To be implemented by subclasses
###########################################################################
end
end
bindata-2.5.1/lib/bindata/lazy.rb 0000644 0000041 0000041 00000005643 15000064511 016625 0 ustar www-data www-data module BinData
# A LazyEvaluator is bound to a data object. The evaluator will evaluate
# lambdas in the context of this data object. These lambdas
# are those that are passed to data objects as parameters, e.g.:
#
# BinData::String.new(value: -> { %w(a test message).join(" ") })
#
# As a shortcut, :foo is the equivalent of lambda { foo }.
#
# When evaluating lambdas, unknown methods are resolved in the context of the
# parent of the bound data object. Resolution is attempted firstly as keys
# in #parameters, and secondly as methods in this parent. This
# resolution propagates up the chain of parent data objects.
#
# An evaluation will recurse until it returns a result that is not
# a lambda or a symbol.
#
# This resolution process makes the lambda easier to read as we just write
# field instead of obj.field.
class LazyEvaluator
# Creates a new evaluator. All lazy evaluation is performed in the
# context of +obj+.
def initialize(obj)
@obj = obj
end
def lazy_eval(val, overrides = nil)
@overrides = overrides if overrides
if val.is_a? Symbol
__send__(val)
elsif callable?(val)
instance_exec(&val)
else
val
end
end
# Returns a LazyEvaluator for the parent of this data object.
def parent
if @obj.parent
@obj.parent.lazy_evaluator
else
nil
end
end
# Returns the index of this data object inside it's nearest container
# array.
def index
return @overrides[:index] if defined?(@overrides) && @overrides.key?(:index)
child = @obj
parent = @obj.parent
while parent
if parent.respond_to?(:find_index_of)
return parent.find_index_of(child)
end
child = parent
parent = parent.parent
end
raise NoMethodError, "no index found"
end
def method_missing(symbol, *args)
return @overrides[symbol] if defined?(@overrides) && @overrides.key?(symbol)
if @obj.parent
eval_symbol_in_parent_context(symbol, args)
else
super
end
end
#---------------
private
def eval_symbol_in_parent_context(symbol, args)
result = resolve_symbol_in_parent_context(symbol, args)
recursively_eval(result, args)
end
def resolve_symbol_in_parent_context(symbol, args)
obj_parent = @obj.parent
if obj_parent.has_parameter?(symbol)
obj_parent.get_parameter(symbol)
elsif obj_parent.safe_respond_to?(symbol, true)
obj_parent.__send__(symbol, *args)
else
symbol
end
end
def recursively_eval(val, args)
if val.is_a?(Symbol)
parent.__send__(val, *args)
elsif callable?(val)
parent.instance_exec(&val)
else
val
end
end
def callable?(obj)
Proc === obj || Method === obj || UnboundMethod === obj
end
end
end
bindata-2.5.1/lib/bindata/struct.rb 0000644 0000041 0000041 00000030237 15000064511 017167 0 ustar www-data www-data require 'bindata/base'
require 'bindata/delayed_io'
module BinData
class Base
optional_parameter :onlyif, :byte_align # Used by Struct
end
# A Struct is an ordered collection of named data objects.
#
# require 'bindata'
#
# class Tuple < BinData::Record
# int8 :x
# int8 :y
# int8 :z
# end
#
# obj = BinData::Struct.new(hide: :a,
# fields: [ [:int32le, :a],
# [:int16le, :b],
# [:tuple, :s] ])
# obj.field_names =># [:b, :s]
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# :fields:: An array specifying the fields for this struct.
# Each element of the array is of the form [type, name,
# params]. Type is a symbol representing a registered
# type. Name is the name of this field. Params is an
# optional hash of parameters to pass to this field
# when instantiating it. If name is "" or nil, then
# that field is anonymous and behaves as a hidden field.
# :hide:: A list of the names of fields that are to be hidden
# from the outside world. Hidden fields don't appear
# in #snapshot or #field_names but are still accessible
# by name.
# :endian:: Either :little or :big. This specifies the default
# endian of any numerics in this struct, or in any
# nested data objects.
# :search_prefix:: Allows abbreviated type names. If a type is
# unrecognised, then each prefix is applied until
# a match is found.
#
# == Field Parameters
#
# Fields may have have extra parameters as listed below:
#
# [:onlyif] Used to indicate a data object is optional.
# if +false+, this object will not be included in any
# calls to #read, #write, #num_bytes or #snapshot.
# [:byte_align] This field's rel_offset must be a multiple of
# :byte_align.
class Struct < BinData::Base
arg_processor :struct
mandatory_parameter :fields
optional_parameters :endian, :search_prefix, :hide
# These reserved words may not be used as field names
RESERVED =
Hash[*
(Hash.instance_methods +
%w[alias and begin break case class def defined do else elsif
end ensure false for if in module next nil not or redo
rescue retry return self super then true undef unless until
when while yield] +
%w[array element index value] +
%w[type initial_length read_until] +
%w[fields endian search_prefix hide onlyif byte_align] +
%w[choices selection copy_on_change] +
%w[read_abs_offset struct_params])
.collect(&:to_sym)
.uniq.collect { |key| [key, true] }
.flatten
]
def initialize_shared_instance
fields = get_parameter(:fields)
@field_names = fields.field_names.freeze
extend ByteAlignPlugin if fields.any_field_has_parameter?(:byte_align)
define_field_accessors
super
end
def initialize_instance
@field_objs = []
end
def clear # :nodoc:
@field_objs.each { |f| f.nil? || f.clear }
end
def clear? # :nodoc:
@field_objs.all? { |f| f.nil? || f.clear? }
end
def assign(val)
clear
assign_fields(val)
end
def snapshot
snapshot = Snapshot.new
field_names.each do |name|
obj = find_obj_for_name(name)
snapshot[name] = obj.snapshot if include_obj?(obj)
end
snapshot
end
# Returns a list of the names of all fields accessible through this
# object. +include_hidden+ specifies whether to include hidden names
# in the listing.
def field_names(include_hidden = false)
if include_hidden
@field_names.compact
else
hidden = get_parameter(:hide) || []
@field_names.compact - hidden
end
end
def debug_name_of(child) # :nodoc:
field_name = @field_names[find_index_of(child)]
"#{debug_name}.#{field_name}"
end
def offset_of(child) # :nodoc:
instantiate_all_objs
sum = sum_num_bytes_below_index(find_index_of(child))
child.bit_aligned? ? sum.floor : sum.ceil
end
def do_read(io) # :nodoc:
instantiate_all_objs
@field_objs.each { |f| f.do_read(io) if include_obj_for_io?(f) }
end
def do_write(io) # :nodoc:
instantiate_all_objs
@field_objs.each { |f| f.do_write(io) if include_obj_for_io?(f) }
end
def do_num_bytes # :nodoc:
instantiate_all_objs
sum_num_bytes_for_all_fields
end
def [](key)
find_obj_for_name(key)
end
def []=(key, value)
find_obj_for_name(key)&.assign(value)
end
def key?(key)
@field_names.index(base_field_name(key))
end
# Calls the given block for each field_name-field_obj pair.
#
# Does not include anonymous or hidden fields unless
# +include_all+ is true.
def each_pair(include_all = false)
instantiate_all_objs
pairs = @field_names.zip(@field_objs).select do |name, _obj|
name || include_all
end
if block_given?
pairs.each { |el| yield(el) }
else
pairs.each
end
end
#---------------
private
def define_field_accessors
get_parameter(:fields).each_with_index do |field, i|
name = field.name_as_sym
define_field_accessors_for(name, i) if name
end
end
def define_field_accessors_for(name, index)
define_singleton_method(name) do
instantiate_obj_at(index) if @field_objs[index].nil?
@field_objs[index]
end
define_singleton_method("#{name}=") do |*vals|
instantiate_obj_at(index) if @field_objs[index].nil?
@field_objs[index].assign(*vals)
end
define_singleton_method("#{name}?") do
instantiate_obj_at(index) if @field_objs[index].nil?
include_obj?(@field_objs[index])
end
end
def find_index_of(obj)
@field_objs.index { |el| el.equal?(obj) }
end
def find_obj_for_name(name)
index = @field_names.index(base_field_name(name))
if index
instantiate_obj_at(index)
@field_objs[index]
end
end
def base_field_name(name)
name.to_s.sub(/(=|\?)\z/, "").to_sym
end
def instantiate_all_objs
@field_names.each_index { |i| instantiate_obj_at(i) }
end
def instantiate_obj_at(index)
if @field_objs[index].nil?
field = get_parameter(:fields)[index]
@field_objs[index] = field.instantiate(nil, self)
end
end
def assign_fields(val)
src = as_stringified_hash(val)
@field_names.compact.each do |name|
obj = find_obj_for_name(name)
if obj && src.key?(name)
obj.assign(src[name])
end
end
end
def as_stringified_hash(val)
if BinData::Struct === val
val
elsif val.nil?
{}
else
hash = Snapshot.new
val.each_pair { |k, v| hash[k] = v }
hash
end
end
def sum_num_bytes_for_all_fields
sum_num_bytes_below_index(@field_objs.length)
end
def sum_num_bytes_below_index(index)
(0...index).inject(0) do |sum, i|
obj = @field_objs[i]
if include_obj?(obj)
nbytes = obj.do_num_bytes
(nbytes.is_a?(Integer) ? sum.ceil : sum) + nbytes
else
sum
end
end
end
def include_obj_for_io?(obj)
# Used by #do_read and #do_write, to ensure the stream is passed to
# DelayedIO objects for delayed processing.
include_obj?(obj) || DelayedIO === obj
end
def include_obj?(obj)
!obj.has_parameter?(:onlyif) || obj.eval_parameter(:onlyif)
end
# A hash that can be accessed via attributes.
class Snapshot < ::Hash # :nodoc:
def []=(key, value)
super unless value.nil?
end
def respond_to_missing?(symbol, include_all = false)
key?(symbol) || super
end
def method_missing(symbol, *args)
key?(symbol) ? self[symbol] : super
end
end
# Align fields to a multiple of :byte_align
module ByteAlignPlugin
def do_read(io)
offset = 0
instantiate_all_objs
@field_objs.each do |f|
next unless include_obj?(f)
if align_obj?(f)
nbytes = bytes_to_align(f, offset.ceil)
offset = offset.ceil + nbytes
io.readbytes(nbytes)
end
f.do_read(io)
nbytes = f.do_num_bytes
offset = (nbytes.is_a?(Integer) ? offset.ceil : offset) + nbytes
end
end
def do_write(io)
offset = 0
instantiate_all_objs
@field_objs.each do |f|
next unless include_obj?(f)
if align_obj?(f)
nbytes = bytes_to_align(f, offset.ceil)
offset = offset.ceil + nbytes
io.writebytes("\x00" * nbytes)
end
f.do_write(io)
nbytes = f.do_num_bytes
offset = (nbytes.is_a?(Integer) ? offset.ceil : offset) + nbytes
end
end
def sum_num_bytes_below_index(index)
sum = 0
@field_objs.each_with_index do |obj, i|
next unless include_obj?(obj)
if align_obj?(obj)
sum = sum.ceil + bytes_to_align(obj, sum.ceil)
end
break if i >= index
nbytes = obj.do_num_bytes
sum = (nbytes.is_a?(Integer) ? sum.ceil : sum) + nbytes
end
sum
end
def bytes_to_align(obj, rel_offset)
align = obj.eval_parameter(:byte_align)
(align - (rel_offset % align)) % align
end
def align_obj?(obj)
obj.has_parameter?(:byte_align)
end
end
end
class StructArgProcessor < BaseArgProcessor
def sanitize_parameters!(obj_class, params)
sanitize_endian(params)
sanitize_search_prefix(params)
sanitize_fields(obj_class, params)
sanitize_hide(params)
end
#-------------
private
def sanitize_endian(params)
params.sanitize_endian(:endian)
end
def sanitize_search_prefix(params)
params.sanitize(:search_prefix) do |sprefix|
search_prefix = Array(sprefix).collect do |prefix|
prefix.to_s.chomp("_")
end
search_prefix - [""]
end
end
def sanitize_fields(obj_class, params)
params.sanitize_fields(:fields) do |fields, sanitized_fields|
fields.each do |ftype, fname, fparams|
sanitized_fields.add_field(ftype, fname, fparams)
end
field_names = sanitized_field_names(sanitized_fields)
ensure_field_names_are_valid(obj_class, field_names)
end
end
def sanitize_hide(params)
params.sanitize(:hide) do |hidden|
field_names = sanitized_field_names(params[:fields])
hfield_names = hidden_field_names(hidden)
hfield_names & field_names
end
end
def sanitized_field_names(sanitized_fields)
sanitized_fields.field_names.compact
end
def hidden_field_names(hidden)
(hidden || []).collect(&:to_sym)
end
def ensure_field_names_are_valid(obj_class, field_names)
reserved_names = BinData::Struct::RESERVED
field_names.each do |name|
if obj_class.method_defined?(name)
raise NameError.new("Rename field '#{name}' in #{obj_class}, " \
"as it shadows an existing method.", name)
end
if reserved_names.include?(name)
raise NameError.new("Rename field '#{name}' in #{obj_class}, " \
"as it is a reserved name.", name)
end
if field_names.count(name) != 1
raise NameError.new("field '#{name}' in #{obj_class}, " \
"is defined multiple times.", name)
end
end
end
end
end
bindata-2.5.1/lib/bindata/primitive.rb 0000644 0000041 0000041 00000006571 15000064511 017657 0 ustar www-data www-data require 'bindata/base_primitive'
require 'bindata/dsl'
require 'bindata/struct'
module BinData
# A Primitive is a declarative way to define a new BinData data type.
# The data type must contain a primitive value only, i.e numbers or strings.
# For new data types that contain multiple values see BinData::Record.
#
# To define a new data type, set fields as if for Record and add a
# #get and #set method to extract / convert the data between the fields
# and the #value of the object.
#
# require 'bindata'
#
# class PascalString < BinData::Primitive
# uint8 :len, value: -> { data.length }
# string :data, read_length: :len
#
# def get
# self.data
# end
#
# def set(v)
# self.data = v
# end
# end
#
# ps = PascalString.new(initial_value: "hello")
# ps.to_binary_s #=> "\005hello"
# ps.read("\003abcde")
# ps #=> "abc"
#
# # Unsigned 24 bit big endian integer
# class Uint24be < BinData::Primitive
# uint8 :byte1
# uint8 :byte2
# uint8 :byte3
#
# def get
# (self.byte1 << 16) | (self.byte2 << 8) | self.byte3
# end
#
# def set(v)
# v = 0 if v < 0
# v = 0xffffff if v > 0xffffff
#
# self.byte1 = (v >> 16) & 0xff
# self.byte2 = (v >> 8) & 0xff
# self.byte3 = v & 0xff
# end
# end
#
# u24 = Uint24be.new
# u24.read("\x12\x34\x56")
# "0x%x" % u24 #=> 0x123456
#
# == Parameters
#
# Primitive objects accept all the parameters that BinData::BasePrimitive do.
#
class Primitive < BasePrimitive
extend DSLMixin
unregister_self
dsl_parser :primitive
arg_processor :primitive
mandatory_parameter :struct_params
def initialize_instance
super
@struct = BinData::Struct.new(get_parameter(:struct_params), self)
end
def respond_to?(symbol, include_private = false) # :nodoc:
@struct.respond_to?(symbol, include_private) || super
end
def method_missing(symbol, *args, &block) # :nodoc:
if @struct.respond_to?(symbol)
@struct.__send__(symbol, *args, &block)
else
super
end
end
def assign(val)
super(val)
set(_value)
@value = get
end
def debug_name_of(child) # :nodoc:
debug_name + "-internal-"
end
def do_write(io)
set(_value)
@struct.do_write(io)
end
def do_num_bytes
set(_value)
@struct.do_num_bytes
end
#---------------
private
def sensible_default
get
end
def read_and_return_value(io)
@struct.do_read(io)
get
end
###########################################################################
# To be implemented by subclasses
# Extracts the value for this data object from the fields of the
# internal struct.
def get
raise NotImplementedError
end
# Sets the fields of the internal struct to represent +v+.
def set(v)
raise NotImplementedError
end
# To be implemented by subclasses
###########################################################################
end
class PrimitiveArgProcessor < BaseArgProcessor
def sanitize_parameters!(obj_class, params)
params[:struct_params] = params.create_sanitized_params(obj_class.dsl_params, BinData::Struct)
end
end
end
bindata-2.5.1/lib/bindata/section.rb 0000644 0000041 0000041 00000004671 15000064511 017312 0 ustar www-data www-data require 'bindata/base'
require 'bindata/dsl'
module BinData
# A Section is a layer on top of a stream that transforms the underlying
# data. This allows BinData to process a stream that has multiple
# encodings. e.g. Some data data is compressed or encrypted.
#
# require 'bindata'
#
# class XorTransform < BinData::IO::Transform
# def initialize(xor)
# super()
# @xor = xor
# end
#
# def read(n)
# chain_read(n).bytes.map { |byte| (byte ^ @xor).chr }.join
# end
#
# def write(data)
# chain_write(data.bytes.map { |byte| (byte ^ @xor).chr }.join)
# end
# end
#
# obj = BinData::Section.new(transform: -> { XorTransform.new(0xff) },
# type: [:string, read_length: 5])
#
# obj.read("\x97\x9A\x93\x93\x90") #=> "hello"
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# :transform:: A callable that returns a new BinData::IO::Transform.
# :type:: The single type inside the buffer. Use a struct if
# multiple fields are required.
class Section < BinData::Base
extend DSLMixin
dsl_parser :section
arg_processor :section
mandatory_parameters :transform, :type
def initialize_instance
@type = get_parameter(:type).instantiate(nil, self)
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:
io.transform(eval_parameter(:transform)) do |transformed_io, _raw_io|
@type.do_read(transformed_io)
end
end
def do_write(io) # :nodoc:
io.transform(eval_parameter(:transform)) do |transformed_io, _raw_io|
@type.do_write(transformed_io)
end
end
def do_num_bytes # :nodoc:
to_binary_s.size
end
end
class SectionArgProcessor < BaseArgProcessor
include MultiFieldArgSeparator
def sanitize_parameters!(obj_class, params)
params.merge!(obj_class.dsl_params)
params.sanitize_object_prototype(:type)
end
end
end
bindata-2.5.1/lib/bindata/virtual.rb 0000644 0000041 0000041 00000002260 15000064511 017324 0 ustar www-data www-data require 'bindata/base'
module BinData
# A virtual field is one that is neither read, written nor occupies space in
# the data stream. It is used to make assertions or as a convenient label
# for determining offsets or storing values.
#
# require 'bindata'
#
# class A < BinData::Record
# string :a, read_length: 5
# string :b, read_length: 5
# virtual :c, assert: -> { a == b }
# end
#
# obj = A.read("abcdeabcde")
# obj.a #=> "abcde"
# obj.c.rel_offset #=> 10
#
# obj = A.read("abcdeABCDE") #=> BinData::ValidityError: assertion failed for obj.c
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params include those for BinData::Base as well as:
#
# [:assert] Raise an error when reading or assigning if the value
# of this evaluated parameter is false.
# [:value] The virtual object will always have this value.
#
class Virtual < BinData::BasePrimitive
def do_read(io); end
def do_write(io); end
def do_num_bytes
0.0
end
def sensible_default
nil
end
end
end
bindata-2.5.1/lib/bindata/record.rb 0000644 0000041 0000041 00000000757 15000064511 017125 0 ustar www-data www-data require 'bindata/dsl'
require 'bindata/struct'
module BinData
# A Record is a declarative wrapper around Struct.
#
# See +Struct+ for more info.
class Record < BinData::Struct
extend DSLMixin
unregister_self
dsl_parser :struct
arg_processor :record
end
class RecordArgProcessor < StructArgProcessor
include MultiFieldArgSeparator
def sanitize_parameters!(obj_class, params)
super(obj_class, params.merge!(obj_class.dsl_params))
end
end
end
bindata-2.5.1/lib/bindata/version.rb 0000644 0000041 0000041 00000000047 15000064511 017324 0 ustar www-data www-data module BinData
VERSION = '2.5.1'
end
bindata-2.5.1/lib/bindata/skip.rb 0000644 0000041 0000041 00000013354 15000064511 016612 0 ustar www-data www-data 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:
#
# :length:: The number of bytes to skip.
# :to_abs_offset:: Skips to the given absolute offset.
# :until_valid:: 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
# [type_symbol, hash_params].
#
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
bindata-2.5.1/lib/bindata/rest.rb 0000644 0000041 0000041 00000001235 15000064511 016614 0 ustar www-data www-data require 'bindata/base_primitive'
module BinData
# Rest will consume the input stream from the current position to the end of
# the stream. This will mainly be useful for debugging and developing.
#
# require 'bindata'
#
# class A < BinData::Record
# string :a, read_length: 5
# rest :rest
# end
#
# obj = A.read("abcdefghij")
# obj.a #=> "abcde"
# obj.rest #=" "fghij"
#
class Rest < BinData::BasePrimitive
#---------------
private
def value_to_binary_string(val)
val
end
def read_and_return_value(io)
io.read_all_bytes
end
def sensible_default
""
end
end
end
bindata-2.5.1/lib/bindata/stringz.rb 0000644 0000041 0000041 00000004504 15000064511 017341 0 ustar www-data www-data require 'bindata/base_primitive'
module BinData
# A BinData::Stringz object is a container for a zero ("\0") terminated
# string.
#
# For convenience, the zero terminator is not necessary when setting the
# value. Likewise, the returned value will not be zero terminated.
#
# require 'bindata'
#
# data = "abcd\x00efgh"
#
# obj = BinData::Stringz.new
# obj.read(data)
# obj.snapshot #=> "abcd"
# obj.num_bytes #=> 5
# obj.to_binary_s #=> "abcd\000"
#
# == Parameters
#
# Stringz objects accept all the params that BinData::BasePrimitive
# does, as well as the following:
#
# :max_length:: The maximum length of the string including the zero
# byte.
class Stringz < BinData::BasePrimitive
optional_parameters :max_length
def assign(val)
super(binary_string(val))
end
def snapshot
# override to always remove trailing zero bytes
result = super
trim_and_zero_terminate(result).chomp("\0")
end
#---------------
private
def value_to_binary_string(val)
trim_and_zero_terminate(val)
end
def read_and_return_value(io)
max_length = eval_parameter(:max_length)
str = binary_string("")
i = 0
ch = nil
# read until zero byte or we have read in the max number of bytes
while ch != "\0" && i != max_length
ch = io.readbytes(1)
str << ch
i += 1
end
trim_and_zero_terminate(str)
end
def sensible_default
""
end
def trim_and_zero_terminate(str)
max_length = eval_parameter(:max_length)
if max_length && max_length < 1
msg = "max_length must be >= 1 in #{debug_name} (got #{max_length})"
raise ArgumentError, msg
end
result = binary_string(str)
truncate_after_first_zero_byte!(result)
trim_to!(result, max_length)
append_zero_byte_if_needed!(result)
result
end
def truncate_after_first_zero_byte!(str)
str.sub!(/([^\0]*\0).*/, '\1')
end
def trim_to!(str, max_length = nil)
if max_length
str.slice!(max_length..-1)
str[-1, 1] = "\0" if str.length == max_length
end
end
def append_zero_byte_if_needed!(str)
if str.empty? || str[-1, 1] != "\0"
str << "\0"
end
end
end
end
bindata-2.5.1/lib/bindata/registry.rb 0000644 0000041 0000041 00000006645 15000064511 017521 0 ustar www-data www-data module BinData
# Raised when #lookup fails.
class UnRegisteredTypeError < StandardError; end
# This registry contains a register of name -> class mappings.
#
# Numerics (integers and floating point numbers) have an endian property as
# part of their name (e.g. int32be, float_le).
#
# Classes can be looked up based on their full name or an abbreviated +name+
# with +hints+.
#
# There are two hints supported, :endian and :search_prefix.
#
# #lookup("int32", { endian: :big }) will return Int32Be.
#
# #lookup("my_type", { search_prefix: :ns }) will return NsMyType.
#
# Names are stored in under_score_style, not camelCase.
class Registry
def initialize
@registry = {}
end
def register(name, class_to_register)
return if name.nil? || class_to_register.nil?
formatted_name = underscore_name(name)
warn_if_name_is_already_registered(formatted_name, class_to_register)
@registry[formatted_name] = class_to_register
end
def unregister(name)
@registry.delete(underscore_name(name))
end
def lookup(name, hints = {})
search_names(name, hints).each do |search|
register_dynamic_class(search)
if @registry.has_key?(search)
return @registry[search]
end
end
# give the user a hint if the endian keyword is missing
search_names(name, hints.merge(endian: :big)).each do |search|
register_dynamic_class(search)
if @registry.has_key?(search)
raise(UnRegisteredTypeError, "#{name}, do you need to specify endian?")
end
end
raise(UnRegisteredTypeError, name)
end
# Convert CamelCase +name+ to underscore style.
def underscore_name(name)
name
.to_s
.sub(/.*::/, "")
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.tr('-', '_')
.downcase
end
#---------------
private
def search_names(name, hints)
base = underscore_name(name)
searches = []
search_prefix = [""] + Array(hints[:search_prefix])
search_prefix.each do |prefix|
nwp = name_with_prefix(base, prefix)
nwe = name_with_endian(nwp, hints[:endian])
searches << nwp
searches << nwe if nwe
end
searches
end
def name_with_prefix(name, prefix)
prefix = prefix.to_s.chomp('_')
if prefix == ""
name
else
"#{prefix}_#{name}"
end
end
def name_with_endian(name, endian)
return nil if endian.nil?
suffix = (endian == :little) ? 'le' : 'be'
if /^u?int\d+$/.match?(name)
name + suffix
else
name + '_' + suffix
end
end
def register_dynamic_class(name)
if /^u?int\d+(le|be)$/.match?(name) || /^s?bit\d+(le)?$/.match?(name)
class_name = name.gsub(/(?:^|_)(.)/) { $1.upcase }
begin
# call const_get for side effect of creating class
BinData.const_get(class_name)
rescue NameError
end
end
end
def warn_if_name_is_already_registered(name, class_to_register)
prev_class = @registry[name]
if prev_class && prev_class != class_to_register
Kernel.warn "warning: replacing registered class #{prev_class} " \
"with #{class_to_register}"
end
end
end
# A singleton registry of all registered classes.
RegisteredClasses = Registry.new
end
bindata-2.5.1/lib/bindata/trace.rb 0000644 0000041 0000041 00000003612 15000064511 016736 0 ustar www-data www-data module BinData
# Turn on trace information when reading a BinData object.
# If +block+ is given then the tracing only occurs for that block.
# This is useful for debugging a BinData declaration.
def trace_reading(io = STDERR)
@tracer = Tracer.new(io)
[BasePrimitive, Choice].each(&:turn_on_tracing)
if block_given?
begin
yield
ensure
[BasePrimitive, Choice].each(&:turn_off_tracing)
@tracer = nil
end
end
end
# reference to the current tracer
@tracer ||= nil
class Tracer # :nodoc:
def initialize(io)
@trace_io = io
end
def trace(msg)
@trace_io.puts(msg)
end
def trace_obj(obj_name, val)
if val.length > 30
val = val.slice(0..30) + "..."
end
trace "#{obj_name} => #{val}"
end
end
def trace_message # :nodoc:
yield @tracer
end
module_function :trace_reading, :trace_message
module TraceHook
def turn_on_tracing
if !method_defined? :do_read_without_hook
alias_method :do_read_without_hook, :do_read
alias_method :do_read, :do_read_with_hook
end
end
def turn_off_tracing
if method_defined? :do_read_without_hook
alias_method :do_read, :do_read_without_hook
remove_method :do_read_without_hook
end
end
end
class BasePrimitive < BinData::Base
extend TraceHook
def do_read_with_hook(io)
do_read_without_hook(io)
BinData.trace_message do |tracer|
value_string = _value.inspect
tracer.trace_obj(debug_name, value_string)
end
end
end
class Choice < BinData::Base
extend TraceHook
def do_read_with_hook(io)
BinData.trace_message do |tracer|
selection_string = eval_parameter(:selection).inspect
tracer.trace_obj("#{debug_name}-selection-", selection_string)
end
do_read_without_hook(io)
end
end
end
bindata-2.5.1/lib/bindata/array.rb 0000644 0000041 0000041 00000021433 15000064511 016757 0 ustar www-data www-data 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:
#
# :type:: 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
# [type_symbol, hash_params].
# :initial_length:: The initial length of the array.
# :read_until:: 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
@elements = nil
end
def clear?
@elements.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?
@elements = []
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
@elements ||= []
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
# 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 @elements.nil?
@elements = []
eval_parameter(:initial_length).times do
@elements << new_element
end
end
@elements
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
end
bindata-2.5.1/lib/bindata/delayed_io.rb 0000644 0000041 0000041 00000012162 15000064511 017736 0 ustar www-data www-data 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_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 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 include_obj?
!has_parameter?(:onlyif) || eval_parameter(: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.seek_to_abs_offset(abs_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.seek_to_abs_offset(abs_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
class Base
# Add +auto_call_delayed_io+ keyword to BinData::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
include AutoCallDelayedIO
return if DelayedIO.method_defined? :initialize_instance_without_record_io
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
bindata-2.5.1/lib/bindata/framework.rb 0000644 0000041 0000041 00000004474 15000064511 017644 0 ustar www-data www-data module BinData
# Error raised when unexpected results occur when reading data from IO.
class ValidityError < StandardError; end
# All methods provided by the framework are to be implemented or overridden
# by subclasses of BinData::Base.
module Framework
# Initializes the state of the object. All instance variables that
# are used by the object must be initialized here.
def initialize_instance; end
# Initialises state that is shared by objects with the same parameters.
#
# This should only be used when optimising for performance. Instance
# variables set here, and changes to the singleton class will be shared
# between all objects that are initialized with the same parameters.
# This method is called only once for a particular set of parameters.
def initialize_shared_instance; end
# Returns true if the object has not been changed since creation.
def clear?
raise NotImplementedError
end
# Assigns the value of +val+ to this data object. Note that +val+ must
# always be deep copied to ensure no aliasing problems can occur.
def assign(val)
raise NotImplementedError
end
# Returns a snapshot of this data object.
def snapshot
raise NotImplementedError
end
# Returns the debug name of +child+. This only needs to be implemented
# by objects that contain child objects.
def debug_name_of(child) # :nodoc:
debug_name
end
# Returns the offset of +child+. This only needs to be implemented
# by objects that contain child objects.
def offset_of(child) # :nodoc:
0
end
# Is this object aligned on non-byte boundaries?
def bit_aligned?
false
end
# Reads the data for this data object from +io+.
def do_read(io) # :nodoc:
raise NotImplementedError
end
# Writes the value for this data to +io+.
def do_write(io) # :nodoc:
raise NotImplementedError
end
# Returns the number of bytes it will take to write this data.
def do_num_bytes # :nodoc:
raise NotImplementedError
end
# Set visibility requirements of methods to implement
public :clear?, :assign, :snapshot, :debug_name_of, :offset_of
protected :initialize_instance, :initialize_shared_instance
protected :do_read, :do_write, :do_num_bytes
end
end
bindata-2.5.1/lib/bindata/transform/ 0000755 0000041 0000041 00000000000 15000064511 017324 5 ustar www-data www-data bindata-2.5.1/lib/bindata/transform/xor.rb 0000755 0000041 0000041 00000000636 15000064511 020471 0 ustar www-data www-data module BinData
module Transform
# Transforms the data stream by xoring each byte.
class Xor < BinData::IO::Transform
def initialize(xor)
super()
@xor = xor
end
def read(n)
chain_read(n).bytes.map { |byte| (byte ^ @xor).chr }.join
end
def write(data)
chain_write(data.bytes.map { |byte| (byte ^ @xor).chr }.join)
end
end
end
end
bindata-2.5.1/lib/bindata/transform/lzma.rb 0000755 0000041 0000041 00000001332 15000064511 020616 0 ustar www-data www-data require 'xz'
module BinData
module Transform
# Transforms a lzma compressed data stream.
#
# gem install ruby-xz
class Lzma < BinData::IO::Transform
transform_changes_stream_length!
def initialize(read_length)
super()
@length = read_length
end
def read(n)
@read ||= ::XZ::decompress(chain_read(@length))
@read.slice!(0...n)
end
def write(data)
@write ||= create_empty_binary_string
@write << data
end
def after_read_transform
raise IOError, "didn't read all data" unless @read.empty?
end
def after_write_transform
chain_write(::XZ::compress(@write))
end
end
end
end
bindata-2.5.1/lib/bindata/transform/zlib.rb 0000755 0000041 0000041 00000001310 15000064511 020607 0 ustar www-data www-data require 'zlib'
module BinData
module Transform
# Transforms a zlib compressed data stream.
class Zlib < BinData::IO::Transform
transform_changes_stream_length!
def initialize(read_length)
super()
@length = read_length
end
def read(n)
@read ||= ::Zlib::Inflate.inflate(chain_read(@length))
@read.slice!(0...n)
end
def write(data)
@write ||= create_empty_binary_string
@write << data
end
def after_read_transform
raise IOError, "didn't read all data" unless @read.empty?
end
def after_write_transform
chain_write(::Zlib::Deflate.deflate(@write))
end
end
end
end
bindata-2.5.1/lib/bindata/transform/lz4.rb 0000755 0000041 0000041 00000001327 15000064511 020370 0 ustar www-data www-data require 'extlz4'
module BinData
module Transform
# Transforms a LZ4 compressed data stream.
#
# gem install extlz4
class LZ4 < BinData::IO::Transform
transform_changes_stream_length!
def initialize(read_length)
super()
@length = read_length
end
def read(n)
@read ||= ::LZ4::decode(chain_read(@length))
@read.slice!(0...n)
end
def write(data)
@write ||= create_empty_binary_string
@write << data
end
def after_read_transform
raise IOError, "didn't read all data" unless @read.empty?
end
def after_write_transform
chain_write(::LZ4::encode(@write))
end
end
end
end
bindata-2.5.1/lib/bindata/transform/zstd.rb 0000755 0000041 0000041 00000001347 15000064511 020645 0 ustar www-data www-data require 'zstd-ruby'
module BinData
module Transform
# Transforms a zstd compressed data stream.
#
# gem install zstd-ruby
class Zstd < BinData::IO::Transform
transform_changes_stream_length!
def initialize(read_length)
super()
@length = read_length
end
def read(n)
@read ||= ::Zstd::decompress(chain_read(@length))
@read.slice!(0...n)
end
def write(data)
@write ||= create_empty_binary_string
@write << data
end
def after_read_transform
raise IOError, "didn't read all data" unless @read.empty?
end
def after_write_transform
chain_write(::Zstd::compress(@write))
end
end
end
end
bindata-2.5.1/lib/bindata/transform/brotli.rb 0000755 0000041 0000041 00000001345 15000064511 021152 0 ustar www-data www-data require 'brotli'
module BinData
module Transform
# Transforms a brotli compressed data stream.
#
# gem install brotli
class Brotli < BinData::IO::Transform
transform_changes_stream_length!
def initialize(read_length)
super()
@length = read_length
end
def read(n)
@read ||= ::Brotli::inflate(chain_read(@length))
@read.slice!(0...n)
end
def write(data)
@write ||= create_empty_binary_string
@write << data
end
def after_read_transform
raise IOError, "didn't read all data" unless @read.empty?
end
def after_write_transform
chain_write(::Brotli::deflate(@write))
end
end
end
end
bindata-2.5.1/lib/bindata/transform/xz.rb 0000755 0000041 0000041 00000001326 15000064511 020317 0 ustar www-data www-data require 'xz'
module BinData
module Transform
# Transforms a xz compressed data stream.
#
# gem install ruby-xz
class XZ < BinData::IO::Transform
transform_changes_stream_length!
def initialize(read_length)
super()
@length = read_length
end
def read(n)
@read ||= ::XZ::decompress(chain_read(@length))
@read.slice!(0...n)
end
def write(data)
@write ||= create_empty_binary_string
@write << data
end
def after_read_transform
raise IOError, "didn't read all data" unless @read.empty?
end
def after_write_transform
chain_write(::XZ::compress(@write))
end
end
end
end
bindata-2.5.1/lib/bindata.rb 0000644 0000041 0000041 00000001654 15000064511 015644 0 ustar www-data www-data # BinData -- Binary data manipulator.
# Copyright (c) 2007 - 2025 Dion Mendel.
require 'bindata/version'
require 'bindata/array'
require 'bindata/bits'
require 'bindata/buffer'
require 'bindata/choice'
require 'bindata/count_bytes_remaining'
require 'bindata/delayed_io'
require 'bindata/float'
require 'bindata/int'
require 'bindata/primitive'
require 'bindata/record'
require 'bindata/rest'
require 'bindata/section'
require 'bindata/skip'
require 'bindata/string'
require 'bindata/stringz'
require 'bindata/struct'
require 'bindata/trace'
require 'bindata/uint8_array'
require 'bindata/virtual'
require 'bindata/alignment'
require 'bindata/warnings'
# = BinData
#
# A declarative way to read and write structured binary data.
#
# A full reference manual is available online at
# https://github.com/dmendel/bindata/wiki
#
# == License
#
# BinData is released under the same license as Ruby.
#
# Copyright (c) 2007 - 2025 Dion Mendel.
bindata-2.5.1/NEWS.rdoc 0000644 0000041 0000041 00000015673 15000064511 014602 0 ustar www-data www-data = 2.4.15
This software was originally dual licensed under the Ruby license and the
BSD-2-Clause license. It is now licensed solely as BSD-2-Clause.
= 2.4.0
This release changes the internal API for sanitizing parameters. This only
affects those that implement the `#sanitize_parameters` method.
= 2.3.x
if params.needs_sanitizing?(:type)
el_type, el_params = params[:type]
params[:type] = params.create_sanitized_object_prototype(el_type, el_params)
end
= 2.4.0
params.sanitize_object_prototype(:type)
= 2.3.0
This release adds I/O features.
Seeking forward to an arbitrary offset is achieved with Skip's :to_abs_offset
parameter.
Multi pass I/O has been added. BinData performs I/O in a single pass. See
DelayedIO to perform multi pass (backwards seeking) I/O.
The experimental :adjust_offset feature has been removed. If you have existing
code that uses this feature, you need to explicitly require 'bindata/offset' to
retain this functionality.
= 2.1.0
Several new features in this release.
* Fields can be aligned on n-bytes boundaries. This make it easier to write
parsers for aligned structures.
* The endian value :big_and_little can be used to automatically create :big and
:little endian versions of the class.
* Bit fields can be defined to have their length determined at runtime.
* The state of :onlyif fields can be determined by #field?
== Deprecations
The #offset method has been renamed to #abs_offset. This make an obvious
distinction to #rel_offset.
= 2.0.0
BinData 2.0.0 requires ruby 1.9 or greater. Support for ruby 1.8 is found in
BinData versions 1.8.x.
Prior to version 2.0.0, BinData used the Ruby 1.8 conversion of strings for
method names. As of 2.0.0, BinData uses symbols.
class A < BinData::Record
uint8 :a
uint8 :b
end
# BinData 1.8.x
A.read "\001\002" #=> {"a"=>1, "b"=>2}
# BinData >= 2.0.0
A.read "\001\002" #=> {:a=>1, :b=>2}
= 1.6.0
Added :assert as a replacement for :check_value. Note that :assert performs
the checking on assignment as well as reading.
The parameter :asserted_value is a new shortcut for combining :assert and :value
int8 :magic, :assert => 42, :value => 42
Can be written more concisely as
int8 :magic, :asserted_value => 42
= 1.5.0
Finally moved the source code to github.
Deprecated functionality has been removed to cleanup the codebase.
= 1.4.4
== Deprecations
The BinData::String parameter :pad_char has been renamed to :pad_byte. This
aids readibilty as Ruby 1.9 characters are not necessarily one byte.
= 1.4.0
== Deprecations
There is no longer any need to call #register_self when inheriting from
BinData::Base.
BinData::Wrapper is deprecated. You should use direct inheritance instead.
If your code looks like:
class Uint8Array < BinData::Wrapper
default_parameter :initial_element_value => 0
array :initial_length => 2 do
uint8 :initial_value => :initial_element_value
end
end
It should be changed to:
class Uint8Array < BinData::Array
default_parameter :initial_element_value => 0
default_parameter :initial_length => 2
uint8 :initial_value => :initial_element_value
end
See http://bindata.rubyforge.org/manual.html#extending_existing_types for details.
= 1.3.0
== New features
You can now assign values to BinData objects when instantiating them.
Previously:
obj = BinData::Stringz.new(:max_length => 10)
obj.assign("abc")
Now:
obj = BinData::Stringz.new("abc", :max_length => 10)
== Backwards incompatible changes
This version makes some backwards incompatible changes for more advanced
users of BinData.
This only affects you if you have created your own custom types by
subclassing BinData::Base or BinData::BasePrimitive.
All instance variables must now be initialized in #initialize_instance.
Implementing #initialize is no longer allowed.
Run your existing code in $VERBOSE mode ("ruby -w"), and BinData will
inform you of any changes you need to make to your code.
If your code previously looked like:
class MyType < BinData::Base
register_self
def initialize(parameters = {}, parent = nil)
super
@var1 = ...
@var2 = ...
end
...
end
You must change it to look like:
class MyType < BinData::Base
register_self
def initialize_instance
@var1 = ...
@var2 = ...
end
...
end
= 1.0.0
Version 1.0.0 removes all deprecated features. If you are upgrading from a
version prior to 0.10.0 you will need to make changes to your code before it
will work with BinData 1.0.0 or later.
It is recommended to first upgrade to 0.11.1, and run your code with the -w
command line switch. This will turn on warning messages that should give you
hints about which parts of your code need changing. See the NEWS for 0.10.0
for details of the deprecated features.
= 0.10.0
There are several new features in this release. The major ones are:
* Debugging declarations is now easier with the ability to trace values when
reading.
BinData::trace_reading(STDERR) do
obj_not_quite_working_correctly.read(io)
end
Will result in a debugging trace written to STDERR.
* Support for arbitrary sized integers and bit fields.
* Struct/Array field/element access now returns a BinData object, rather than
the underlying Fixnum or String. The BinData object will behave like it's
underlying primitive so existing code should continue to work. This allows
for an improved syntax in your declarations.
If your code requires access to the underlying primitive, you can access it
with the #value method.
There are several deprecations and one backwards incompatible change in this
release. Turn on $VERBOSE mode (-w command line switch) to see warnings
regarding these deprecations.
== IMPORTANT - Ruby does not warn you about this change!
* The #to_s method for getting the binary string representation of a data
object has been renamed to #to_binary_s. If you use this method you must
manually change your code or you will get strange results.
== Deprecations. Ruby will warn you of these when in $VERBOSE mode.
Code using these deprecated features will still work in this release but will
fail to work in some future release. Change your code now.
* BinData::SingleValue has been renamed to BinData::Primitive.
* BinData::MultiValue has been renamed to BinData::Record.
* String :trim_value parameter has been renamed to :trim_padding.
* Registry.instance should be replaced with RegisteredClasses.
* struct.offset_of("field") should be replaced with struct.field.offset.
* struct.clear("field") should be replaced with struct.field.clear.
* struct.clear?("field") should be replaced with struct.field.clear?.
* struct.num_bytes("field") should be replaced with struct.field.num_bytes.
* array.clear(n) should be replaced with array[n].clear.
* array.clear?(n) should be replaced with array[n].clear?.
* array.num_bytes(n) should be replaced with array[n].num_bytes.
bindata-2.5.1/test/ 0000755 0000041 0000041 00000000000 15000064511 014120 5 ustar www-data www-data bindata-2.5.1/test/io_test.rb 0000755 0000041 0000041 00000023037 15000064511 016123 0 ustar www-data www-data #!/usr/bin/env ruby
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
describe BinData::IO::Read, "reading from non seekable stream" do
before do
@rd, @wr = IO::pipe
@io = BinData::IO::Read.new(@rd)
@wr.write "a" * 2000
@wr.write "b" * 2000
@wr.close
end
after do
@rd.close
end
it "seeks correctly" do
@io.skipbytes(1999)
_(@io.readbytes(5)).must_equal "abbbb"
end
it "seeks to abs_offset" do
@io.skipbytes(1000)
@io.seek_to_abs_offset(1999)
_(@io.readbytes(5)).must_equal "abbbb"
end
it "wont seek backwards" do
@io.skipbytes(5)
_ {
@io.skipbytes(-1)
}.must_raise IOError
end
it "#num_bytes_remaining raises IOError" do
_ {
@io.num_bytes_remaining
}.must_raise IOError
end
end
describe BinData::IO::Write, "writing to non seekable stream" do
before do
@rd, @wr = IO::pipe
@io = BinData::IO::Write.new(@wr)
end
after do
@rd.close
@wr.close
end
def written_data
@io.flush
@wr.close
@rd.read
end
it "writes correctly" do
@io.writebytes("hello")
_(written_data).must_equal "hello"
end
it "must not attempt to seek" do
_ {
@io.seek_to_abs_offset(5)
}.must_raise IOError
end
end
describe BinData::IO::Read, "when reading" do
let(:stream) { StringIO.new "abcdefghij" }
let(:io) { BinData::IO::Read.new(stream) }
it "raises error when io is BinData::IO::Read" do
_ {
BinData::IO::Read.new(BinData::IO::Read.new(""))
}.must_raise ArgumentError
end
it "seeks correctly" do
io.skipbytes(2)
_(io.readbytes(4)).must_equal "cdef"
end
it "wont seek backwards" do
io.skipbytes(5)
_ {
io.skipbytes(-1)
}.must_raise IOError
end
it "reads all bytes" do
_(io.read_all_bytes).must_equal "abcdefghij"
end
it "returns number of bytes remaining" do
stream_length = io.num_bytes_remaining
io.readbytes(4)
_(io.num_bytes_remaining).must_equal stream_length - 4
end
it "raises error when reading at eof" do
io.skipbytes(10)
_ {
io.readbytes(3)
}.must_raise EOFError
end
it "raises error on short reads" do
_ {
io.readbytes(20)
}.must_raise IOError
end
end
describe BinData::IO::Write, "writing to non seekable stream" do
before do
@rd, @wr = IO::pipe
@io = BinData::IO::Write.new(@wr)
end
after do
@rd.close
@wr.close
end
it "writes data" do
@io.writebytes("1234567890")
_(@rd.read(10)).must_equal "1234567890"
end
end
describe BinData::IO::Write, "when writing" do
let(:stream) { StringIO.new }
let(:io) { BinData::IO::Write.new(stream) }
it "raises error when io is BinData::IO" do
_ {
BinData::IO::Write.new(BinData::IO::Write.new(""))
}.must_raise ArgumentError
end
it "writes correctly" do
io.writebytes("abcd")
_(stream.value).must_equal "abcd"
end
it "flushes" do
io.writebytes("abcd")
io.flush
_(stream.value).must_equal "abcd"
end
end
describe BinData::IO::Read, "reading bits in big endian" do
let(:b1) { 0b1111_1010 }
let(:b2) { 0b1100_1110 }
let(:b3) { 0b0110_1010 }
let(:io) { BinData::IO::Read.new([b1, b2, b3].pack("CCC")) }
it "reads a bitfield less than 1 byte" do
_(io.readbits(3, :big)).must_equal 0b111
end
it "reads a bitfield more than 1 byte" do
_(io.readbits(10, :big)).must_equal 0b1111_1010_11
end
it "reads a bitfield more than 2 bytes" do
_(io.readbits(17, :big)).must_equal 0b1111_1010_1100_1110_0
end
it "reads two bitfields totalling less than 1 byte" do
_(io.readbits(5, :big)).must_equal 0b1111_1
_(io.readbits(2, :big)).must_equal 0b01
end
it "reads two bitfields totalling more than 1 byte" do
_(io.readbits(6, :big)).must_equal 0b1111_10
_(io.readbits(8, :big)).must_equal 0b10_1100_11
end
it "reads two bitfields totalling more than 2 bytes" do
_(io.readbits(7, :big)).must_equal 0b1111_101
_(io.readbits(12, :big)).must_equal 0b0_1100_1110_011
end
it "ignores unused bits when reading bytes" do
_(io.readbits(3, :big)).must_equal 0b111
_(io.readbytes(1)).must_equal [b2].pack("C")
_(io.readbits(2, :big)).must_equal 0b01
end
it "resets read bits to realign stream to next byte" do
_(io.readbits(3, :big)).must_equal 0b111
io.reset_read_bits
_(io.readbits(3, :big)).must_equal 0b110
end
end
describe BinData::IO::Read, "reading bits in little endian" do
let(:b1) { 0b1111_1010 }
let(:b2) { 0b1100_1110 }
let(:b3) { 0b0110_1010 }
let(:io) { BinData::IO::Read.new([b1, b2, b3].pack("CCC")) }
it "reads a bitfield less than 1 byte" do
_(io.readbits(3, :little)).must_equal 0b010
end
it "reads a bitfield more than 1 byte" do
_(io.readbits(10, :little)).must_equal 0b10_1111_1010
end
it "reads a bitfield more than 2 bytes" do
_(io.readbits(17, :little)).must_equal 0b0_1100_1110_1111_1010
end
it "reads two bitfields totalling less than 1 byte" do
_(io.readbits(5, :little)).must_equal 0b1_1010
_(io.readbits(2, :little)).must_equal 0b11
end
it "reads two bitfields totalling more than 1 byte" do
_(io.readbits(6, :little)).must_equal 0b11_1010
_(io.readbits(8, :little)).must_equal 0b00_1110_11
end
it "reads two bitfields totalling more than 2 bytes" do
_(io.readbits(7, :little)).must_equal 0b111_1010
_(io.readbits(12, :little)).must_equal 0b010_1100_1110_1
end
it "ignores unused bits when reading bytes" do
_(io.readbits(3, :little)).must_equal 0b010
_(io.readbytes(1)).must_equal [b2].pack("C")
_(io.readbits(2, :little)).must_equal 0b10
end
it "resets read bits to realign stream to next byte" do
_(io.readbits(3, :little)).must_equal 0b010
io.reset_read_bits
_(io.readbits(3, :little)).must_equal 0b110
end
end
class BitWriterHelper
def initialize
@stringio = BinData::IO.create_string_io
@io = BinData::IO::Write.new(@stringio)
end
def writebits(val, nbits, endian)
@io.writebits(val, nbits, endian)
end
def writebytes(val)
@io.writebytes(val)
end
def value
@io.flushbits
@stringio.rewind
@stringio.read
end
end
describe BinData::IO::Write, "writing bits in big endian" do
let(:io) { BitWriterHelper.new }
it "writes a bitfield less than 1 byte" do
io.writebits(0b010, 3, :big)
_(io.value).must_equal [0b0100_0000].pack("C")
end
it "writes a bitfield more than 1 byte" do
io.writebits(0b10_1001_1101, 10, :big)
_(io.value).must_equal [0b1010_0111, 0b0100_0000].pack("CC")
end
it "writes a bitfield more than 2 bytes" do
io.writebits(0b101_1000_0010_1001_1101, 19, :big)
_(io.value).must_equal [0b1011_0000, 0b0101_0011, 0b1010_0000].pack("CCC")
end
it "writes two bitfields totalling less than 1 byte" do
io.writebits(0b1_1001, 5, :big)
io.writebits(0b00, 2, :big)
_(io.value).must_equal [0b1100_1000].pack("C")
end
it "writes two bitfields totalling more than 1 byte" do
io.writebits(0b01_0101, 6, :big)
io.writebits(0b001_1001, 7, :big)
_(io.value).must_equal [0b0101_0100, 0b1100_1000].pack("CC")
end
it "writes two bitfields totalling more than 2 bytes" do
io.writebits(0b01_0111, 6, :big)
io.writebits(0b1_0010_1001_1001, 13, :big)
_(io.value).must_equal [0b0101_1110, 0b0101_0011, 0b0010_0000].pack("CCC")
end
it "pads unused bits when writing bytes" do
io.writebits(0b101, 3, :big)
io.writebytes([0b1011_1111].pack("C"))
io.writebits(0b01, 2, :big)
_(io.value).must_equal [0b1010_0000, 0b1011_1111, 0b0100_0000].pack("CCC")
end
end
describe BinData::IO::Write, "writing bits in little endian" do
let(:io) { BitWriterHelper.new }
it "writes a bitfield less than 1 byte" do
io.writebits(0b010, 3, :little)
_(io.value).must_equal [0b0000_0010].pack("C")
end
it "writes a bitfield more than 1 byte" do
io.writebits(0b10_1001_1101, 10, :little)
_(io.value).must_equal [0b1001_1101, 0b0000_0010].pack("CC")
end
it "writes a bitfield more than 2 bytes" do
io.writebits(0b101_1000_0010_1001_1101, 19, :little)
_(io.value).must_equal [0b1001_1101, 0b1000_0010, 0b0000_0101].pack("CCC")
end
it "writes two bitfields totalling less than 1 byte" do
io.writebits(0b1_1001, 5, :little)
io.writebits(0b00, 2, :little)
_(io.value).must_equal [0b0001_1001].pack("C")
end
it "writes two bitfields totalling more than 1 byte" do
io.writebits(0b01_0101, 6, :little)
io.writebits(0b001_1001, 7, :little)
_(io.value).must_equal [0b0101_0101, 0b0000_0110].pack("CC")
end
it "writes two bitfields totalling more than 2 bytes" do
io.writebits(0b01_0111, 6, :little)
io.writebits(0b1_0010_1001_1001, 13, :little)
_(io.value).must_equal [0b0101_0111, 0b1010_0110, 0b0000_0100].pack("CCC")
end
it "pads unused bits when writing bytes" do
io.writebits(0b101, 3, :little)
io.writebytes([0b1011_1111].pack("C"))
io.writebits(0b01, 2, :little)
_(io.value).must_equal [0b0000_0101, 0b1011_1111, 0b0000_0001].pack("CCC")
end
end
describe BinData::IO::Read, "with changing endian" do
it "does not mix different endianness when reading" do
b1 = 0b0110_1010
b2 = 0b1110_0010
str = [b1, b2].pack("CC")
io = BinData::IO::Read.new(str)
_(io.readbits(3, :big)).must_equal 0b011
_(io.readbits(4, :little)).must_equal 0b0010
end
end
describe BinData::IO::Write, "with changing endian" do
it "does not mix different endianness when writing" do
io = BitWriterHelper.new
io.writebits(0b110, 3, :big)
io.writebits(0b010, 3, :little)
_(io.value).must_equal [0b1100_0000, 0b0000_0010].pack("CC")
end
end
bindata-2.5.1/test/uint8_array_test.rb 0000755 0000041 0000041 00000002257 15000064511 017762 0 ustar www-data www-data #!/usr/bin/env ruby
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
describe BinData::Uint8Array, "when initialising" do
it "with mutually exclusive parameters :initial_length and :read_until" do
params = {initial_length: 5, read_until: :eof}
_ { BinData::Uint8Array.new(params) }.must_raise ArgumentError
end
it "with :read_until" do
params = {read_until: :not_eof}
_ { BinData::Uint8Array.new(params) }.must_raise ArgumentError
end
it "with no parameters" do
arr = BinData::Uint8Array.new
_(arr.num_bytes).must_equal 0
end
end
describe BinData::Uint8Array, "with :read_until" do
it "reads until :eof" do
arr = BinData::Uint8Array.new(read_until: :eof)
arr.read("\xff\xfe\xfd\xfc")
_(arr).must_equal([255, 254, 253, 252])
end
end
describe BinData::Uint8Array, "with :initial_length" do
it "reads" do
arr = BinData::Uint8Array.new(initial_length: 3)
arr.read("\xff\xfe\xfd\xfc")
_(arr).must_equal([255, 254, 253])
end
end
describe BinData::Uint8Array, "when assigning" do
it "copies data" do
arr = BinData::Uint8Array.new
arr.assign([1, 2, 3])
_(arr).must_equal([1, 2, 3])
end
end
bindata-2.5.1/test/skip_test.rb 0000755 0000041 0000041 00000011332 15000064511 016455 0 ustar www-data www-data #!/usr/bin/env ruby
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
describe BinData::Skip, "when instantiating" do
describe "with no mandatory parameters supplied" do
it "raises an error" do
args = {}
_ { BinData::Skip.new(args) }.must_raise ArgumentError
end
end
end
describe BinData::Skip, "with :length" do
let(:obj) { BinData::Skip.new(length: 5) }
let(:io) { StringIO.new("abcdefghij") }
it "initial state" do
_(obj).must_equal ""
_(obj.to_binary_s).must_equal_binary "\000" * 5
end
it "skips bytes" do
obj.read(io)
_(io.pos).must_equal 5
end
it "has expected binary representation after setting value" do
obj.assign("123")
_(obj.to_binary_s).must_equal_binary "\000" * 5
end
it "has expected binary representation after reading" do
obj.read(io)
_(obj.to_binary_s).must_equal_binary "\000" * 5
end
end
describe BinData::Skip, "with :to_abs_offset" do
BinData::Struct.new(fields: [ [:skip, :f, { to_abs_offset: 5 } ] ])
let(:skip_obj) { [:skip, :f, { to_abs_offset: 5 } ] }
let(:io) { StringIO.new("abcdefghij") }
it "reads skipping forward" do
fields = [ skip_obj ]
obj = BinData::Struct.new(fields: fields)
obj.read(io)
_(io.pos).must_equal 5
end
it "reads skipping in place" do
fields = [ [:string, :a, { read_length: 5 }], skip_obj ]
obj = BinData::Struct.new(fields: fields)
obj.read(io)
_(io.pos).must_equal 5
end
it "does not read skipping backwards" do
fields = [ [:string, :a, { read_length: 10 }], skip_obj ]
obj = BinData::Struct.new(fields: fields)
_ {
obj.read(io)
}.must_raise ArgumentError
end
it "writes skipping forward" do
fields = [ skip_obj ]
obj = BinData::Struct.new(fields: fields)
_(obj.to_binary_s).must_equal "\000\000\000\000\000"
end
it "reads skipping in place" do
fields = [ [:string, :a, { value: "abcde" }], skip_obj ]
obj = BinData::Struct.new(fields: fields)
_(obj.to_binary_s).must_equal "abcde"
end
it "does not write skipping backwards" do
fields = [ [:string, :a, { value: "abcdefghij" }], skip_obj ]
obj = BinData::Struct.new(fields: fields)
_ {
obj.to_binary_s
}.must_raise ArgumentError
end
end
describe BinData::Skip, "with :until_valid" do
let(:io) { StringIO.new("abcdefghij") }
it "doesn't skip when writing" do
skip_obj = [:string, { read_length: 1, assert: "f" }]
args = { until_valid: skip_obj }
obj = BinData::Skip.new(args)
_(obj.to_binary_s).must_equal ""
end
it "skips to valid match" do
skip_obj = [:string, { read_length: 1, assert: "f" }]
fields = [ [:skip, :s, { until_valid: skip_obj }] ]
obj = BinData::Struct.new(fields: fields)
obj.read(io)
_(io.pos).must_equal 5
end
it "won't skip on unseekable stream" do
rd, wr = IO::pipe
unseekable_io = BinData::IO::Read.new(rd)
wr.write io
wr.close
skip_obj = [:string, { read_length: 1, assert: "f" }]
fields = [ [:skip, :s, { until_valid: skip_obj }] ]
obj = BinData::Struct.new(fields: fields)
_ {obj.read(unseekable_io)}.must_raise IOError
rd.close
end
it "doesn't skip when validator doesn't assert" do
skip_obj = [:string, { read_length: 1 }]
fields = [ [:skip, :s, { until_valid: skip_obj }] ]
obj = BinData::Struct.new(fields: fields)
obj.read(io)
_(io.pos).must_equal 0
end
it "raises IOError when no match" do
skip_obj = [:string, { read_length: 1, assert: "X" }]
fields = [ [:skip, :s, { until_valid: skip_obj }] ]
obj = BinData::Struct.new(fields: fields)
_ {
obj.read(io)
}.must_raise IOError
end
it "raises IOError when validator reads beyond stream" do
skip_obj = [:string, { read_length: 30 }]
fields = [ [:skip, :s, { until_valid: skip_obj }] ]
obj = BinData::Struct.new(fields: fields)
_ {
obj.read(io)
}.must_raise IOError
end
it "uses block form" do
class DSLSkip < BinData::Record
skip :s do
string read_length: 1, assert: "f"
end
string :a, read_length: 1
end
obj = DSLSkip.read(io)
_(obj.a).must_equal "f"
end
end
describe BinData::Skip, "with :until_valid" do
class SkipSearch < BinData::Record
skip :s do
uint8
uint8 asserted_value: 1
uint8 :a
uint8 :b
virtual assert: -> { a == b }
end
array :data, type: :uint8, initial_length: 4
end
let(:io) { BinData::IO.create_string_io("\x0f" * 10 + "\x00\x01\x02\x03\x00" + "\x02\x01\x03\x03" + "\x06") }
it "finds valid match" do
obj = SkipSearch.read(io)
_(obj.data).must_equal [2, 1, 3, 3]
end
it "match is at expected offset" do
obj = SkipSearch.read(io)
_(obj.data.rel_offset).must_equal 15
end
end
bindata-2.5.1/test/base_primitive_test.rb 0000755 0000041 0000041 00000015464 15000064511 020523 0 ustar www-data www-data #!/usr/bin/env ruby
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
class ExampleSingle < BinData::BasePrimitive
def self.io_with_value(val)
StringIO.new([val].pack("V"))
end
private
def value_to_binary_string(val)
[val].pack("V")
end
def read_and_return_value(io)
io.readbytes(4).unpack("V").at(0)
end
def sensible_default
0
end
end
describe BinData::BasePrimitive do
it "is not registered" do
_ {
BinData::RegisteredClasses.lookup("BasePrimitive")
}.must_raise BinData::UnRegisteredTypeError
end
end
describe BinData::BasePrimitive, "all subclasses" do
class SubClassOfBasePrimitive < BinData::BasePrimitive
expose_methods_for_testing
end
let(:obj) { SubClassOfBasePrimitive.new }
it "raise errors on unimplemented methods" do
_ { obj.value_to_binary_string(nil) }.must_raise NotImplementedError
_ { obj.read_and_return_value(nil) }.must_raise NotImplementedError
_ { obj.sensible_default }.must_raise NotImplementedError
end
end
describe ExampleSingle do
let(:obj) { ExampleSingle.new(5) }
it "fails when assigning nil values" do
_ { obj.assign(nil) }.must_raise ArgumentError
end
it "sets and retrieves values" do
obj.assign(7)
_(obj).must_equal 7
end
it "sets and retrieves BinData::BasePrimitives" do
obj.assign(ExampleSingle.new(7))
_(obj).must_equal 7
end
it "responds to known methods" do
_(obj).must_respond_to :num_bytes
end
it "responds to known methods in #snapshot" do
_(obj).must_respond_to :div
end
it "does not respond to unknown methods in self or #snapshot" do
_(obj).wont_respond_to :does_not_exist
end
it "behaves as #snapshot" do
_((obj + 1)).must_equal 6
_((1 + obj)).must_equal 6
end
it "is equal to other ExampleSingle" do
_(obj).must_equal ExampleSingle.new(5)
end
it "is equal to raw values" do
_(obj).must_equal 5
_(5).must_equal obj
end
it "can be used as a hash key" do
hash = {5 => 17}
_(hash[obj]).must_equal 17
end
it "is sortable" do
_([ExampleSingle.new(5), ExampleSingle.new(3)].sort).must_equal [3, 5]
end
end
describe BinData::BasePrimitive, "after initialisation" do
let(:obj) { ExampleSingle.new }
it "does not allow both :initial_value and :value" do
params = {initial_value: 1, value: 2}
_ { ExampleSingle.new(params) }.must_raise ArgumentError
end
it "initial state" do
assert obj.clear?
_(obj.value).must_equal 0
_(obj.num_bytes).must_equal 4
end
it "has symmetric IO" do
obj.assign(42)
written = obj.to_binary_s
_(ExampleSingle.read(written)).must_equal 42
end
it "sets and retrieves values" do
obj.value = 5
_(obj.value).must_equal 5
end
it "is not clear after setting value" do
obj.assign(5)
refute obj.clear?
end
it "is not clear after reading" do
obj.read("\x11\x22\x33\x44")
refute obj.clear?
end
it "returns a snapshot" do
obj.assign(5)
_(obj.snapshot).must_equal 5
end
end
describe BinData::BasePrimitive, "with :initial_value" do
let(:obj) { ExampleSingle.new(initial_value: 5) }
it "initial state" do
_(obj.value).must_equal 5
end
it "forgets :initial_value after being set" do
obj.assign(17)
_(obj).wont_equal 5
end
it "forgets :initial_value after reading" do
obj.read("\x11\x22\x33\x44")
_(obj).wont_equal 5
end
it "remembers :initial_value after being cleared" do
obj.assign(17)
obj.clear
_(obj).must_equal 5
end
end
describe BinData::BasePrimitive, "with :value" do
let(:obj) { ExampleSingle.new(value: 5) }
let(:io) { ExampleSingle.io_with_value(56) }
it "initial state" do
_(obj.value).must_equal 5
end
it "changes during reading" do
obj.read(io)
obj.stub :reading?, true do
_(obj).must_equal 56
end
end
it "does not change after reading" do
obj.read(io)
_(obj).must_equal 5
end
it "is unaffected by assigning" do
obj.assign(17)
_(obj).must_equal 5
end
end
describe BinData::BasePrimitive, "asserting value" do
let(:io) { ExampleSingle.io_with_value(12) }
describe ":assert is non boolean" do
it "asserts sensible value" do
data = ExampleSingle.new(assert: 0)
data.assert!
_(data.value).must_equal 0
end
it "succeeds when assert is correct" do
data = ExampleSingle.new(assert: 12)
data.read(io)
_(data.value).must_equal 12
end
it "fails when assert is incorrect" do
data = ExampleSingle.new(assert: -> { 99 })
_ { data.read(io) }.must_raise BinData::ValidityError
end
end
describe ":assert is boolean" do
it "succeeds when assert is true" do
data = ExampleSingle.new(assert: -> { value < 20 })
data.read(io)
_(data.value).must_equal 12
end
it "fails when assert is false" do
data = ExampleSingle.new(assert: -> { value > 20 })
_ { data.read(io) }.must_raise BinData::ValidityError
end
end
describe "assigning with :assert" do
it "succeeds when assert is correct" do
data = ExampleSingle.new(assert: 12)
data.assign(12)
_(data.value).must_equal 12
end
it "fails when assert is incorrect" do
data = ExampleSingle.new(assert: 12)
_ { data.assign(99) }.must_raise BinData::ValidityError
end
end
end
describe BinData::BasePrimitive, ":asserted_value" do
it "has :value" do
data = ExampleSingle.new(asserted_value: -> { 12 })
_(data.value).must_equal 12
end
describe "assigning with :assert" do
it "succeeds when assert is correct" do
data = ExampleSingle.new(asserted_value: -> { 12 })
data.assign(12)
_(data.value).must_equal 12
end
it "fails when assert is incorrect" do
data = ExampleSingle.new(asserted_value: -> { 12 })
_ { data.assign(99) }.must_raise BinData::ValidityError
end
end
end
describe BinData::BasePrimitive do
it "conforms to rule 1 for returning a value" do
data = ExampleSingle.new(value: 5)
_(data).must_equal 5
end
it "conforms to rule 2 for returning a value" do
io = ExampleSingle.io_with_value(42)
data = ExampleSingle.new(value: 5)
data.read(io)
data.stub :reading?, true do
_(data).must_equal 42
end
end
it "conforms to rule 3 for returning a value" do
data = ExampleSingle.new(initial_value: 5)
assert data.clear?
_(data).must_equal 5
end
it "conforms to rule 4 for returning a value" do
data = ExampleSingle.new(initial_value: 5)
data.assign(17)
refute data.clear?
_(data).must_equal 17
end
it "conforms to rule 5 for returning a value" do
data = ExampleSingle.new
assert data.clear?
_(data).must_equal 0
end
it "conforms to rule 6 for returning a value" do
data = ExampleSingle.new
data.assign(8)
refute data.clear?
_(data).must_equal 8
end
end
bindata-2.5.1/test/test_helper.rb 0000644 0000041 0000041 00000003615 15000064511 016770 0 ustar www-data www-data require 'rubygems'
require 'simplecov'
SimpleCov.start do
enable_coverage :branch
end
require 'minitest/autorun'
require 'stringio'
$LOAD_PATH.unshift File.expand_path("../lib", File.dirname(__FILE__))
require 'bindata'
class StringIO
# Returns the value that was written to the io
def value
rewind
read
end
end
module Kernel
def expose_methods_for_testing
cls = (Class === self) ? self : (class << self ; self; end)
private_method_names = cls.private_instance_methods - Object.private_instance_methods
cls.send(:public, *private_method_names)
protected_method_names = cls.protected_instance_methods - Object.protected_instance_methods
cls.send(:public, *protected_method_names)
end
def value_read_from_written
self.class.read(self.to_binary_s)
end
end
module Minitest::Assertions
def assert_equals_binary(expected, actual)
assert_equal expected.dup.force_encoding(Encoding::BINARY), actual
end
def assert_raises_on_line(exp, line, msg = nil, &block)
ex = assert_raises(exp, &block)
assert_equal(msg, ex.message) if msg
line_num_regex = /(.*):(\d+)(:in.*)$/
filename = line_num_regex.match(ex.backtrace[0])[1]
filtered = ex.backtrace.grep(/^#{Regexp.escape(filename)}/)
top = filtered.grep(Regexp.new(Regexp.escape("in