ブログ・ア・ラ・クレーム

技術的なメモとかライフログとか。

protobuf のシリアライズ済みバイナリを無理やり読む

Protocol Buffer wire format について

Protocol Buffer でシリアライズされた後のバイナリのレイアウトの仕様は wire format の仕様という形で独立してドキュメントが用意されています。 この wire format の仕様は見ればわかる通りそれほど記述量が多くなく、それでいて互換性を気にしつつ、かつ拡張の邪魔にならないような配慮がされています。

developers.google.com

wire format として特に重要な点は以下の通りだと個人的には考えます。

  • 整数型に対するデータサイズがなるべく小さくなるような工夫がされている
  • 複雑な型はバイト配列に押し込める。(多くのシリアライゼーションフォーマットと同じように)長さが指定されたバイト配列として表現
  • フィールドの順序が任意にできる。のでスキーマ更新において順序を意識しなくてよくなる。

wire format では各フィールドはフィールド番号と wire format 上での型で識別されます。 これらを元に、 .proto ファイルでどう記述されていたのかに対応してより具体的な型として解釈したりフィールドの名前を取得したりできるわけです。 例えば field number が 10 、 wire type が 2(バイト配列) であった場合に、 .proto では string name = 10; という対応する field number があるならフィールド名は name でありバイト配列は文字列として解釈できそうだと判断できます。

とはいえシリアライズ済みバイナリを頻繁に扱ってくると、ましてや複数のシステムでデータ交換をし出すと時には .proto や descriptor 、ライブラリなしに内容を確認したい時があるかも知れません。 特にデータ交換を行う場合システム間で持つスキーマが異なる場合にはこの希望は大きくなるかもです。 またパースエラーが起こる箇所を絞り込みたいなどの要望から、スキーマを用いた具体的なデータの解釈をなるべく遅らせたい場合にこの戦略は有効かも知れません。 そんなことを考え、 Go で無理やりシリアライズ済みバイナリを、それ単体の持つ情報だけで解釈してみる試みをしました。

Protocol Buffer without schema

というわけで Go で schema less で protobuf のデータを無理やり解釈するコードを書いてみました。

github.com

wire format のフィールドの解釈は protowire という既存のモジュールがあり、詳細な部分は概ねこれで行えます。 wire format の解釈の上で面倒くさい点は可変長になりうる varint の値や ZigZag encoding されうる signed integer あたりですがこのあたりは実装されてくれています。

godoc.org

バイト列の詳細な解釈は descriptor の情報が無いと正確な値は取れません。 ここでは雑に、思いつく範囲の解釈を全部試して wire format としては valid な値をすべて返すようにしています。 具体的には文字列とバイト列、ネストされたメッセージ、整数型の repeated な値などです。 そのほかにも Protocol Buffer としてはバイト列で map 型が表現できるのですが、これについては wire format やその他のドキュメントに詳細な仕様がなく(うまく見つけてなくてどこかに存在するかも?)一旦諦めています。 Go の実装的にはバイト列の中に更にネストされたメッセージに似てフィールド番号や wire type が指定されたフィールド列が存在して、フィールド番号が 1 であれば map の key 、 2 であれば value となるようです。

github.com

これを用いて以下のような .proto のメッセージを

syntax = "proto3";

message Example {
    enum Num {
        ZERO = 0;
        ONE = 1;
    }

    uint64 uint64_val = 1;
    string string_val = 2;
    fixed64 fixed64_val = 3;
    fixed32 fixed32_val = 4;
    Num enum_val = 5;
    Child child_val = 6;

    repeated uint64 r_uint64_val = 101;
    repeated string r_string_val = 102;
    repeated fixed64 r_fixed64_val = 103;
    repeated fixed32 r_fixed32_val = 104;
    repeated Num r_enum_val = 105;
    repeated Child r_child_val = 106;
}

message Child {
    uint64 v = 1;
}

こんな様な値を設定してシリアライズした時の

               msg := &protosl.Example{
                    Uint64Val:  1,
                    StringVal:  "testing",
                    Fixed64Val: 11,
                    Fixed32Val: 111,
                    EnumVal:    protosl.Example_ONE,
                    ChildVal: &protosl.Child{
                        V: 1,
                    },
                    RUint64Val: []uint64{2, 3},
                    RStringVal: []string{"aaa", "bbb"},
                    // RFixed64Val: []uint64{22, 33}, TODO repeated fixed isn't supported
                    // RFixed32Val: []uint32{222, 333}, TODO repeated fixed isn't supported
                    REnumVal: []protosl.Example_Num{protosl.Example_ZERO, protosl.Example_ONE},
                    RChildVal: []*protosl.Child{
                        {
                            V: 2,
                        },
                        {
                            V: 3,
                        },
                    },
                }

シリアライズ後のバイナリを食わせると、かなり冗長にはなりますが以下のように解釈できます。

$ echo -n "\x08\x01\x12\x07\x74\x65\x73\x74\x69\x6e\x67\x19\x0b\x00\x00\x00\x00\x00\x00\x00\x25\x6f\x00\x00\x00\x28\x01\x32\x02\x08\x01\xaa\x06\x02\x02\x03\xb2\x06\x03\x61\x61\x61\xb2\x06\x03\x62\x62\x62\xca\x06\x02\x00\x01\xd2\x06\x02\x08\x02\xd2\x06\x02\x08\x03" | protosl
{"1":1,"101":{"__bytes":"AgM=","__packed":[2,3],"__string":"\u0002\u0003"},"102":[{"__bytes":"YWFh","__packed":[97,97,97],"__string":"aaa"},{"__bytes":"YmJi","__packed":[98,98,98],"__string":"bbb"}],"105":{"__bytes":"AAE=","__packed":[0,1],"__string":"\u0000\u0001"},"106":[{"__bytes":"CAI=","__message":{"1":2},"__packed":[8,2],"__string":"\u0008\u0002"},{"__bytes":"CAM=","__message":{"1":3},"__packed":[8,3],"__string":"\u0008\u0003"}],"2":{"__bytes":"dGVzdGluZw==","__packed":[116,101,115,116,105,110,103],"__string":"testing"},"3":11,"4":111,"5":1,"6":{"__bytes":"CAE=","__message":{"1":1},"__packed":[8,1],"__string":"\u0008\u0001"}}

バイナリを直接確認できると何かしら便利、フィールドとの対応付けは後で行うので・・・という時に役のに立つかも知れません。