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)) +}