はやし雑記

はやしです

proto の Unmarshal は Marshal する proto と違っててもエラーにならないことがある

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,
    },
}

なぜこうなるのか、ドキュメントを参考にバイナリを読んでみた

developers.google.com

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 が違ってても、エラーにならないことがある