diff --git a/storagegroup/storagegroup.go b/storagegroup/storagegroup.go index df64a5da..a0d3ff16 100644 --- a/storagegroup/storagegroup.go +++ b/storagegroup/storagegroup.go @@ -1,9 +1,14 @@ package storagegroup import ( + "fmt" + "strconv" + + objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/storagegroup" "github.com/nspcc-dev/neofs-sdk-go/checksum" + objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" ) @@ -205,3 +210,87 @@ func formatCheck(v2 *storagegroup.StorageGroup) error { return nil } + +// ReadFromObject assemble StorageGroup from a regular +// Object structure. Object must contain unambiguous information +// about its expiration epoch, otherwise behaviour is undefined. +// +// Returns any error appeared during storage group parsing; returns +// error if object is not of TypeStorageGroup type. +func ReadFromObject(sg *StorageGroup, o objectSDK.Object) error { + if typ := o.Type(); typ != objectSDK.TypeStorageGroup { + return fmt.Errorf("object is not of StorageGroup type: %s", typ) + } + + err := sg.Unmarshal(o.Payload()) + if err != nil { + return fmt.Errorf("could not unmarshal object: %w", err) + } + + var expObj uint64 + + for _, attr := range o.Attributes() { + if attr.Key() == objectV2.SysAttributeExpEpoch { + expObj, err = strconv.ParseUint(attr.Value(), 10, 64) + if err != nil { + return fmt.Errorf("could not get expiration from object: %w", err) + } + + break + } + } + + // Supporting deprecated functionality. + // See https://github.com/nspcc-dev/neofs-api/pull/205. + if expSG := sg.ExpirationEpoch(); expObj != expSG { + return fmt.Errorf( + "expiration does not match: from object: %d, from payload: %d", + expObj, expSG) + } + + return nil +} + +// WriteToObject writes StorageGroup to a regular +// Object structure. Object must not contain ambiguous +// information about its expiration epoch or must not +// have it at all. +// +// Written information: +// * expiration epoch; +// * object type (TypeStorageGroup); +// * raw payload. +func WriteToObject(sg StorageGroup, o *objectSDK.Object) { + sgRaw, err := sg.Marshal() + if err != nil { + // Marshal() does not return errors + // in the next API release + panic(fmt.Errorf("could not marshal storage group: %w", err)) + } + + o.SetPayload(sgRaw) + o.SetType(objectSDK.TypeStorageGroup) + + attrs := o.Attributes() + var expAttrFound bool + + for i := range attrs { + if attrs[i].Key() == objectV2.SysAttributeExpEpoch { + expAttrFound = true + attrs[i].SetValue(strconv.FormatUint(sg.ExpirationEpoch(), 10)) + + break + } + } + + if !expAttrFound { + var attr objectSDK.Attribute + + attr.SetKey(objectV2.SysAttributeExpEpoch) + attr.SetValue(strconv.FormatUint(sg.ExpirationEpoch(), 10)) + + attrs = append(attrs, attr) + } + + o.SetAttributes(attrs...) +} diff --git a/storagegroup/storagegroup_test.go b/storagegroup/storagegroup_test.go index f25959d0..8c67c73c 100644 --- a/storagegroup/storagegroup_test.go +++ b/storagegroup/storagegroup_test.go @@ -2,13 +2,16 @@ package storagegroup_test import ( "crypto/sha256" + "strconv" "testing" + objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" "github.com/nspcc-dev/neofs-api-go/v2/refs" storagegroupV2 "github.com/nspcc-dev/neofs-api-go/v2/storagegroup" storagegroupV2test "github.com/nspcc-dev/neofs-api-go/v2/storagegroup/test" "github.com/nspcc-dev/neofs-sdk-go/checksum" checksumtest "github.com/nspcc-dev/neofs-sdk-go/checksum/test" + objectSDK "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" "github.com/nspcc-dev/neofs-sdk-go/storagegroup" @@ -193,3 +196,94 @@ func TestStorageGroup_SetMembers_DoubleSetting(t *testing.T) { // and apply update correctly sg.SetMembers(mm[:1]) } + +func TestStorageGroupFromObject(t *testing.T) { + sg := storagegrouptest.StorageGroup() + + var o objectSDK.Object + + var expAttr objectSDK.Attribute + expAttr.SetKey(objectV2.SysAttributeExpEpoch) + expAttr.SetValue(strconv.FormatUint(sg.ExpirationEpoch(), 10)) + + sgRaw, err := sg.Marshal() + require.NoError(t, err) + + o.SetPayload(sgRaw) + o.SetType(objectSDK.TypeStorageGroup) + + t.Run("correct object", func(t *testing.T) { + o.SetAttributes(objectSDK.Attribute{}, expAttr, objectSDK.Attribute{}) + + var sg2 storagegroup.StorageGroup + require.NoError(t, storagegroup.ReadFromObject(&sg2, o)) + require.Equal(t, sg, sg2) + }) + + t.Run("incorrect exp attr", func(t *testing.T) { + var sg2 storagegroup.StorageGroup + + expAttr.SetValue(strconv.FormatUint(sg.ExpirationEpoch()+1, 10)) + o.SetAttributes(expAttr) + + require.Error(t, storagegroup.ReadFromObject(&sg2, o)) + }) + + t.Run("incorrect object type", func(t *testing.T) { + var sg2 storagegroup.StorageGroup + + o.SetType(objectSDK.TypeTombstone) + require.Error(t, storagegroup.ReadFromObject(&sg2, o)) + }) +} + +func TestStorageGroupToObject(t *testing.T) { + sg := storagegrouptest.StorageGroup() + + sgRaw, err := sg.Marshal() + require.NoError(t, err) + + t.Run("empty object", func(t *testing.T) { + var o objectSDK.Object + storagegroup.WriteToObject(sg, &o) + + exp, found := expFromObj(t, o) + require.True(t, found) + + require.Equal(t, sgRaw, o.Payload()) + require.Equal(t, sg.ExpirationEpoch(), exp) + require.Equal(t, objectSDK.TypeStorageGroup, o.Type()) + }) + + t.Run("obj already has exp attr", func(t *testing.T) { + var o objectSDK.Object + + var attr objectSDK.Attribute + attr.SetKey(objectV2.SysAttributeExpEpoch) + attr.SetValue(strconv.FormatUint(sg.ExpirationEpoch()+1, 10)) + + o.SetAttributes(objectSDK.Attribute{}, attr, objectSDK.Attribute{}) + + storagegroup.WriteToObject(sg, &o) + + exp, found := expFromObj(t, o) + require.True(t, found) + + require.Equal(t, sgRaw, o.Payload()) + require.Equal(t, sg.ExpirationEpoch(), exp) + require.Equal(t, objectSDK.TypeStorageGroup, o.Type()) + }) +} + +func expFromObj(t *testing.T, o objectSDK.Object) (uint64, bool) { + for _, attr := range o.Attributes() { + if attr.Key() == objectV2.SysAttributeExpEpoch { + exp, err := strconv.ParseUint(attr.Value(), 10, 64) + require.NoError(t, err) + + return exp, true + } + } + + return 0, false +}