diff --git a/util/proto/marshal.go b/util/proto/marshal.go new file mode 100644 index 0000000..cd9c493 --- /dev/null +++ b/util/proto/marshal.go @@ -0,0 +1,44 @@ +/* +This package contains help functions for stable marshaller. Their usage is +totally optional. One can implement fast stable marshaller without these +runtime function calls. +*/ + +package proto + +import ( + "encoding/binary" + "math/bits" +) + +func BytesMarshal(field int, buf, v []byte) (int, error) { + if len(v) == 0 { + return 0, nil + } + + // buf length check can prevent panic at PutUvarint, but it will make + // marshaller a bit slower. + + prefix := field<<3 | 0x2 + i := binary.PutUvarint(buf, uint64(prefix)) + i += binary.PutUvarint(buf[i:], uint64(len(v))) + i += copy(buf[i:], v) + + return i, nil +} + +func BytesSize(field int, v []byte) int { + ln := len(v) + if ln == 0 { + return 0 + } + + prefix := field<<3 | 0x2 + + return VarUIntSize(uint64(prefix)) + VarUIntSize(uint64(ln)) + ln +} + +// varUIntSize returns length of varint byte sequence for uint64 value 'x'. +func VarUIntSize(x uint64) int { + return (bits.Len64(x|1) + 6) / 7 +} diff --git a/util/proto/marshal_test.go b/util/proto/marshal_test.go new file mode 100644 index 0000000..ccc1cca --- /dev/null +++ b/util/proto/marshal_test.go @@ -0,0 +1,118 @@ +package proto_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/util/proto" + "github.com/nspcc-dev/neofs-api-go/util/proto/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type stablePrimitives struct { + FieldA []byte +} + +func (s *stablePrimitives) stableMarshal(buf []byte) ([]byte, error) { + if s == nil { + return []byte{}, nil + } + + if buf == nil { + buf = make([]byte, s.stableSize()) + } + + var ( + i, offset int + ) + + offset, err := proto.BytesMarshal(1, buf, s.FieldA) + if err != nil { + return nil, errors.Wrap(err, "can't marshal field a") + } + i += offset + + return buf, nil +} + +func (s *stablePrimitives) stableMarshalWrongFieldNum(buf []byte) ([]byte, error) { + if s == nil { + return []byte{}, nil + } + + if buf == nil { + buf = make([]byte, s.stableSize()) + } + + var ( + i, offset int + ) + + offset, err := proto.BytesMarshal(1+1, buf, s.FieldA) + if err != nil { + return nil, errors.Wrap(err, "can't marshal field a") + } + i += offset + + return buf, nil +} + +func (s *stablePrimitives) stableSize() int { + return proto.BytesSize(1, s.FieldA) +} + +func TestBytesMarshal(t *testing.T) { + t.Run("not empty", func(t *testing.T) { + data := []byte("Hello World") + testBytesMarshal(t, data, false) + testBytesMarshal(t, data, true) + }) + + t.Run("empty", func(t *testing.T) { + testBytesMarshal(t, []byte{}, false) + }) + + t.Run("nil", func(t *testing.T) { + testBytesMarshal(t, nil, false) + }) +} + +func testBytesMarshal(t *testing.T, data []byte, wrongField bool) { + var ( + wire []byte + err error + + custom = stablePrimitives{FieldA: data} + transport = test.Primitives{FieldA: data} + ) + + if !wrongField { + wire, err = custom.stableMarshal(nil) + } else { + wire, err = custom.stableMarshalWrongFieldNum(nil) + } + require.NoError(t, err) + + wireGen, err := transport.Marshal() + require.NoError(t, err) + + if !wrongField { + // we can check equality because single field cannot be unstable marshalled + require.Equal(t, wireGen, wire) + } else { + require.NotEqual(t, wireGen, wire) + } + + result := new(test.Primitives) + err = result.Unmarshal(wire) + require.NoError(t, err) + + if !wrongField { + require.Len(t, result.FieldA, len(data)) + if len(data) > 0 { + require.Equal(t, data, result.FieldA) + } + } else { + require.Len(t, result.FieldA, 0) + } +} diff --git a/util/proto/test/test.pb.go b/util/proto/test/test.pb.go new file mode 100644 index 0000000..5db45e5 Binary files /dev/null and b/util/proto/test/test.pb.go differ diff --git a/util/proto/test/test.proto b/util/proto/test/test.proto new file mode 100644 index 0000000..cf51a44 --- /dev/null +++ b/util/proto/test/test.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package test; + +message Primitives { + bytes field_a = 1; +} \ No newline at end of file