hexstruct2/hexstruct.rb
#
# Copyright (C) 2005 Minero Aoki
#
# This program is free software.
# You can distribute/modify this program under the Ruby License.
#
require 'stringio'
require 'forwardable'
class HexStruct
class NoMemberError < StandardError; end
class SizeError < StandardError; end
class FormatError < StandardError; end
class << self
def field(name, byte_size)
define_field(name) {|f|
s = f.read(byte_size * 2).to_s
raise SizeError, 'field too short' unless s.length == byte_size * 2
FixedSizeField.new(byte_size, s.hex)
}
end
def variable_length_field(name)
define_field(name) {|f|
s = f.read
VariableSizeField.new(s.hex, s.length)
}
end
def struct(name, &block)
c = Class.new(HexStruct, &block)
define_field(name) {|f|
c.for_io(f)
}
end
def array(name, &block)
c = Class.new(HexStruct, &block)
define_field(name) {|f|
list = []
until f.eof?
list.push c.for_io(f)
end
ListField.new(list)
}
end
private
def define_field(name, &block)
(@field_specs ||= []).push [name, block]
define_field_accessor name
end
def define_field_accessor(name)
define_method(name) {
data_of(name)
}
define_method("#{name}=") {|obj|
write_to name, obj
}
end
end
class << self
def byte_size
new().byte_size
end
alias size byte_size
end
class << self
alias newobj new
def new(str = nil)
if str
parse(str)
else
for_io(DevZero.new)
end
end
class DevZero
def read(len = nil)
len ? ('0' * len) : ''
end
def eof?
true
end
end
def parse(hexstr)
for_io(StringIO.new(hexstr))
end
private
def for_io(f)
init_field_specs
alist = []
@field_specs.each do |name, constructor|
alist.push [name, constructor.call(f)]
end
raise SizeError, 'data too long' unless f.eof?
newobj(alist)
end
# For backward compatibility
def init_field_specs
if not defined?(@field_specs) and const_defined?(:STRUCT)
self::STRUCT.each do |name, byte_size|
raise FormatError, "missing field name" unless name
raise FormatError, "missing field size" unless byte_size
if byte_size > 0
field name, byte_size
else
variable_length_field name
end
end
end
end
end
def initialize(alist)
@fields = alist
end
def byte_size
fields().inject(0) {|sum, field| sum + field.byte_size }
end
alias size byte_size
def size_of(name)
fetch(name).byte_size
end
def data_of(name)
fetch(name).get
end
def write_to(name, val)
case
when val.kind_of?(Array)
store name, ListField.new(val)
when val.kind_of?(HexStruct)
store name, CascadingField.new(val)
else
fetch(name).set val
end
end
def ==(other)
self.to_s == other.to_s
end
def string
fields().map {|field| field.string }.join
end
alias to_s string
alias to_str string # should be obsolete
def to_a
fields().map {|field| field.aitem }
end
def inspect
"\#<#{self.class} #{
@fields.map {|name, i| "#{name}=#{i.get.inspect}" }.join(", ")
}>"
end
private
def fields
@fields.map {|name, field| field }
end
def fetch(name)
a = @fields.assoc(name.to_s.intern) or
raise NoMemberError, "wrong member name: #{name}"
a[1]
end
def store(name, val)
a = @fields.assoc(name.to_s.intern) or
raise NoMemberError, "wrong member name: #{name}"
a[1] = val
end
# For backward compatibility
def method_missing(mid, *args)
raise NoMemberError, "wrong member: #{mid}"
end
class FixedSizeField
def initialize(byte_size, int = 0)
@len = byte_size * 2
@integer = int
end
def byte_size
@len / 2
end
def string
s = sprintf("%0#{@len}X", @integer)
raise SizeError, 'number too big' unless s.length == @len
s
end
alias get string
def set(obj)
case
when obj.kind_of?(Integer)
@integer = obj
when obj.kind_of?(String)
unless obj.length == @len
raise SizeError, "expected #{@len} but was: #{obj.length}"
end
@integer = obj.hex
else
raise TypeError, "unexpected type: #{obj.class}"
end
end
alias aitem string # internal use only: DO NOT USE
end
class VariableSizeField
def initialize(int = 0, len = nil)
@integer = int
if len
@len = len
else
@len = 0
@len = string().length
end
end
def byte_size
@len / 2
end
def string
return '' if @integer == 0
s = sprintf("%0#{@len}X", @integer)
(s.size % 2 == 0) ? s : '0' + s
end
alias get string
def set(obj)
case
when obj.kind_of?(Integer)
@integer = obj
when obj.kind_of?(String)
@len = obj.size
@integer = obj.hex
else
raise TypeError, "unexpected type: #{obj.class}"
end
end
alias aitem string # internal use only: DO NOT USE
end
class ListField
def initialize(list)
@list = list
end
attr_accessor :list
alias get list
alias set list=
def byte_size
@list.map {|i| i.byte_size }.inject(0) {|sum, n| sum + n }
end
def string
# @list.map {|i| i.string }.join('') # FIXME
@list.join('')
end
alias aitem get # internal use only: DO NOT USE
end
class CascadingField
def initialize(struct)
@struct = struct
end
attr_accessor :struct
alias get struct
alias set struct=
extend Forwardable
def_delegator "@struct", :byte_size
def_delegator "@struct", :string
def_delegator "@struct", :to_a, :aitem # internal use only: DO NOT USE
end
end