From 268eaacfb4774fe655a39109af52be67cecea8ce Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Wed, 26 Jun 2024 15:41:33 +0400 Subject: [PATCH] Add functionality for Protocol Buffers stable marshaling This introduces `proto` package providing specific protobuf encoding - strict ascending field order. The format is used in NeoFS for cryptographic signatures requiring consistent checksums. Same lib is provided by `github.com/nspcc-dev/neofs-api-go/v2` module, but it is planned to be deprecated soon. Although all functionality is duplicated, the code is improved with generic typing that have appeared during this time. The library is intended to be used by other SDK packages, and does not yet imply external import. Signed-off-by: Leonard Lyubich --- internal/proto/encoding.go | 249 +++++++++++++++++++ internal/proto/encoding_test.go | 412 ++++++++++++++++++++++++++++++++ 2 files changed, 661 insertions(+) create mode 100644 internal/proto/encoding.go create mode 100644 internal/proto/encoding_test.go diff --git a/internal/proto/encoding.go b/internal/proto/encoding.go new file mode 100644 index 00000000..79e7e9c9 --- /dev/null +++ b/internal/proto/encoding.go @@ -0,0 +1,249 @@ +// Package proto contains helper functions for Protocol Buffers +// (https://protobuf.dev) in addition to the ones from +// [google.golang.org/protobuf/encoding/protowire] package. +package proto + +import ( + "encoding/binary" + "math" + + "google.golang.org/protobuf/encoding/protowire" +) + +// Message is provided by protobuf 'message' types used in NeoFS for so-called +// stable marshaling: protobuf with the order of fields in strict ascending +// order of their numbers. +type Message interface { + // MarshaledSize returns size of the encoded Message in bytes. + MarshaledSize() int + // MarshalStable encodes the Message into b. If the buffer is too small, + // MarshalStable will panic. + MarshalStable(b []byte) +} + +// Bytes is a type parameter constraint for any byte arrays. +type Bytes interface{ ~[]byte | ~string } + +// Varint is a type parameter constraint for any variable-length protobuf +// integers. +type Varint interface { + ~int32 | int64 | uint32 | uint64 // ~int32 for 'enum' fields +} + +// SizeVarint returns the encoded size of varint protobuf field with given +// number and value. +func SizeVarint[T Varint](num protowire.Number, v T) int { + if v == 0 { + return 0 + } + return protowire.SizeTag(num) + protowire.SizeVarint(uint64(v)) +} + +// MarshalVarint encodes varint protobuf field with given number and value into +// b and returns the number of bytes written. If the buffer is too small, +// MarshalVarint will panic. +func MarshalVarint[T Varint](b []byte, num protowire.Number, v T) int { + if v == 0 { + return 0 + } + off := binary.PutUvarint(b, protowire.EncodeTag(num, protowire.VarintType)) + return off + binary.PutUvarint(b[off:], uint64(v)) +} + +// SizeBool returns the encoded size of 'bool' protobuf field with given number +// and value. +func SizeBool(num protowire.Number, v bool) int { + return SizeVarint(num, protowire.EncodeBool(v)) +} + +// MarshalBool encodes 'bool' protobuf field with given number and value into b +// and returns the number of bytes written. If the buffer is too small, +// MarshalBool will panic. +func MarshalBool(b []byte, num protowire.Number, v bool) int { + return MarshalVarint(b, num, protowire.EncodeBool(v)) +} + +// SizeBytes returns the encoded size of 'bytes' or 'string' protobuf field with +// given number and value. +func SizeBytes[T Bytes](num protowire.Number, v T) int { + ln := len(v) + if ln == 0 { + return 0 + } + return protowire.SizeTag(num) + protowire.SizeBytes(ln) +} + +// MarshalBytes encodes 'bytes' or 'string' protobuf field with given number and +// value into b and returns the number of bytes written. If the buffer is too +// small, MarshalBytes will panic. +func MarshalBytes[T Bytes](b []byte, num protowire.Number, v T) int { + if len(v) == 0 { + return 0 + } + off := binary.PutUvarint(b, protowire.EncodeTag(num, protowire.BytesType)) + off += binary.PutUvarint(b[off:], uint64(len(v))) + return off + copy(b[off:], v) +} + +// SizeFixed32 returns the encoded size of 'fixed32' protobuf field with given +// number and value. +func SizeFixed32(num protowire.Number, v uint32) int { + if v == 0 { + return 0 + } + return protowire.SizeTag(num) + protowire.SizeFixed32() +} + +// MarshalFixed32 encodes 'fixed32' protobuf field with given number and value +// into b and returns the number of bytes written. If the buffer is too small, +// MarshalFixed32 will panic. +func MarshalFixed32(b []byte, num protowire.Number, v uint32) int { + if v == 0 { + return 0 + } + off := binary.PutUvarint(b, protowire.EncodeTag(num, protowire.Fixed32Type)) + binary.LittleEndian.PutUint32(b[off:], v) + return off + protowire.SizeFixed32() +} + +// SizeFixed64 returns the encoded size of 'fixed64' protobuf field with given +// number and value. +func SizeFixed64(num protowire.Number, v uint64) int { + if v == 0 { + return 0 + } + return protowire.SizeTag(num) + protowire.SizeFixed64() +} + +// MarshalFixed64 encodes 'fixed64' protobuf field with given number and value +// into b and returns the number of bytes written. If the buffer is too small, +// MarshalFixed64 will panic. +func MarshalFixed64(b []byte, num protowire.Number, v uint64) int { + if v == 0 { + return 0 + } + off := binary.PutUvarint(b, protowire.EncodeTag(num, protowire.Fixed64Type)) + binary.LittleEndian.PutUint64(b[off:], v) + return off + protowire.SizeFixed64() +} + +// SizeFloat returns the encoded size of 'float' protobuf field with given +// number and value. +func SizeFloat(num protowire.Number, v float32) int { + return SizeFixed32(num, math.Float32bits(v)) +} + +// MarshalFloat encodes 'float' protobuf field with given number and value into +// b and returns the number of bytes written. If the buffer is too small, +// MarshalFloat will panic. +func MarshalFloat(b []byte, num protowire.Number, v float32) int { + return MarshalFixed32(b, num, math.Float32bits(v)) +} + +// SizeDouble returns the encoded size of 'double' protobuf field with given +// number and value. +func SizeDouble(num protowire.Number, v float64) int { + return SizeFixed64(num, math.Float64bits(v)) +} + +// MarshalDouble encodes 'double' protobuf field with given number and value +// into b and returns the number of bytes written. If the buffer is too small, +// MarshalDouble will panic. +func MarshalDouble(b []byte, num protowire.Number, v float64) int { + return MarshalFixed64(b, num, math.Float64bits(v)) +} + +// SizeEmbedded returns the encoded size of embedded message being a protobuf +// field with given number and value. +func SizeEmbedded(num protowire.Number, v Message) int { + if v == nil { + return 0 + } + sz := v.MarshaledSize() + if sz == 0 { + return 0 + } + return protowire.SizeTag(num) + protowire.SizeBytes(sz) +} + +// MarshalEmbedded encodes embedded message being a protobuf field with given +// number and value into b and returns the number of bytes written. If the +// buffer is too small, MarshalEmbedded will panic. +func MarshalEmbedded(b []byte, num protowire.Number, v Message) int { + if v == nil { + return 0 + } + sz := v.MarshaledSize() + if sz == 0 { + return 0 + } + off := binary.PutUvarint(b, protowire.EncodeTag(num, protowire.BytesType)) + off += binary.PutUvarint(b[off:], uint64(sz)) + v.MarshalStable(b[off:]) + return off + sz +} + +func sizeRepeatedVarint[T Varint](v []T) int { + var sz int + for i := range v { + // packed (https://protobuf.dev/programming-guides/encoding/#packed) + sz += protowire.SizeVarint(uint64(v[i])) + } + return sz +} + +// SizeRepeatedVarint returns the encoded size of 'repeated' varint protobuf +// field with given number and value. +func SizeRepeatedVarint[T Varint](num protowire.Number, v []T) int { + if len(v) == 0 { + return 0 + } + return protowire.SizeTag(num) + protowire.SizeBytes(sizeRepeatedVarint(v)) +} + +// MarshalRepeatedVarint encodes 'repeated' varint protobuf field with given +// number and value into b and returns the number of bytes written. If the +// buffer is too small, MarshalRepeatedVarint will panic. +func MarshalRepeatedVarint[T Varint](b []byte, num protowire.Number, v []T) int { + if len(v) == 0 { + return 0 + } + off := binary.PutUvarint(b, protowire.EncodeTag(num, protowire.BytesType)) + off += binary.PutUvarint(b[off:], uint64(sizeRepeatedVarint(v))) + for i := range v { + off += binary.PutUvarint(b[off:], uint64(v[i])) + } + return off +} + +// SizeRepeatedBytes returns the encoded size of 'repeated bytes' or 'repeated +// string' protobuf field with given number and value. +func SizeRepeatedBytes[T Bytes](num protowire.Number, v []T) int { + if len(v) == 0 { + return 0 + } + var sz int + tagSz := protowire.SizeTag(num) + for i := range v { + // non-packed (https://protobuf.dev/programming-guides/encoding/#packed) + sz += tagSz + protowire.SizeBytes(len(v[i])) + } + return sz +} + +// MarshalRepeatedBytes encodes 'repeated bytes' or 'repeated string' protobuf +// field with given number and value into b and returns the number of bytes +// written. If the buffer is too small, MarshalRepeatedBytes will panic. +func MarshalRepeatedBytes[T Bytes](b []byte, num protowire.Number, v []T) int { + if len(v) == 0 { + return 0 + } + var off int + tag := protowire.EncodeTag(num, protowire.BytesType) + for i := range v { + off += binary.PutUvarint(b[off:], tag) + off += binary.PutUvarint(b[off:], uint64(len(v[i]))) + off += copy(b[off:], v[i]) + } + return off +} diff --git a/internal/proto/encoding_test.go b/internal/proto/encoding_test.go new file mode 100644 index 00000000..b7adcb17 --- /dev/null +++ b/internal/proto/encoding_test.go @@ -0,0 +1,412 @@ +package proto_test + +import ( + "fmt" + "math" + "math/rand" + "testing" + + "github.com/nspcc-dev/neofs-sdk-go/internal/proto" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protowire" + stdproto "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type enum int32 + +type timestamp timestamppb.Timestamp + +func (x *timestamp) MarshaledSize() int { + var sz int + if x != nil { + if x.Seconds != 0 { + sz += protowire.SizeTag(1) + protowire.SizeVarint(uint64(x.Seconds)) + } + if x.Nanos != 0 { + sz += protowire.SizeTag(2) + protowire.SizeVarint(uint64(x.Nanos)) + } + } + return sz +} + +func (x *timestamp) MarshalStable(b []byte) { + if x != nil { + b2 := b[:0:cap(b)] + if x.Seconds != 0 { + b2 = protowire.AppendTag(b2, 1, protowire.VarintType) + b2 = protowire.AppendVarint(b2, uint64(x.Seconds)) + } + if x.Nanos != 0 { + b2 = protowire.AppendTag(b2, 2, protowire.VarintType) + protowire.AppendVarint(b2, uint64(x.Nanos)) + } + } +} + +func randFieldNum() protowire.Number { + n := protowire.Number(rand.Uint32() % uint32(protowire.MaxValidNumber)) + if n < protowire.MinValidNumber { + return protowire.MinValidNumber + } + return n +} + +func randBytes[T proto.Bytes]() T { + ln := rand.Uint32() % 64 + if ln == 0 { + ln = 1 + } + b := make([]byte, ln) + //nolint:staticcheck // cryptorandom is not required for this test + rand.Read(b) + return T(b) +} + +func randVarint[T proto.Varint]() T { return T(rand.Uint64()) } + +func randRepeatedBytes[T proto.Bytes]() []T { + n := rand.Uint32() % 10 + if n == 0 { + n = 1 + } + res := make([]T, n) + for i := range res { + res[i] = randBytes[T]() + } + // unlike non-repeated field, zero element of repeated field must be presented + res[rand.Uint32()%n] = T("") + return res +} + +func randRepeatedVarint[T proto.Varint]() []T { + n := rand.Uint32() % 10 + if n == 0 { + n = 1 + } + res := make([]T, n) + for i := range res { + res[i] = T(rand.Uint64()) + } + // unlike non-repeated field, zero element of repeated field must be presented + res[rand.Uint32()%n] = 0 + return res +} + +func consumeVarint[T proto.Varint](b []byte) (T, int) { + v, ln := protowire.ConsumeVarint(b) + return T(v), ln +} + +func consumeRepeatedVarint[T proto.Varint](b []byte) ([]T, int) { + bs, code := protowire.ConsumeBytes(b) + if code < 0 { + return nil, code + } + var res []T + for len(bs) > 0 { + i, ln := protowire.ConsumeVarint(bs) + if ln < 0 { + return nil, ln + } + res = append(res, T(i)) + bs = bs[ln:] + } + return res, 0 +} + +func consumeBytes[T proto.Bytes](b []byte) (T, int) { + v, ln := protowire.ConsumeBytes(b) + return T(v), ln +} + +func consumePackedRepeated[T proto.Bytes](b []byte, num protowire.Number) ([]T, int) { + var res []T + for len(b) > 0 { + n, t, tagLn := protowire.ConsumeTag(b) + if tagLn < 0 { + return nil, tagLn + } else if n != num { + return nil, -2 + } else if t != protowire.BytesType { + return nil, math.MinInt + } + + bs, ln := protowire.ConsumeBytes(b[tagLn:]) + if ln < 0 { + return nil, ln + } + res = append(res, T(bs)) + b = b[tagLn+ln:] + } + return res, 0 +} + +type anySupportedType interface { + proto.Varint | proto.Bytes | bool | float32 | float64 | []uint64 | []uint32 | []int64 | []int32 | []enum +} + +func testEncoding[T anySupportedType]( + t testing.TB, + wireType protowire.Type, + sizeFunc func(protowire.Number, T) int, + marshalFunc func([]byte, protowire.Number, T) int, + consumeFunc func([]byte) (T, int), + randFunc func() T, +) { + var v T + fieldNum := randFieldNum() + require.Zero(t, sizeFunc(fieldNum, v), fieldNum) + require.Zero(t, marshalFunc(nil, fieldNum, v), fieldNum) + + v = randFunc() + msg := fmt.Sprintf("num=%d,val=%v", fieldNum, v) + + sz := sizeFunc(fieldNum, v) + b := make([]byte, sz) + require.EqualValues(t, sz, marshalFunc(b, fieldNum, v), msg) + + num, typ, tagLn := protowire.ConsumeTag(b) + require.NoError(t, protowire.ParseError(tagLn), msg) + require.EqualValues(t, fieldNum, num, msg) + require.Equal(t, wireType, typ, msg) + + res, code := consumeFunc(b[tagLn:]) + require.NoError(t, protowire.ParseError(code)) + require.EqualValues(t, v, res, msg) +} + +func testPackedRepeated[T proto.Bytes](t testing.TB, + sizeFunc func(protowire.Number, []T) int, + marshalFunc func([]byte, protowire.Number, []T) int, + consumeFunc func([]byte, protowire.Number) ([]T, int), + randFunc func() []T, +) { + var v []T + fieldNum := randFieldNum() + require.Zero(t, sizeFunc(fieldNum, v), fieldNum) + require.Zero(t, marshalFunc(nil, fieldNum, v), fieldNum) + require.Zero(t, sizeFunc(fieldNum, []T{}), fieldNum) + require.Zero(t, marshalFunc(nil, fieldNum, []T{}), fieldNum) + + v = randFunc() + msg := fmt.Sprintf("num=%d,val=%v", fieldNum, v) + + sz := sizeFunc(fieldNum, v) + b := make([]byte, sz) + require.EqualValues(t, sz, marshalFunc(b, fieldNum, v), msg) + + res, code := consumeFunc(b, fieldNum) + require.NoError(t, protowire.ParseError(code)) + require.EqualValues(t, v, res, msg) +} + +func benchmarkType[T anySupportedType]( + b *testing.B, + v T, + sizeFunc func(protowire.Number, T) int, + marshalFunc func([]byte, protowire.Number, T) int, +) { + const fieldNum = protowire.MaxValidNumber + buf := make([]byte, sizeFunc(fieldNum, v)) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + marshalFunc(buf, fieldNum, v) + } +} + +func benchmarkRepeatedType[T anySupportedType]( + b *testing.B, + v []T, + sizeFunc func(protowire.Number, []T) int, + marshalFunc func([]byte, protowire.Number, []T) int, +) { + const fieldNum = protowire.MaxValidNumber + buf := make([]byte, sizeFunc(fieldNum, v)) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + marshalFunc(buf, fieldNum, v) + } +} + +func TestVarint(t *testing.T) { + testEncoding(t, protowire.VarintType, proto.SizeVarint[uint64], proto.MarshalVarint[uint64], protowire.ConsumeVarint, randVarint[uint64]) + testEncoding(t, protowire.VarintType, proto.SizeVarint[uint32], proto.MarshalVarint[uint32], consumeVarint[uint32], randVarint[uint32]) + testEncoding(t, protowire.VarintType, proto.SizeVarint[int64], proto.MarshalVarint[int64], consumeVarint[int64], randVarint[int64]) + testEncoding(t, protowire.VarintType, proto.SizeVarint[int32], proto.MarshalVarint[int32], consumeVarint[int32], randVarint[int32]) + testEncoding(t, protowire.VarintType, proto.SizeVarint[enum], proto.MarshalVarint[enum], consumeVarint[enum], randVarint[enum]) +} + +func benchmarkMarshalVarint[T proto.Varint](b *testing.B, v T) { + b.Run(fmt.Sprintf("%T", v), func(b *testing.B) { + benchmarkType(b, v, proto.SizeVarint[T], proto.MarshalVarint[T]) + }) +} + +func BenchmarkMarshalVarint(b *testing.B) { + v := uint64(math.MaxUint64) + benchmarkMarshalVarint(b, v) + benchmarkMarshalVarint(b, uint32(v)) + benchmarkMarshalVarint(b, int64(v)) + benchmarkMarshalVarint(b, int32(v)) +} + +func TestBool(t *testing.T) { + fieldNum := randFieldNum() + require.Zero(t, proto.SizeBool(fieldNum, false)) + require.Zero(t, proto.MarshalBool(nil, fieldNum, false)) + + sz := proto.SizeBool(fieldNum, true) + b := make([]byte, sz) + require.EqualValues(t, sz, proto.MarshalBool(b, fieldNum, true)) + + num, typ, tagLn := protowire.ConsumeTag(b) + require.Positive(t, tagLn, protowire.ParseError(tagLn)) + require.EqualValues(t, fieldNum, num) + require.EqualValues(t, protowire.VarintType, typ) + + res, code := protowire.ConsumeVarint(b[tagLn:]) + require.NoError(t, protowire.ParseError(code)) + require.EqualValues(t, 1, res) +} + +func BenchmarkMarshalBool(b *testing.B) { + benchmarkType(b, true, proto.SizeBool, proto.MarshalBool) +} + +func TestFixed32(t *testing.T) { + testEncoding(t, protowire.Fixed32Type, proto.SizeFixed32, proto.MarshalFixed32, protowire.ConsumeFixed32, rand.Uint32) +} + +func BenchmarkMarshalFixed32(b *testing.B) { + benchmarkType(b, math.MaxUint32, proto.SizeFixed32, proto.MarshalFixed32) +} + +func TestFixed64(t *testing.T) { + testEncoding(t, protowire.Fixed64Type, proto.SizeFixed64, proto.MarshalFixed64, protowire.ConsumeFixed64, rand.Uint64) +} + +func BenchmarkMarshalFixed64(b *testing.B) { + benchmarkType(b, math.MaxUint64, proto.SizeFixed64, proto.MarshalFixed64) +} + +func TestFloat(t *testing.T) { + testEncoding(t, protowire.Fixed32Type, proto.SizeFloat, proto.MarshalFloat, func(b []byte) (float32, int) { + v, ln := protowire.ConsumeFixed32(b) + return math.Float32frombits(v), ln + }, func() float32 { return math.Float32frombits(rand.Uint32()) }) +} + +func BenchmarkMarshalFloat(b *testing.B) { + benchmarkType(b, math.Float32frombits(math.MaxUint32), proto.SizeFloat, proto.MarshalFloat) +} + +func TestDouble(t *testing.T) { + testEncoding(t, protowire.Fixed64Type, proto.SizeDouble, proto.MarshalDouble, func(b []byte) (float64, int) { + v, ln := protowire.ConsumeFixed64(b) + return math.Float64frombits(v), ln + }, func() float64 { return math.Float64frombits(rand.Uint64()) }) +} + +func BenchmarkDouble(b *testing.B) { + benchmarkType(b, math.Float64frombits(math.MaxUint64), proto.SizeDouble, proto.MarshalDouble) +} + +func TestBytes(t *testing.T) { + testEncoding(t, protowire.BytesType, proto.SizeBytes[[]byte], proto.MarshalBytes[[]byte], consumeBytes[[]byte], randBytes[[]byte]) + testEncoding(t, protowire.BytesType, proto.SizeBytes[string], proto.MarshalBytes[string], consumeBytes[string], randBytes[string]) +} + +func benchmarkMarshalBytes[T proto.Bytes](b *testing.B, v T) { + b.Run(fmt.Sprintf("%T", v), func(b *testing.B) { + benchmarkType(b, v, proto.SizeBytes[T], proto.MarshalBytes[T]) + }) +} + +func BenchmarkMarshalBytes(b *testing.B) { + const bs = "Hello, world!" + benchmarkMarshalBytes(b, bs) + benchmarkMarshalBytes(b, []byte(bs)) +} + +func TestEmbedded(t *testing.T) { + fieldNum := randFieldNum() + require.Zero(t, proto.SizeEmbedded(fieldNum, nil)) + require.Zero(t, proto.MarshalEmbedded(nil, fieldNum, nil)) + require.Zero(t, proto.SizeEmbedded(fieldNum, (*timestamp)(nil))) + require.Zero(t, proto.MarshalEmbedded(nil, fieldNum, (*timestamp)(nil))) + require.Zero(t, proto.SizeEmbedded(fieldNum, new(timestamp))) + require.Zero(t, proto.MarshalEmbedded(nil, fieldNum, new(timestamp))) + + v := (*timestamp)(timestamppb.Now()) + sz := proto.SizeEmbedded(fieldNum, v) + b := make([]byte, sz) + require.EqualValues(t, sz, proto.MarshalEmbedded(b, fieldNum, v)) + + num, typ, tagLn := protowire.ConsumeTag(b) + require.Positive(t, tagLn, protowire.ParseError(tagLn)) + require.EqualValues(t, fieldNum, num) + require.EqualValues(t, protowire.BytesType, typ) + + res, code := protowire.ConsumeBytes(b[tagLn:]) + require.NoError(t, protowire.ParseError(code)) + var v2 timestamp + require.NoError(t, stdproto.Unmarshal(res, (*timestamppb.Timestamp)(&v2))) + require.Equal(t, v.Seconds, v2.Seconds) + require.Equal(t, v.Nanos, v2.Nanos) +} + +func BenchmarkMarshalEmbedded(b *testing.B) { + const fieldNum = protowire.MaxValidNumber + v := (*timestamp)(timestamppb.Now()) + buf := make([]byte, proto.SizeEmbedded(fieldNum, v)) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + proto.MarshalEmbedded(buf, fieldNum, v) + } +} + +func TestRepeatedVarint(t *testing.T) { + testEncoding(t, protowire.BytesType, proto.SizeRepeatedVarint[uint64], proto.MarshalRepeatedVarint[uint64], consumeRepeatedVarint[uint64], randRepeatedVarint[uint64]) + testEncoding(t, protowire.BytesType, proto.SizeRepeatedVarint[uint32], proto.MarshalRepeatedVarint[uint32], consumeRepeatedVarint[uint32], randRepeatedVarint[uint32]) + testEncoding(t, protowire.BytesType, proto.SizeRepeatedVarint[int64], proto.MarshalRepeatedVarint[int64], consumeRepeatedVarint[int64], randRepeatedVarint[int64]) + testEncoding(t, protowire.BytesType, proto.SizeRepeatedVarint[int32], proto.MarshalRepeatedVarint[int32], consumeRepeatedVarint[int32], randRepeatedVarint[int32]) + testEncoding(t, protowire.BytesType, proto.SizeRepeatedVarint[enum], proto.MarshalRepeatedVarint[enum], consumeRepeatedVarint[enum], randRepeatedVarint[enum]) +} + +func benchmarkMarshalRepeatedVarint[T proto.Varint](b *testing.B, v T) { + b.Run(fmt.Sprintf("%T", v), func(b *testing.B) { + benchmarkRepeatedType(b, []T{v, v, v}, proto.SizeRepeatedVarint[T], proto.MarshalRepeatedVarint[T]) + }) +} + +func BenchmarkMarshalRepeatedVarint(b *testing.B) { + v := uint64(math.MaxUint64) + benchmarkMarshalRepeatedVarint(b, v) + benchmarkMarshalRepeatedVarint(b, uint32(v)) + benchmarkMarshalRepeatedVarint(b, int64(v)) + benchmarkMarshalRepeatedVarint(b, int32(v)) + benchmarkMarshalRepeatedVarint(b, enum(v)) +} + +func TestRepeatedBytes(t *testing.T) { + testPackedRepeated(t, proto.SizeRepeatedBytes[[]byte], proto.MarshalRepeatedBytes[[]byte], consumePackedRepeated[[]byte], randRepeatedBytes[[]byte]) + testPackedRepeated(t, proto.SizeRepeatedBytes[string], proto.MarshalRepeatedBytes[string], consumePackedRepeated[string], randRepeatedBytes[string]) +} + +func benchmarkMarshalRepeatedBytes[T proto.Bytes](b *testing.B, v T) { + b.Run(fmt.Sprintf("%T", v), func(b *testing.B) { + benchmarkRepeatedType(b, []T{v, v, v}, proto.SizeRepeatedBytes[T], proto.MarshalRepeatedBytes[T]) + }) +} + +func BenchmarkMarshalRepeatedBytes(b *testing.B) { + const bs = "Hello, world!" + benchmarkMarshalRepeatedBytes(b, bs) + benchmarkMarshalRepeatedBytes(b, []byte(bs)) +}