| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package impl_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "strings" |
| "testing" |
| |
| "google.golang.org/protobuf/encoding/protowire" |
| "google.golang.org/protobuf/proto" |
| "google.golang.org/protobuf/testing/protopack" |
| |
| lnwtpb "google.golang.org/protobuf/internal/testprotos/lazy" |
| ) |
| |
| func unmarshalsTheSame(b []byte, expected *lnwtpb.FTop) error { |
| unmarshaledTop := &lnwtpb.FTop{} |
| if err := proto.Unmarshal(b, unmarshaledTop); err != nil { |
| return err |
| } |
| if !proto.Equal(unmarshaledTop, expected) { |
| return fmt.Errorf("!proto.Equal") |
| } |
| return nil |
| } |
| |
| func bytesTag(num protowire.Number) protopack.Tag { |
| return protopack.Tag{ |
| Number: num, |
| Type: protopack.BytesType, |
| } |
| } |
| |
| func varintTag(num protowire.Number) protopack.Tag { |
| return protopack.Tag{ |
| Number: num, |
| Type: protopack.VarintType, |
| } |
| } |
| |
| // Constructs a message encoded in denormalized (non-minimal) wire format, but |
| // using two levels of nesting: A top-level message with a child message which |
| // in turn has a grandchild message. |
| func denormalizedTwoLevelField() ([]byte, *lnwtpb.FTop, error) { |
| expectedMessage := &lnwtpb.FTop{ |
| A: proto.Uint32(2342), |
| Child: &lnwtpb.FSub{ |
| Grandchild: &lnwtpb.FSub{ |
| B: proto.Uint32(1337), |
| }, |
| }, |
| } |
| |
| fullMessage := protopack.Message{ |
| varintTag(1), protopack.Varint(2342), |
| // Child |
| bytesTag(2), protopack.LengthPrefix(protopack.Message{ |
| // Grandchild |
| bytesTag(4), protopack.LengthPrefix(protopack.Message{ |
| // The first occurrence of B matches expectedMessage: |
| varintTag(2), protopack.Varint(1337), |
| // This second duplicative occurrence of B is spec'd in Protobuf: |
| // https://github.com/protocolbuffers/protobuf/issues/9257 |
| varintTag(2), protopack.Varint(1337), |
| }), |
| }), |
| }.Marshal() |
| |
| return fullMessage, expectedMessage, nil |
| } |
| |
| func TestInvalidWireFormat(t *testing.T) { |
| fullMessage, expectedMessage, err := denormalizedTwoLevelField() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| top := &lnwtpb.FTop{} |
| if err := proto.Unmarshal(fullMessage, top); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Access the top-level submessage, but not the grandchild. |
| // This populates the size cache in the top-level message. |
| top.GetChild() |
| |
| marshal1, err := proto.MarshalOptions{ |
| UseCachedSize: true, |
| }.Marshal(top) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := unmarshalsTheSame(marshal1, expectedMessage); err != nil { |
| t.Error(err) |
| } |
| |
| // Call top.GetChild().GetGrandchild() to unmarshal the lazy message, |
| // which will normalize it: the size cache shrinks from 6 bytes to 3. |
| // Notably, top.GetChild()’s size cache is not updated! |
| top.GetChild().GetGrandchild() |
| marshal2, err := proto.MarshalOptions{ |
| // GetGrandchild+UseCachedSize is one way to trigger this bug. |
| // The other way is to call GetGrandchild in another goroutine, |
| // after proto.Marshal has called proto.Size but |
| // before proto.Marshal started encoding. |
| UseCachedSize: true, |
| }.Marshal(top) |
| if err != nil { |
| if strings.Contains(err.Error(), "size mismatch") { |
| // This is the expected failure mode: proto.Marshal() detects the |
| // combination of non-minimal wire format and lazy decoding and |
| // returns an error, prompting the user to disable lazy decoding. |
| return |
| } |
| t.Fatal(err) |
| } |
| if err := unmarshalsTheSame(marshal2, expectedMessage); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| func TestIdenticalOverAccessWhenDeterministic(t *testing.T) { |
| fullMessage, _, err := denormalizedTwoLevelField() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| top := &lnwtpb.FTop{} |
| if err := proto.Unmarshal(fullMessage, top); err != nil { |
| t.Fatal(err) |
| } |
| |
| deterministic := proto.MarshalOptions{ |
| Deterministic: true, |
| } |
| marshal1, err := deterministic.Marshal(top) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Call top.GetChild().GetGrandchild() to unmarshal the lazy message, |
| // which will normalize it. |
| top.GetChild().GetGrandchild() |
| |
| marshal2, err := deterministic.Marshal(top) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !bytes.Equal(marshal1, marshal2) { |
| t.Errorf("MarshalOptions{Deterministic: true}.Marshal() not identical over accessing a non-minimal wire format lazy message:\nbefore:\n%x\nafter:\n%x", marshal1, marshal2) |
| } |
| } |