go で proto の Marshal/Unmarshal したときに、軽くハマっておや?となったので調べてみた
こんな感じの proto があるとする
syntax = "proto3"; package hoge; message A { B b = 1; } message B { C c1 = 1; C c2 = 2; } message C { D d = 1; } message D { int32 e = 1; }
A を Marshal して []byte にして、ゴニョゴニョして、再度 A に戻すコードを書くつもりが、 間違えて B を Marshal して、それを A に戻す実装になってしまっていた。
func main() { b := hoge.B{ C1: &hoge.C{ D: &hoge.D{ E: 1, }, }, C2: &hoge.C{ D: &hoge.D{ E: 1, }, }, } bytes, _ := proto.Marshal(&b) fmt.Printf("%s\n", hex.EncodeToString(bytes)) var a hoge.A if err := proto.Unmarshal(bytes, &a); err != nil { fmt.Printf("Error: %s", err) } }
Marshall と Unmarshal する proto が異なるので、エラーでわかるだろうと思っていたが、 ↑のコードは Unmarshal でエラーにならない
こんな感じに Unmarshal される
hoge.A{ B: &hoge.B{ C1: &hoge.C{ D:nil, }, C2: nil, }, }
なぜこうなるのか、ドキュメントを参考にバイナリを読んでみた
bytes の hex dump は 0a040a02080112040a020801
と出力された
色分けしたりすると、こんな感じになる
B を Marshal しているので、1バイト目が C1 に対応する 00001010 となり、7バイト目に C2 に対応する 00010010 が来る
では、これを A だと思って Unmarshal するとどうなるか
まず、A の最初のフィールドは B なので、1, 2バイト目の 00001010 00000100 は特に問題無い(元はC1)
3, 4 バイト目の 00001010 00000010 は C1 と解釈されるので、こちらも問題無い(元はD)
次のフィールドとして期待されるのは D で、D の ID は LEN のはずだが、 5, 6 バイト目の 00001000 00000001 は ID が VARINT となっている
ここでおかしい〜となってエラーが返るかと思いきや、エラーにはならず、D: nil になる
では、どんな場合でもエラーにならないのか?
proto を少し修正して、D の field を string にしてみる
message D { string e = 1; }
どうように実行してみると
func main() { b := hoge.B{ C1: &hoge.C{ D: &hoge.D{ E: "", }, }, } bytes, _ := proto.Marshal(&b) fmt.Printf("%s\n", hex.EncodeToString(bytes)) var a hoge.A if err := proto.Unmarshal(bytes, &a); err != nil { fmt.Printf("Error: %s", err) return } }
これもエラーにならない(C2 は除いた)
バイナリは 0a020a00
となり、読み解くと以下のような感じになる
E が空文字だからか、D は Size: 0 で E については何も書いてない
それはそうという感じ
次に、E に文字を入れて
b := hoge.B{ C1: &hoge.C{ D: &hoge.D{ E: "!", }, }, }
でやってみると、
Error: proto: cannot parse invalid wire-format data
になった
A で Marshal すると、1--6バイト目まではすんなり通るが、 7バイト目は Message (LEN) のはずなので、ID: 1 => I64, Tag: 4 と解釈され、 次に来るべきSizeなどが無く、不完全なデータだからエラーになっているのだろうか?
では、E を少し変えて
b := hoge.B{ C1: &hoge.C{ D: &hoge.D{ E: "\"\n!!!!!!!!!!", }, }, }
としてみると、エラー無く通るようになる
バイナリは 0a100a0e0a0c220a21212121212121212121
で、読み解くと以下のようになる
つまり、"\"\n!!!!!!!!!!"
の文字列 (12文字) は Size: 10 の文字列を持つ Message と等価(紫点線で囲んだところ)なので、
エラー無く Unmarshal できるということみたい
"!" の文字数が多かったり少なかったりすると、当然 Size と Data の数が合わなくなるので、エラーになる
スッキリした 可変長データだとこういうことが起こるんだな〜
結論としては、Mershal する proto と Unmarshal する proto が違ってても、エラーにならないことがある