diff --git a/container/container_internal_test.go b/container/container_internal_test.go index b40c44af..aec3b9c5 100644 --- a/container/container_internal_test.go +++ b/container/container_internal_test.go @@ -20,7 +20,7 @@ func TestContainer_CopyTo(t *testing.T) { attrOne := "a0" attrValue := "a1" - container.SetOwner(*owner) + container.SetOwner(owner) container.SetBasicACL(acl.PublicRWExtended) container.SetAttribute(attrOne, attrValue) @@ -78,7 +78,7 @@ func TestContainer_CopyTo(t *testing.T) { require.True(t, container.Owner().Equals(dst.Owner())) newOwner := usertest.ID(t) - dst.SetOwner(*newOwner) + dst.SetOwner(newOwner) require.False(t, container.Owner().Equals(dst.Owner())) }) diff --git a/session/common.go b/session/common.go index 244d81bd..7636c67e 100644 --- a/session/common.go +++ b/session/common.go @@ -30,6 +30,48 @@ type commonData struct { type contextReader func(session.TokenContext, bool) error +func (x commonData) copyTo(dst *commonData) { + dst.idSet = x.idSet + copy(dst.id[:], x.id[:]) + + dst.issuerSet = x.issuerSet + iss := x.issuer + dst.issuer = iss + + dst.lifetimeSet = x.lifetimeSet + dst.iat = x.iat + dst.nbf = x.nbf + dst.exp = x.exp + + if auth := x.authKey; auth != nil { + dst.authKey = make([]byte, len(x.authKey)) + copy(dst.authKey, x.authKey) + } else { + dst.authKey = nil + } + + dst.sigSet = x.sigSet + if sig := x.sig.GetKey(); sig != nil { + bts := make([]byte, len(sig)) + copy(bts, sig) + + dst.sig.SetKey(bts) + } else { + dst.sig.SetKey(nil) + } + + dst.sig.SetScheme(x.sig.GetScheme()) + + if sign := x.sig.GetSign(); sign != nil { + bts := make([]byte, len(sign)) + copy(bts, sign) + + dst.sig.SetSign(sign) + } else { + dst.sig.SetSign(nil) + } +} + // reads commonData and custom context from the session.Token message. // If checkFieldPresence is set, returns an error on absence of any protocol-required // field. Verifies format of any presented field according to NeoFS API V2 protocol. diff --git a/session/common_test.go b/session/common_test.go new file mode 100644 index 00000000..5da0fd6b --- /dev/null +++ b/session/common_test.go @@ -0,0 +1,234 @@ +package session + +import ( + "bytes" + "testing" + + "github.com/google/uuid" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-api-go/v2/session" + "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/stretchr/testify/require" +) + +func Test_commonData_copyTo(t *testing.T) { + var sig refs.Signature + + sig.SetKey([]byte("key")) + sig.SetSign([]byte("sign")) + sig.SetScheme(refs.ECDSA_SHA512) + + signer := test.RandomSignerRFC6979(t) + + data := commonData{ + idSet: true, + id: uuid.New(), + issuerSet: true, + issuer: signer.UserID(), + lifetimeSet: true, + iat: 1, + nbf: 2, + exp: 3, + authKey: []byte{1, 2, 3, 4}, + sigSet: true, + sig: sig, + } + + t.Run("copy", func(t *testing.T) { + var dst commonData + data.copyTo(&dst) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + + require.Equal(t, data, dst) + require.True(t, bytes.Equal(data.marshal(emptyWriter), dst.marshal(emptyWriter))) + + require.Equal(t, data.issuerSet, dst.issuerSet) + require.Equal(t, data.issuer.String(), dst.issuer.String()) + }) + + t.Run("change id", func(t *testing.T) { + var dst commonData + data.copyTo(&dst) + + require.Equal(t, data.idSet, dst.idSet) + require.Equal(t, data.id.String(), dst.id.String()) + + dst.SetID(uuid.New()) + + require.Equal(t, data.idSet, dst.idSet) + require.NotEqual(t, data.id.String(), dst.id.String()) + }) + + t.Run("overwrite id", func(t *testing.T) { + // id is not set + local := commonData{} + require.False(t, local.idSet) + + // id is set + var dst commonData + dst.SetID(uuid.New()) + require.True(t, dst.idSet) + + // overwrite ID data + local.copyTo(&dst) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + require.True(t, bytes.Equal(local.marshal(emptyWriter), dst.marshal(emptyWriter))) + + require.False(t, local.idSet) + require.False(t, dst.idSet) + + // update id + dst.SetID(uuid.New()) + + // check that affects only dst + require.False(t, local.idSet) + require.True(t, dst.idSet) + }) + + t.Run("change issuer", func(t *testing.T) { + var dst commonData + data.copyTo(&dst) + + require.Equal(t, data.issuerSet, dst.issuerSet) + require.True(t, data.issuer.Equals(dst.issuer)) + + dst.SetIssuer(test.RandomSignerRFC6979(t).UserID()) + + require.Equal(t, data.issuerSet, dst.issuerSet) + require.False(t, data.issuer.Equals(dst.issuer)) + }) + + t.Run("overwrite issuer", func(t *testing.T) { + var local commonData + require.False(t, local.issuerSet) + + var dst commonData + dst.SetIssuer(test.RandomSignerRFC6979(t).UserID()) + require.True(t, dst.issuerSet) + + local.copyTo(&dst) + require.False(t, local.issuerSet) + require.False(t, dst.issuerSet) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + require.True(t, bytes.Equal(local.marshal(emptyWriter), dst.marshal(emptyWriter))) + + require.Equal(t, local.issuerSet, dst.issuerSet) + require.True(t, local.issuer.Equals(dst.issuer)) + + dst.SetIssuer(test.RandomSignerRFC6979(t).UserID()) + require.False(t, local.issuerSet) + require.True(t, dst.issuerSet) + + require.False(t, local.issuer.Equals(dst.issuer)) + }) + + t.Run("change lifetime", func(t *testing.T) { + var dst commonData + data.copyTo(&dst) + + require.Equal(t, data.lifetimeSet, dst.lifetimeSet) + require.Equal(t, data.iat, dst.iat) + require.Equal(t, data.nbf, dst.nbf) + require.Equal(t, data.exp, dst.exp) + + dst.SetExp(100) + dst.SetIat(200) + dst.SetNbf(300) + + require.Equal(t, data.lifetimeSet, dst.lifetimeSet) + require.NotEqual(t, data.iat, dst.iat) + require.NotEqual(t, data.nbf, dst.nbf) + require.NotEqual(t, data.exp, dst.exp) + }) + + t.Run("overwrite lifetime", func(t *testing.T) { + // lifetime is not set + local := commonData{} + require.False(t, local.lifetimeSet) + + // lifetime is set + var dst commonData + dst.SetExp(100) + dst.SetIat(200) + dst.SetNbf(300) + require.True(t, dst.lifetimeSet) + + local.copyTo(&dst) + require.False(t, local.lifetimeSet) + require.False(t, dst.lifetimeSet) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + require.True(t, bytes.Equal(local.marshal(emptyWriter), dst.marshal(emptyWriter))) + + // check both are equal + require.Equal(t, local.lifetimeSet, dst.lifetimeSet) + require.Equal(t, local.iat, dst.iat) + require.Equal(t, local.nbf, dst.nbf) + require.Equal(t, local.exp, dst.exp) + + // update lifetime + dst.SetExp(100) + dst.SetIat(200) + dst.SetNbf(300) + + // check that affects only dst + require.False(t, local.lifetimeSet) + require.True(t, dst.lifetimeSet) + require.NotEqual(t, local.iat, dst.iat) + require.NotEqual(t, local.nbf, dst.nbf) + require.NotEqual(t, local.exp, dst.exp) + }) + + t.Run("change sig", func(t *testing.T) { + var dst commonData + data.copyTo(&dst) + + require.Equal(t, data.sigSet, dst.sigSet) + require.Equal(t, data.sig.GetScheme(), dst.sig.GetScheme()) + require.True(t, bytes.Equal(data.sig.GetKey(), dst.sig.GetKey())) + require.True(t, bytes.Equal(data.sig.GetSign(), dst.sig.GetSign())) + + dst.sig.SetKey([]byte{1, 2, 3}) + dst.sig.SetScheme(100) + dst.sig.SetSign([]byte{10, 11, 12}) + + require.Equal(t, data.issuerSet, dst.issuerSet) + require.NotEqual(t, data.sig.GetScheme(), dst.sig.GetScheme()) + require.False(t, bytes.Equal(data.sig.GetKey(), dst.sig.GetKey())) + require.False(t, bytes.Equal(data.sig.GetSign(), dst.sig.GetSign())) + }) + + t.Run("overwrite sig", func(t *testing.T) { + local := commonData{} + require.False(t, local.sigSet) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + + var dst commonData + require.NoError(t, dst.sign(signer, emptyWriter)) + require.True(t, dst.sigSet) + + local.copyTo(&dst) + require.False(t, local.sigSet) + require.False(t, dst.sigSet) + + require.True(t, bytes.Equal(local.marshal(emptyWriter), dst.marshal(emptyWriter))) + + require.NoError(t, dst.sign(signer, emptyWriter)) + require.False(t, local.sigSet) + require.True(t, dst.sigSet) + }) +} diff --git a/session/container.go b/session/container.go index 37cdd58e..f8ca0cd4 100644 --- a/session/container.go +++ b/session/container.go @@ -30,6 +30,17 @@ type Container struct { cnr cid.ID } +// CopyTo writes deep copy of the [Container] to dst. +func (x Container) CopyTo(dst *Container) { + x.commonData.copyTo(&dst.commonData) + + dst.verb = x.verb + + dst.cnrSet = x.cnrSet + contID := x.cnr + dst.cnr = contID +} + // readContext is a contextReader needed for commonData methods. func (x *Container) readContext(c session.TokenContext, checkFieldPresence bool) error { cCnr, ok := c.(*session.ContainerSessionContext) diff --git a/session/container_internal_test.go b/session/container_internal_test.go new file mode 100644 index 00000000..3cbfbe80 --- /dev/null +++ b/session/container_internal_test.go @@ -0,0 +1,70 @@ +package session + +import ( + "bytes" + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/session" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/stretchr/testify/require" +) + +func TestContainer_CopyTo(t *testing.T) { + var container Container + + containerID := cidtest.ID() + + container.ForVerb(VerbContainerDelete) + container.ApplyOnlyTo(containerID) + + t.Run("copy", func(t *testing.T) { + var dst Container + container.CopyTo(&dst) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + + require.Equal(t, container, dst) + require.True(t, bytes.Equal(container.marshal(emptyWriter), dst.marshal(emptyWriter))) + }) + + t.Run("change", func(t *testing.T) { + var dst Container + container.CopyTo(&dst) + + require.Equal(t, container.verb, dst.verb) + require.True(t, container.cnrSet) + require.True(t, dst.cnrSet) + + container.ForVerb(VerbContainerSetEACL) + + require.NotEqual(t, container.verb, dst.verb) + require.True(t, container.cnrSet) + require.True(t, dst.cnrSet) + }) + + t.Run("overwrite container id", func(t *testing.T) { + var local Container + require.False(t, local.cnrSet) + + var dst Container + dst.ApplyOnlyTo(containerID) + require.True(t, dst.cnrSet) + + local.CopyTo(&dst) + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + + require.Equal(t, local, dst) + require.True(t, bytes.Equal(local.marshal(emptyWriter), dst.marshal(emptyWriter))) + + require.False(t, local.cnrSet) + require.False(t, dst.cnrSet) + + dst.ApplyOnlyTo(containerID) + require.True(t, dst.cnrSet) + require.False(t, local.cnrSet) + }) +} diff --git a/session/object.go b/session/object.go index 08a10120..e5777318 100644 --- a/session/object.go +++ b/session/object.go @@ -32,6 +32,24 @@ type Object struct { objs []oid.ID } +// CopyTo writes deep copy of the [Container] to dst. +func (x Object) CopyTo(dst *Object) { + x.commonData.copyTo(&dst.commonData) + + dst.verb = x.verb + + dst.cnrSet = x.cnrSet + contID := x.cnr + dst.cnr = contID + + if objs := x.objs; objs != nil { + dst.objs = make([]oid.ID, len(x.objs)) + copy(dst.objs, x.objs) + } else { + dst.objs = nil + } +} + func (x *Object) readContext(c session.TokenContext, checkFieldPresence bool) error { cObj, ok := c.(*session.ObjectSessionContext) if !ok || cObj == nil { diff --git a/session/object_internal_test.go b/session/object_internal_test.go new file mode 100644 index 00000000..3bc0c6e4 --- /dev/null +++ b/session/object_internal_test.go @@ -0,0 +1,107 @@ +package session + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/session" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/stretchr/testify/require" +) + +func generateRandomOids(size int) []oid.ID { + checksum := [sha256.Size]byte{} + + result := make([]oid.ID, size) + for i := 0; i < size; i++ { + _, _ = rand.Read(checksum[:]) + + var id oid.ID + id.SetSHA256(checksum) + result[i] = id + } + + return result +} + +func TestObject_CopyTo(t *testing.T) { + var container Object + + containerID := cidtest.ID() + + container.ForVerb(VerbObjectDelete) + container.BindContainer(containerID) + container.LimitByObjects(generateRandomOids(2)...) + + t.Run("copy", func(t *testing.T) { + var dst Object + container.CopyTo(&dst) + + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + + require.Equal(t, container, dst) + require.True(t, bytes.Equal(container.marshal(emptyWriter), dst.marshal(emptyWriter))) + }) + + t.Run("change simple fields", func(t *testing.T) { + var dst Object + container.CopyTo(&dst) + + require.Equal(t, container.verb, dst.verb) + require.True(t, container.cnrSet) + require.True(t, dst.cnrSet) + + container.ForVerb(VerbObjectHead) + + require.NotEqual(t, container.verb, dst.verb) + require.True(t, container.cnrSet) + require.True(t, dst.cnrSet) + }) + + t.Run("change ids", func(t *testing.T) { + var dst Object + container.CopyTo(&dst) + + for i := range container.objs { + require.True(t, container.objs[i].Equals(dst.objs[i])) + + // change object id in the new object + for j := range dst.objs[i] { + dst.objs[i][j] = byte(j) + } + } + + for i := range container.objs { + require.False(t, container.objs[i].Equals(dst.objs[i])) + } + }) + + t.Run("overwrite container id", func(t *testing.T) { + var local Object + require.False(t, local.cnrSet) + + var dst Object + dst.BindContainer(containerID) + require.True(t, dst.cnrSet) + + local.CopyTo(&dst) + emptyWriter := func() session.TokenContext { + return &session.ContainerSessionContext{} + } + + require.Equal(t, local, dst) + require.True(t, bytes.Equal(local.marshal(emptyWriter), dst.marshal(emptyWriter))) + + require.False(t, local.cnrSet) + require.False(t, dst.cnrSet) + + dst.BindContainer(containerID) + require.True(t, dst.cnrSet) + require.False(t, local.cnrSet) + }) +}