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