diff --git a/v2/tombstone/convert.go b/v2/tombstone/convert.go new file mode 100644 index 00000000..c2e6ecde --- /dev/null +++ b/v2/tombstone/convert.go @@ -0,0 +1,53 @@ +package tombstone + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/refs" + refsGRPC "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + tombstone "github.com/nspcc-dev/neofs-api-go/v2/tombstone/grpc" +) + +// TombstoneToGRPCMessage converts unified tombstone message into gRPC message. +func TombstoneToGRPCMessage(t *Tombstone) *tombstone.Tombstone { + if t == nil { + return nil + } + + m := new(tombstone.Tombstone) + + m.SetExpirationEpoch(t.GetExpirationEpoch()) + m.SetSplitId(t.GetSplitID()) + + members := t.GetMembers() + memberMsg := make([]*refsGRPC.ObjectID, 0, len(members)) + + for i := range members { + memberMsg = append(memberMsg, refs.ObjectIDToGRPCMessage(members[i])) + } + + m.SetMembers(memberMsg) + + return m +} + +// TombstoneFromGRPCMessage converts gRPC message into unified tombstone message. +func TombstoneFromGRPCMessage(m *tombstone.Tombstone) *Tombstone { + if m == nil { + return nil + } + + t := new(Tombstone) + + t.SetExpirationEpoch(m.GetExpirationEpoch()) + t.SetSplitID(m.GetSplitId()) + + memberMsg := m.GetMembers() + members := make([]*refs.ObjectID, 0, len(memberMsg)) + + for i := range memberMsg { + members = append(members, refs.ObjectIDFromGRPCMessage(memberMsg[i])) + } + + t.SetMembers(members) + + return t +} diff --git a/v2/tombstone/grpc/types.go b/v2/tombstone/grpc/types.go new file mode 100644 index 00000000..17fe9db6 --- /dev/null +++ b/v2/tombstone/grpc/types.go @@ -0,0 +1,26 @@ +package tombstone + +import ( + refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" +) + +// SetExpirationEpoch sets number of tombstone expiration epoch. +func (x *Tombstone) SetExpirationEpoch(v uint64) { + if x != nil { + x.ExpirationEpoch = v + } +} + +// SetSplitId sets identifier of split object hierarchy. +func (x *Tombstone) SetSplitId(v []byte) { + if x != nil { + x.SplitId = v + } +} + +// SetMembers sets list of objects to be deleted. +func (x *Tombstone) SetMembers(v []*refs.ObjectID) { + if x != nil { + x.Members = v + } +} diff --git a/v2/tombstone/json.go b/v2/tombstone/json.go new file mode 100644 index 00000000..ffa0e157 --- /dev/null +++ b/v2/tombstone/json.go @@ -0,0 +1,26 @@ +package tombstone + +import ( + tombstone "github.com/nspcc-dev/neofs-api-go/v2/tombstone/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func (s *Tombstone) MarshalJSON() ([]byte, error) { + return protojson.MarshalOptions{ + EmitUnpopulated: true, + }.Marshal( + TombstoneToGRPCMessage(s), + ) +} + +func (s *Tombstone) UnmarshalJSON(data []byte) error { + msg := new(tombstone.Tombstone) + + if err := protojson.Unmarshal(data, msg); err != nil { + return err + } + + *s = *TombstoneFromGRPCMessage(msg) + + return nil +} diff --git a/v2/tombstone/json_test.go b/v2/tombstone/json_test.go new file mode 100644 index 00000000..441f173f --- /dev/null +++ b/v2/tombstone/json_test.go @@ -0,0 +1,20 @@ +package tombstone_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/tombstone" + "github.com/stretchr/testify/require" +) + +func TestTombstoneJSON(t *testing.T) { + from := generateTombstone() + + data, err := from.MarshalJSON() + require.NoError(t, err) + + to := new(tombstone.Tombstone) + require.NoError(t, to.UnmarshalJSON(data)) + + require.Equal(t, from, to) +} diff --git a/v2/tombstone/marshal.go b/v2/tombstone/marshal.go new file mode 100644 index 00000000..9b9c8981 --- /dev/null +++ b/v2/tombstone/marshal.go @@ -0,0 +1,83 @@ +package tombstone + +import ( + "github.com/nspcc-dev/neofs-api-go/util/proto" + tombstone "github.com/nspcc-dev/neofs-api-go/v2/tombstone/grpc" + goproto "google.golang.org/protobuf/proto" +) + +const ( + expFNum = 1 + splitIDFNum = 2 + membersFNum = 3 +) + +// StableMarshal marshals unified tombstone message in a protobuf +// compatible way without field order shuffle. +func (s *Tombstone) StableMarshal(buf []byte) ([]byte, error) { + if s == nil { + return []byte{}, nil + } + + if buf == nil { + buf = make([]byte, s.StableSize()) + } + + var ( + offset, n int + err error + ) + + n, err = proto.UInt64Marshal(expFNum, buf[offset:], s.exp) + if err != nil { + return nil, err + } + + offset += n + + n, err = proto.BytesMarshal(splitIDFNum, buf[offset:], s.splitID) + if err != nil { + return nil, err + } + + offset += n + + for i := range s.members { + n, err = proto.NestedStructureMarshal(membersFNum, buf[offset:], s.members[i]) + if err != nil { + return nil, err + } + + offset += n + } + + return buf, nil +} + +// StableSize returns size of tombstone message marshalled by StableMarshal function. +func (s *Tombstone) StableSize() (size int) { + if s == nil { + return 0 + } + + size += proto.UInt64Size(expFNum, s.exp) + size += proto.BytesSize(splitIDFNum, s.splitID) + + for i := range s.members { + size += proto.NestedStructureSize(membersFNum, s.members[i]) + } + + return size +} + +// Unmarshal unmarshal tombstone message from its binary representation. +func (s *Tombstone) Unmarshal(data []byte) error { + m := new(tombstone.Tombstone) + if err := goproto.Unmarshal(data, m); err != nil { + return err + } + + *s = *TombstoneFromGRPCMessage(m) + + return nil +} diff --git a/v2/tombstone/marshal_test.go b/v2/tombstone/marshal_test.go new file mode 100644 index 00000000..c7eda2b4 --- /dev/null +++ b/v2/tombstone/marshal_test.go @@ -0,0 +1,39 @@ +package tombstone_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-api-go/v2/tombstone" + "github.com/stretchr/testify/require" +) + +func TestTombstone_StableMarshal(t *testing.T) { + from := generateTombstone() + + t.Run("non empty", func(t *testing.T) { + wire, err := from.StableMarshal(nil) + require.NoError(t, err) + + to := new(tombstone.Tombstone) + require.NoError(t, to.Unmarshal(wire)) + + require.Equal(t, from, to) + }) +} + +func generateTombstone() *tombstone.Tombstone { + t := new(tombstone.Tombstone) + + oid1 := new(refs.ObjectID) + oid1.SetValue([]byte("Object ID 1")) + + oid2 := new(refs.ObjectID) + oid2.SetValue([]byte("Object ID 2")) + + t.SetExpirationEpoch(100) + t.SetSplitID([]byte("split ID")) + t.SetMembers([]*refs.ObjectID{oid1, oid2}) + + return t +} diff --git a/v2/tombstone/types.go b/v2/tombstone/types.go new file mode 100644 index 00000000..2bcc19d1 --- /dev/null +++ b/v2/tombstone/types.go @@ -0,0 +1,63 @@ +package tombstone + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/refs" +) + +// Tombstone is a unified structure of Tombstone +// message from proto definition. +type Tombstone struct { + exp uint64 + + splitID []byte + + members []*refs.ObjectID +} + +// GetExpirationEpoch returns number of tombstone expiration epoch. +func (s *Tombstone) GetExpirationEpoch() uint64 { + if s != nil { + return s.exp + } + + return 0 +} + +// SetExpirationEpoch sets number of tombstone expiration epoch. +func (s *Tombstone) SetExpirationEpoch(v uint64) { + if s != nil { + s.exp = v + } +} + +// GetSplitID returns identifier of split object hierarchy. +func (s *Tombstone) GetSplitID() []byte { + if s != nil { + return s.splitID + } + + return nil +} + +// SetSplitID sets identifier of split object hierarchy. +func (s *Tombstone) SetSplitID(v []byte) { + if s != nil { + s.splitID = v + } +} + +// GetMembers returns list of objects to be deleted. +func (s *Tombstone) GetMembers() []*refs.ObjectID { + if s != nil { + return s.members + } + + return nil +} + +// SetMembers sets list of objects to be deleted. +func (s *Tombstone) SetMembers(v []*refs.ObjectID) { + if s != nil { + s.members = v + } +}