Cの構造体とかだと、構造体の中に他の構造体ということは普通にあります。

こういった構造体の文字列を unpack すると、全部フラットな配列になってしまうので、Ruby レベルのオブジェクト構造として再構成しようと思うと面倒なことになります。

ということで、入れ子に対応した unpack というのを実装してみました。

概要

以下のような感じで {} (ブレース) で入れ子を表現するようにテンプレート文字列を拡張します。

struct_foo_t = %{
	I!
	I!
	Z16
}

struct_bar_t = %{
	I!
	I!
	{
		#{struct_foo_t}
	}
	I!
} #=> "I!I!{I!I!Z16}I!"

original = [
	0xffff,
	0xfeff,
	[
		0x11,
		0x22,
		"foobar"
	],
	0x10,
]

packed = original.pack_deeply(struct_bar_t)
p packed
#=> "\xFF\xFF\x00\x00\xFF\xFE\x00\x00\x11\x00\x00\x00\"\x00\x00\x00foobar\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00"

unpacked = packed.unpack_deeply(struct_bar_t)
p unpacked
#=> [65535, 65279, [17, 34, "foobar"], 16]

p original == unpacked
#=> true

実装

先頭から unpack して入れ子を処理していくというイメージです。処理済みのバイト長が欲しいので、クソみたいですが一旦 unpack したものをもう一度 pack しています。

pack_deeply は単にフラットにして pack するだけなので簡単です。

class String
	def unpack_deeply(template)
		ret = []
		tree = [ ret ]
		n = 0
		tmpl = ""

		# unpack partial and add to result
		unpack = lambda {
			unpacked = self.slice(n..-1).unpack(tmpl)
			# re-pack to get byte length
			size = unpacked.pack(tmpl).size
			n += size
			tmpl = ""
			tree.last.concat(unpacked)
		}

		template.each_char do |chr|
			case chr
			when "{"
				unpack.call
				ary = []
				tree.last << ary
				tree << ary
			when "}"
				unpack.call
				tree.pop
			else
				tmpl << chr
			end
		end
		unpack.call
		ret
	end
end

class Array
	def pack_deeply(template)
		flatten.pack(template.gsub(/[{}]/, ''))
	end
end
  1. トップ
  2. tech
  3. Ruby でネストした構造体文字列をネストした配列として unpack する
  1. トップ
  2. ruby
  3. Ruby でネストした構造体文字列をネストした配列として unpack する
▲ この日のエントリ