diff --git a/object/address.go b/object/address.go new file mode 100644 index 00000000..0cba83bc --- /dev/null +++ b/object/address.go @@ -0,0 +1,117 @@ +package object + +import ( + "errors" + "strings" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" +) + +// Address represents v2-compatible object address. +type Address refs.Address + +var errInvalidAddressString = errors.New("incorrect format of the string object address") + +const ( + addressParts = 2 + addressSeparator = "/" +) + +// NewAddressFromV2 converts v2 Address message to Address. +// +// Nil refs.Address converts to nil. +func NewAddressFromV2(aV2 *refs.Address) *Address { + return (*Address)(aV2) +} + +// NewAddress creates and initializes blank Address. +// +// Works similar as NewAddressFromV2(new(Address)). +// +// Defaults: +// - cid: nil; +// - oid: nil. +func NewAddress() *Address { + return NewAddressFromV2(new(refs.Address)) +} + +// ToV2 converts Address to v2 Address message. +// +// Nil Address converts to nil. +func (a *Address) ToV2() *refs.Address { + return (*refs.Address)(a) +} + +// ContainerID returns container identifier. +func (a *Address) ContainerID() *cid.ID { + return cid.NewFromV2( + (*refs.Address)(a).GetContainerID()) +} + +// SetContainerID sets container identifier. +func (a *Address) SetContainerID(id *cid.ID) { + (*refs.Address)(a).SetContainerID(id.ToV2()) +} + +// ObjectID returns object identifier. +func (a *Address) ObjectID() *ID { + return NewIDFromV2( + (*refs.Address)(a).GetObjectID()) +} + +// SetObjectID sets object identifier. +func (a *Address) SetObjectID(id *ID) { + (*refs.Address)(a).SetObjectID(id.ToV2()) +} + +// Parse converts base58 string representation into Address. +func (a *Address) Parse(s string) error { + var ( + err error + oid = NewID() + id = cid.New() + parts = strings.Split(s, addressSeparator) + ) + + if len(parts) != addressParts { + return errInvalidAddressString + } else if err = id.Parse(parts[0]); err != nil { + return err + } else if err = oid.Parse(parts[1]); err != nil { + return err + } + + a.SetObjectID(oid) + a.SetContainerID(id) + + return nil +} + +// String returns string representation of Object.Address. +func (a *Address) String() string { + return strings.Join([]string{ + a.ContainerID().String(), + a.ObjectID().String(), + }, addressSeparator) +} + +// Marshal marshals Address into a protobuf binary form. +func (a *Address) Marshal() ([]byte, error) { + return (*refs.Address)(a).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of Address. +func (a *Address) Unmarshal(data []byte) error { + return (*refs.Address)(a).Unmarshal(data) +} + +// MarshalJSON encodes Address to protobuf JSON format. +func (a *Address) MarshalJSON() ([]byte, error) { + return (*refs.Address)(a).MarshalJSON() +} + +// UnmarshalJSON decodes Address from protobuf JSON format. +func (a *Address) UnmarshalJSON(data []byte) error { + return (*refs.Address)(a).UnmarshalJSON(data) +} diff --git a/object/address_test.go b/object/address_test.go new file mode 100644 index 00000000..5dd374d4 --- /dev/null +++ b/object/address_test.go @@ -0,0 +1,119 @@ +package object + +import ( + "strings" + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/stretchr/testify/require" +) + +func TestAddress_SetContainerID(t *testing.T) { + a := NewAddress() + + id := cidtest.GenerateID() + + a.SetContainerID(id) + + require.Equal(t, id, a.ContainerID()) +} + +func TestAddress_SetObjectID(t *testing.T) { + a := NewAddress() + + oid := randID(t) + + a.SetObjectID(oid) + + require.Equal(t, oid, a.ObjectID()) +} + +func TestAddress_Parse(t *testing.T) { + cid := cidtest.GenerateID() + + oid := NewID() + oid.SetSHA256(randSHA256Checksum(t)) + + t.Run("should parse successful", func(t *testing.T) { + s := strings.Join([]string{cid.String(), oid.String()}, addressSeparator) + a := NewAddress() + + require.NoError(t, a.Parse(s)) + require.Equal(t, oid, a.ObjectID()) + require.Equal(t, cid, a.ContainerID()) + }) + + t.Run("should fail for bad address", func(t *testing.T) { + s := strings.Join([]string{cid.String()}, addressSeparator) + require.EqualError(t, NewAddress().Parse(s), errInvalidAddressString.Error()) + }) + + t.Run("should fail on container.ID", func(t *testing.T) { + s := strings.Join([]string{"1", "2"}, addressSeparator) + require.Error(t, NewAddress().Parse(s)) + }) + + t.Run("should fail on object.ID", func(t *testing.T) { + s := strings.Join([]string{cid.String(), "2"}, addressSeparator) + require.Error(t, NewAddress().Parse(s)) + }) +} + +func TestAddressEncoding(t *testing.T) { + a := NewAddress() + a.SetObjectID(randID(t)) + a.SetContainerID(cidtest.GenerateID()) + + t.Run("binary", func(t *testing.T) { + data, err := a.Marshal() + require.NoError(t, err) + + a2 := NewAddress() + require.NoError(t, a2.Unmarshal(data)) + + require.Equal(t, a, a2) + }) + + t.Run("json", func(t *testing.T) { + data, err := a.MarshalJSON() + require.NoError(t, err) + + a2 := NewAddress() + require.NoError(t, a2.UnmarshalJSON(data)) + + require.Equal(t, a, a2) + }) +} + +func TestNewAddressFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *refs.Address + + require.Nil(t, NewAddressFromV2(x)) + }) +} + +func TestAddress_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Address + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewAddress(t *testing.T) { + t.Run("default values", func(t *testing.T) { + a := NewAddress() + + // check initial values + require.Nil(t, a.ContainerID()) + require.Nil(t, a.ObjectID()) + + // convert to v2 message + aV2 := a.ToV2() + + require.Nil(t, aV2.GetContainerID()) + require.Nil(t, aV2.GetObjectID()) + }) +} diff --git a/object/attribute.go b/object/attribute.go new file mode 100644 index 00000000..cc1ac42e --- /dev/null +++ b/object/attribute.go @@ -0,0 +1,73 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" +) + +// Attribute represents v2-compatible object attribute. +type Attribute object.Attribute + +// NewAttributeFromV2 wraps v2 Attribute message to Attribute. +// +// Nil object.Attribute converts to nil. +func NewAttributeFromV2(aV2 *object.Attribute) *Attribute { + return (*Attribute)(aV2) +} + +// NewAttribute creates and initializes blank Attribute. +// +// Works similar as NewAttributeFromV2(new(Attribute)). +// +// Defaults: +// - key: ""; +// - value: "". +func NewAttribute() *Attribute { + return NewAttributeFromV2(new(object.Attribute)) +} + +// Key returns key to the object attribute. +func (a *Attribute) Key() string { + return (*object.Attribute)(a).GetKey() +} + +// SetKey sets key to the object attribute. +func (a *Attribute) SetKey(v string) { + (*object.Attribute)(a).SetKey(v) +} + +// Value return value of the object attribute. +func (a *Attribute) Value() string { + return (*object.Attribute)(a).GetValue() +} + +// SetValue sets value of the object attribute. +func (a *Attribute) SetValue(v string) { + (*object.Attribute)(a).SetValue(v) +} + +// ToV2 converts Attribute to v2 Attribute message. +// +// Nil Attribute converts to nil. +func (a *Attribute) ToV2() *object.Attribute { + return (*object.Attribute)(a) +} + +// Marshal marshals Attribute into a protobuf binary form. +func (a *Attribute) Marshal() ([]byte, error) { + return (*object.Attribute)(a).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of Attribute. +func (a *Attribute) Unmarshal(data []byte) error { + return (*object.Attribute)(a).Unmarshal(data) +} + +// MarshalJSON encodes Attribute to protobuf JSON format. +func (a *Attribute) MarshalJSON() ([]byte, error) { + return (*object.Attribute)(a).MarshalJSON() +} + +// UnmarshalJSON decodes Attribute from protobuf JSON format. +func (a *Attribute) UnmarshalJSON(data []byte) error { + return (*object.Attribute)(a).UnmarshalJSON(data) +} diff --git a/object/attribute_test.go b/object/attribute_test.go new file mode 100644 index 00000000..d5f1b3f9 --- /dev/null +++ b/object/attribute_test.go @@ -0,0 +1,82 @@ +package object + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/stretchr/testify/require" +) + +func TestAttribute(t *testing.T) { + key, val := "some key", "some value" + + a := NewAttribute() + a.SetKey(key) + a.SetValue(val) + + require.Equal(t, key, a.Key()) + require.Equal(t, val, a.Value()) + + aV2 := a.ToV2() + + require.Equal(t, key, aV2.GetKey()) + require.Equal(t, val, aV2.GetValue()) +} + +func TestAttributeEncoding(t *testing.T) { + a := NewAttribute() + a.SetKey("key") + a.SetValue("value") + + t.Run("binary", func(t *testing.T) { + data, err := a.Marshal() + require.NoError(t, err) + + a2 := NewAttribute() + require.NoError(t, a2.Unmarshal(data)) + + require.Equal(t, a, a2) + }) + + t.Run("json", func(t *testing.T) { + data, err := a.MarshalJSON() + require.NoError(t, err) + + a2 := NewAttribute() + require.NoError(t, a2.UnmarshalJSON(data)) + + require.Equal(t, a, a2) + }) +} + +func TestNewAttributeFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *object.Attribute + + require.Nil(t, NewAttributeFromV2(x)) + }) +} + +func TestAttribute_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Attribute + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewAttribute(t *testing.T) { + t.Run("default values", func(t *testing.T) { + a := NewAttribute() + + // check initial values + require.Empty(t, a.Key()) + require.Empty(t, a.Value()) + + // convert to v2 message + aV2 := a.ToV2() + + require.Empty(t, aV2.GetKey()) + require.Empty(t, aV2.GetValue()) + }) +} diff --git a/object/error.go b/object/error.go new file mode 100644 index 00000000..96048c28 --- /dev/null +++ b/object/error.go @@ -0,0 +1,19 @@ +package object + +type SplitInfoError struct { + si *SplitInfo +} + +const splitInfoErrorMsg = "object not found, split info has been provided" + +func (s *SplitInfoError) Error() string { + return splitInfoErrorMsg +} + +func (s *SplitInfoError) SplitInfo() *SplitInfo { + return s.si +} + +func NewSplitInfoError(v *SplitInfo) *SplitInfoError { + return &SplitInfoError{si: v} +} diff --git a/object/error_test.go b/object/error_test.go new file mode 100644 index 00000000..22189a88 --- /dev/null +++ b/object/error_test.go @@ -0,0 +1,33 @@ +package object_test + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/stretchr/testify/require" +) + +func TestNewSplitInfoError(t *testing.T) { + var ( + si = generateSplitInfo() + + err error = object.NewSplitInfoError(si) + expectedErr *object.SplitInfoError + ) + + require.True(t, errors.As(err, &expectedErr)) + + siErr, ok := err.(*object.SplitInfoError) + require.True(t, ok) + require.Equal(t, si, siErr.SplitInfo()) +} + +func generateSplitInfo() *object.SplitInfo { + si := object.NewSplitInfo() + si.SetSplitID(object.NewSplitID()) + si.SetLastPart(generateID()) + si.SetLink(generateID()) + + return si +} diff --git a/object/fmt.go b/object/fmt.go new file mode 100644 index 00000000..3eb5d5f0 --- /dev/null +++ b/object/fmt.go @@ -0,0 +1,175 @@ +package object + +import ( + "crypto/ecdsa" + "crypto/sha256" + "errors" + "fmt" + + signatureV2 "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + "github.com/nspcc-dev/neofs-sdk-go/signature" + sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature" +) + +var errCheckSumMismatch = errors.New("payload checksum mismatch") + +var errIncorrectID = errors.New("incorrect object identifier") + +// CalculatePayloadChecksum calculates and returns checksum of +// object payload bytes. +func CalculatePayloadChecksum(payload []byte) *checksum.Checksum { + res := checksum.New() + res.SetSHA256(sha256.Sum256(payload)) + + return res +} + +// CalculateAndSetPayloadChecksum calculates checksum of current +// object payload and writes it to the object. +func CalculateAndSetPayloadChecksum(obj *RawObject) { + obj.SetPayloadChecksum( + CalculatePayloadChecksum(obj.Payload()), + ) +} + +// VerifyPayloadChecksum checks if payload checksum in the object +// corresponds to its payload. +func VerifyPayloadChecksum(obj *Object) error { + actual := CalculatePayloadChecksum(obj.Payload()) + if !checksum.Equal(obj.PayloadChecksum(), actual) { + return errCheckSumMismatch + } + + return nil +} + +// CalculateID calculates identifier for the object. +func CalculateID(obj *Object) (*ID, error) { + data, err := obj.ToV2().GetHeader().StableMarshal(nil) + if err != nil { + return nil, err + } + + id := NewID() + id.SetSHA256(sha256.Sum256(data)) + + return id, nil +} + +// CalculateAndSetID calculates identifier for the object +// and writes the result to it. +func CalculateAndSetID(obj *RawObject) error { + id, err := CalculateID(obj.Object()) + if err != nil { + return err + } + + obj.SetID(id) + + return nil +} + +// VerifyID checks if identifier in the object corresponds to +// its structure. +func VerifyID(obj *Object) error { + id, err := CalculateID(obj) + if err != nil { + return err + } + + if !id.Equal(obj.ID()) { + return errIncorrectID + } + + return nil +} + +func CalculateIDSignature(key *ecdsa.PrivateKey, id *ID) (*signature.Signature, error) { + sig := signature.New() + + if err := sigutil.SignDataWithHandler( + key, + signatureV2.StableMarshalerWrapper{ + SM: id.ToV2(), + }, + func(key, sign []byte) { + sig.SetKey(key) + sig.SetSign(sign) + }, + ); err != nil { + return nil, err + } + + return sig, nil +} + +func CalculateAndSetSignature(key *ecdsa.PrivateKey, obj *RawObject) error { + sig, err := CalculateIDSignature(key, obj.ID()) + if err != nil { + return err + } + + obj.SetSignature(sig) + + return nil +} + +func VerifyIDSignature(obj *Object) error { + return sigutil.VerifyDataWithSource( + signatureV2.StableMarshalerWrapper{ + SM: obj.ID().ToV2(), + }, + func() ([]byte, []byte) { + sig := obj.Signature() + + return sig.Key(), sig.Sign() + }, + ) +} + +// SetIDWithSignature sets object identifier and signature. +func SetIDWithSignature(key *ecdsa.PrivateKey, obj *RawObject) error { + if err := CalculateAndSetID(obj); err != nil { + return fmt.Errorf("could not set identifier: %w", err) + } + + if err := CalculateAndSetSignature(key, obj); err != nil { + return fmt.Errorf("could not set signature: %w", err) + } + + return nil +} + +// SetVerificationFields calculates and sets all verification fields of the object. +func SetVerificationFields(key *ecdsa.PrivateKey, obj *RawObject) error { + CalculateAndSetPayloadChecksum(obj) + + return SetIDWithSignature(key, obj) +} + +// CheckVerificationFields checks all verification fields of the object. +func CheckVerificationFields(obj *Object) error { + if err := CheckHeaderVerificationFields(obj); err != nil { + return fmt.Errorf("invalid header structure: %w", err) + } + + if err := VerifyPayloadChecksum(obj); err != nil { + return fmt.Errorf("invalid payload checksum: %w", err) + } + + return nil +} + +// CheckHeaderVerificationFields checks all verification fields except payload. +func CheckHeaderVerificationFields(obj *Object) error { + if err := VerifyIDSignature(obj); err != nil { + return fmt.Errorf("invalid signature: %w", err) + } + + if err := VerifyID(obj); err != nil { + return fmt.Errorf("invalid identifier: %w", err) + } + + return nil +} diff --git a/object/fmt_test.go b/object/fmt_test.go new file mode 100644 index 00000000..6e4bab7a --- /dev/null +++ b/object/fmt_test.go @@ -0,0 +1,81 @@ +package object + +import ( + "crypto/rand" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func TestVerificationFields(t *testing.T) { + obj := NewRaw() + + payload := make([]byte, 10) + _, _ = rand.Read(payload) + + obj.SetPayload(payload) + obj.SetPayloadSize(uint64(len(payload))) + + p, err := keys.NewPrivateKey() + require.NoError(t, err) + require.NoError(t, SetVerificationFields(&p.PrivateKey, obj)) + + require.NoError(t, CheckVerificationFields(obj.Object())) + + items := []struct { + corrupt func() + restore func() + }{ + { + corrupt: func() { + payload[0]++ + }, + restore: func() { + payload[0]-- + }, + }, + { + corrupt: func() { + obj.SetPayloadSize(obj.PayloadSize() + 1) + }, + restore: func() { + obj.SetPayloadSize(obj.PayloadSize() - 1) + }, + }, + { + corrupt: func() { + obj.ID().ToV2().GetValue()[0]++ + }, + restore: func() { + obj.ID().ToV2().GetValue()[0]-- + }, + }, + { + corrupt: func() { + obj.Signature().Key()[0]++ + }, + restore: func() { + obj.Signature().Key()[0]-- + }, + }, + { + corrupt: func() { + obj.Signature().Sign()[0]++ + }, + restore: func() { + obj.Signature().Sign()[0]-- + }, + }, + } + + for _, item := range items { + item.corrupt() + + require.Error(t, CheckVerificationFields(obj.Object())) + + item.restore() + + require.NoError(t, CheckVerificationFields(obj.Object())) + } +} diff --git a/object/id.go b/object/id.go new file mode 100644 index 00000000..f5ecdb85 --- /dev/null +++ b/object/id.go @@ -0,0 +1,92 @@ +package object + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-api-go/v2/refs" +) + +// ID represents v2-compatible object identifier. +type ID refs.ObjectID + +var errInvalidIDString = errors.New("incorrect format of the string object ID") + +// NewIDFromV2 wraps v2 ObjectID message to ID. +// +// Nil refs.ObjectID converts to nil. +func NewIDFromV2(idV2 *refs.ObjectID) *ID { + return (*ID)(idV2) +} + +// NewID creates and initializes blank ID. +// +// Works similar as NewIDFromV2(new(ObjectID)). +// +// Defaults: +// - value: nil. +func NewID() *ID { + return NewIDFromV2(new(refs.ObjectID)) +} + +// SetSHA256 sets object identifier value to SHA256 checksum. +func (id *ID) SetSHA256(v [sha256.Size]byte) { + (*refs.ObjectID)(id).SetValue(v[:]) +} + +// Equal returns true if identifiers are identical. +func (id *ID) Equal(id2 *ID) bool { + return bytes.Equal( + (*refs.ObjectID)(id).GetValue(), + (*refs.ObjectID)(id2).GetValue(), + ) +} + +// ToV2 converts ID to v2 ObjectID message. +// +// Nil ID converts to nil. +func (id *ID) ToV2() *refs.ObjectID { + return (*refs.ObjectID)(id) +} + +// Parse converts base58 string representation into ID. +func (id *ID) Parse(s string) error { + data, err := base58.Decode(s) + if err != nil { + return fmt.Errorf("could not parse object.ID from string: %w", err) + } else if len(data) != sha256.Size { + return errInvalidIDString + } + + (*refs.ObjectID)(id).SetValue(data) + + return nil +} + +// String returns base58 string representation of ID. +func (id *ID) String() string { + return base58.Encode((*refs.ObjectID)(id).GetValue()) +} + +// Marshal marshals ID into a protobuf binary form. +func (id *ID) Marshal() ([]byte, error) { + return (*refs.ObjectID)(id).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of ID. +func (id *ID) Unmarshal(data []byte) error { + return (*refs.ObjectID)(id).Unmarshal(data) +} + +// MarshalJSON encodes ID to protobuf JSON format. +func (id *ID) MarshalJSON() ([]byte, error) { + return (*refs.ObjectID)(id).MarshalJSON() +} + +// UnmarshalJSON decodes ID from protobuf JSON format. +func (id *ID) UnmarshalJSON(data []byte) error { + return (*refs.ObjectID)(id).UnmarshalJSON(data) +} diff --git a/object/id_test.go b/object/id_test.go new file mode 100644 index 00000000..e4086d82 --- /dev/null +++ b/object/id_test.go @@ -0,0 +1,142 @@ +package object + +import ( + "crypto/rand" + "crypto/sha256" + "strconv" + "testing" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/stretchr/testify/require" +) + +func TestIDV2(t *testing.T) { + id := NewID() + + checksum := [sha256.Size]byte{} + + _, err := rand.Read(checksum[:]) + require.NoError(t, err) + + id.SetSHA256(checksum) + + idV2 := id.ToV2() + + require.Equal(t, checksum[:], idV2.GetValue()) +} + +func TestID_Equal(t *testing.T) { + cs := randSHA256Checksum(t) + + id1 := NewID() + id1.SetSHA256(cs) + + id2 := NewID() + id2.SetSHA256(cs) + + id3 := NewID() + id3.SetSHA256(randSHA256Checksum(t)) + + require.True(t, id1.Equal(id2)) + require.False(t, id1.Equal(id3)) +} + +func TestID_Parse(t *testing.T) { + t.Run("should parse successful", func(t *testing.T) { + for i := 0; i < 10; i++ { + t.Run(strconv.Itoa(i), func(t *testing.T) { + cs := randSHA256Checksum(t) + str := base58.Encode(cs[:]) + oid := NewID() + + require.NoError(t, oid.Parse(str)) + require.Equal(t, cs[:], oid.ToV2().GetValue()) + }) + } + }) + + t.Run("should failure on parse", func(t *testing.T) { + for i := 0; i < 10; i++ { + j := i + t.Run(strconv.Itoa(j), func(t *testing.T) { + cs := []byte{1, 2, 3, 4, 5, byte(j)} + str := base58.Encode(cs) + oid := NewID() + + require.Error(t, oid.Parse(str)) + }) + } + }) +} + +func TestID_String(t *testing.T) { + t.Run("nil", func(t *testing.T) { + id := NewID() + require.Empty(t, id.String()) + }) + + t.Run("should be equal", func(t *testing.T) { + for i := 0; i < 10; i++ { + t.Run(strconv.Itoa(i), func(t *testing.T) { + cs := randSHA256Checksum(t) + str := base58.Encode(cs[:]) + oid := NewID() + + require.NoError(t, oid.Parse(str)) + require.Equal(t, str, oid.String()) + }) + } + }) +} + +func TestObjectIDEncoding(t *testing.T) { + id := randID(t) + + t.Run("binary", func(t *testing.T) { + data, err := id.Marshal() + require.NoError(t, err) + + id2 := NewID() + require.NoError(t, id2.Unmarshal(data)) + + require.Equal(t, id, id2) + }) + + t.Run("json", func(t *testing.T) { + data, err := id.MarshalJSON() + require.NoError(t, err) + + a2 := NewID() + require.NoError(t, a2.UnmarshalJSON(data)) + + require.Equal(t, id, a2) + }) +} + +func TestNewIDFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *refs.ObjectID + + require.Nil(t, NewIDFromV2(x)) + }) +} + +func TestID_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *ID + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewID(t *testing.T) { + t.Run("default values", func(t *testing.T) { + id := NewID() + + // convert to v2 message + idV2 := id.ToV2() + + require.Nil(t, idV2.GetValue()) + }) +} diff --git a/object/object.go b/object/object.go new file mode 100644 index 00000000..55000dcc --- /dev/null +++ b/object/object.go @@ -0,0 +1,45 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" +) + +// Object represents v2-compatible NeoFS object that provides +// a convenient interface for working in isolation +// from the internal structure of an object. +// +// Object allows to work with the object in read-only +// mode as a reflection of the immutability of objects +// in the system. +type Object struct { + *rwObject +} + +// NewFromV2 wraps v2 Object message to Object. +func NewFromV2(oV2 *object.Object) *Object { + return &Object{ + rwObject: (*rwObject)(oV2), + } +} + +// New creates and initializes blank Object. +// +// Works similar as NewFromV2(new(Object)). +func New() *Object { + return NewFromV2(new(object.Object)) +} + +// ToV2 converts Object to v2 Object message. +func (o *Object) ToV2() *object.Object { + if o != nil { + return (*object.Object)(o.rwObject) + } + + return nil +} + +// MarshalHeaderJSON marshals object's header +// into JSON format. +func (o *Object) MarshalHeaderJSON() ([]byte, error) { + return (*object.Object)(o.rwObject).GetHeader().MarshalJSON() +} diff --git a/object/range.go b/object/range.go new file mode 100644 index 00000000..1c50ca13 --- /dev/null +++ b/object/range.go @@ -0,0 +1,51 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" +) + +// Range represents v2-compatible object payload range. +type Range object.Range + +// NewRangeFromV2 wraps v2 Range message to Range. +// +// Nil object.Range converts to nil. +func NewRangeFromV2(rV2 *object.Range) *Range { + return (*Range)(rV2) +} + +// NewRange creates and initializes blank Range. +// +// Defaults: +// - offset: 0; +// - length: 0. +func NewRange() *Range { + return NewRangeFromV2(new(object.Range)) +} + +// ToV2 converts Range to v2 Range message. +// +// Nil Range converts to nil. +func (r *Range) ToV2() *object.Range { + return (*object.Range)(r) +} + +// GetLength returns payload range size. +func (r *Range) GetLength() uint64 { + return (*object.Range)(r).GetLength() +} + +// SetLength sets payload range size. +func (r *Range) SetLength(v uint64) { + (*object.Range)(r).SetLength(v) +} + +// GetOffset sets payload range offset from start. +func (r *Range) GetOffset() uint64 { + return (*object.Range)(r).GetOffset() +} + +// SetOffset gets payload range offset from start. +func (r *Range) SetOffset(v uint64) { + (*object.Range)(r).SetOffset(v) +} diff --git a/object/range_test.go b/object/range_test.go new file mode 100644 index 00000000..e14d3600 --- /dev/null +++ b/object/range_test.go @@ -0,0 +1,58 @@ +package object + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/stretchr/testify/require" +) + +func TestRange_SetOffset(t *testing.T) { + r := NewRange() + + off := uint64(13) + r.SetOffset(off) + + require.Equal(t, off, r.GetOffset()) +} + +func TestRange_SetLength(t *testing.T) { + r := NewRange() + + ln := uint64(7) + r.SetLength(ln) + + require.Equal(t, ln, r.GetLength()) +} + +func TestNewRangeFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *object.Range + + require.Nil(t, NewRangeFromV2(x)) + }) +} + +func TestRange_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Range + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewRange(t *testing.T) { + t.Run("default values", func(t *testing.T) { + r := NewRange() + + // check initial values + require.Zero(t, r.GetLength()) + require.Zero(t, r.GetOffset()) + + // convert to v2 message + rV2 := r.ToV2() + + require.Zero(t, rV2.GetLength()) + require.Zero(t, rV2.GetOffset()) + }) +} diff --git a/object/raw.go b/object/raw.go new file mode 100644 index 00000000..0bf79170 --- /dev/null +++ b/object/raw.go @@ -0,0 +1,162 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/session" + "github.com/nspcc-dev/neofs-sdk-go/signature" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// RawObject represents v2-compatible NeoFS object that provides +// a convenient interface to fill in the fields of +// an object in isolation from its internal structure. +type RawObject struct { + *rwObject +} + +// NewRawFromV2 wraps v2 Object message to RawObject. +func NewRawFromV2(oV2 *object.Object) *RawObject { + return &RawObject{ + rwObject: (*rwObject)(oV2), + } +} + +// NewRawFrom wraps Object instance to RawObject. +func NewRawFrom(obj *Object) *RawObject { + return NewRawFromV2(obj.ToV2()) +} + +// NewRaw creates and initializes blank RawObject. +// +// Works similar as NewRawFromV2(new(Object)). +func NewRaw() *RawObject { + return NewRawFromV2(new(object.Object)) +} + +// Object returns read-only object instance. +func (o *RawObject) Object() *Object { + if o != nil { + return &Object{ + rwObject: o.rwObject, + } + } + + return nil +} + +// SetID sets object identifier. +func (o *RawObject) SetID(v *ID) { + o.setID(v) +} + +// SetSignature sets signature of the object identifier. +func (o *RawObject) SetSignature(v *signature.Signature) { + o.setSignature(v) +} + +// SetPayload sets payload bytes. +func (o *RawObject) SetPayload(v []byte) { + o.setPayload(v) +} + +// SetVersion sets version of the object. +func (o *RawObject) SetVersion(v *version.Version) { + o.setVersion(v) +} + +// SetPayloadSize sets payload length of the object. +func (o *RawObject) SetPayloadSize(v uint64) { + o.setPayloadSize(v) +} + +// SetContainerID sets identifier of the related container. +func (o *RawObject) SetContainerID(v *cid.ID) { + o.setContainerID(v) +} + +// SetOwnerID sets identifier of the object owner. +func (o *RawObject) SetOwnerID(v *owner.ID) { + o.setOwnerID(v) +} + +// SetCreationEpoch sets epoch number in which object was created. +func (o *RawObject) SetCreationEpoch(v uint64) { + o.setCreationEpoch(v) +} + +// SetPayloadChecksum sets checksum of the object payload. +func (o *RawObject) SetPayloadChecksum(v *checksum.Checksum) { + o.setPayloadChecksum(v) +} + +// SetPayloadHomomorphicHash sets homomorphic hash of the object payload. +func (o *RawObject) SetPayloadHomomorphicHash(v *checksum.Checksum) { + o.setPayloadHomomorphicHash(v) +} + +// SetAttributes sets object attributes. +func (o *RawObject) SetAttributes(v ...*Attribute) { + o.setAttributes(v...) +} + +// SetPreviousID sets identifier of the previous sibling object. +func (o *RawObject) SetPreviousID(v *ID) { + o.setPreviousID(v) +} + +// SetChildren sets list of the identifiers of the child objects. +func (o *RawObject) SetChildren(v ...*ID) { + o.setChildren(v...) +} + +// SetSplitID sets split identifier for the split object. +func (o *RawObject) SetSplitID(id *SplitID) { + o.setSplitID(id) +} + +// SetParentID sets identifier of the parent object. +func (o *RawObject) SetParentID(v *ID) { + o.setParentID(v) +} + +// SetParent sets parent object w/o payload. +func (o *RawObject) SetParent(v *Object) { + o.setParent(v) +} + +// SetSessionToken sets token of the session +// within which object was created. +func (o *RawObject) SetSessionToken(v *session.Token) { + o.setSessionToken(v) +} + +// SetType sets type of the object. +func (o *RawObject) SetType(v Type) { + o.setType(v) +} + +// CutPayload returns RawObject w/ empty payload. +// +// Changes of non-payload fields affect source object. +func (o *RawObject) CutPayload() *RawObject { + if o != nil { + return &RawObject{ + rwObject: o.rwObject.cutPayload(), + } + } + + return nil +} + +// ResetRelations removes all fields of links with other objects. +func (o *RawObject) ResetRelations() { + o.resetRelations() +} + +// InitRelations initializes relation field. +func (o *RawObject) InitRelations() { + o.initRelations() +} diff --git a/object/raw_test.go b/object/raw_test.go new file mode 100644 index 00000000..13cdd7e4 --- /dev/null +++ b/object/raw_test.go @@ -0,0 +1,319 @@ +package object + +import ( + "crypto/rand" + "crypto/sha256" + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/signature" + "github.com/nspcc-dev/neofs-sdk-go/version" + "github.com/stretchr/testify/require" +) + +func randID(t *testing.T) *ID { + id := NewID() + id.SetSHA256(randSHA256Checksum(t)) + + return id +} + +func randSHA256Checksum(t *testing.T) (cs [sha256.Size]byte) { + _, err := rand.Read(cs[:]) + require.NoError(t, err) + + return +} + +func randTZChecksum(t *testing.T) (cs [64]byte) { + _, err := rand.Read(cs[:]) + require.NoError(t, err) + + return +} + +func TestRawObject_SetID(t *testing.T) { + obj := NewRaw() + + id := randID(t) + + obj.SetID(id) + + require.Equal(t, id, obj.ID()) +} + +func TestRawObject_SetSignature(t *testing.T) { + obj := NewRaw() + + sig := signature.New() + sig.SetKey([]byte{1, 2, 3}) + sig.SetSign([]byte{4, 5, 6}) + + obj.SetSignature(sig) + + require.Equal(t, sig, obj.Signature()) +} + +func TestRawObject_SetPayload(t *testing.T) { + obj := NewRaw() + + payload := make([]byte, 10) + _, _ = rand.Read(payload) + + obj.SetPayload(payload) + + require.Equal(t, payload, obj.Payload()) +} + +func TestRawObject_SetVersion(t *testing.T) { + obj := NewRaw() + + ver := version.New() + ver.SetMajor(1) + ver.SetMinor(2) + + obj.SetVersion(ver) + + require.Equal(t, ver, obj.Version()) +} + +func TestRawObject_SetPayloadSize(t *testing.T) { + obj := NewRaw() + + sz := uint64(133) + obj.SetPayloadSize(sz) + + require.Equal(t, sz, obj.PayloadSize()) +} + +func TestRawObject_SetContainerID(t *testing.T) { + obj := NewRaw() + + cid := cidtest.GenerateID() + + obj.SetContainerID(cid) + + require.Equal(t, cid, obj.ContainerID()) +} + +func TestRawObject_SetOwnerID(t *testing.T) { + obj := NewRaw() + + ownerID := ownertest.GenerateID() + + obj.SetOwnerID(ownerID) + + require.Equal(t, ownerID, obj.OwnerID()) +} + +func TestRawObject_SetCreationEpoch(t *testing.T) { + obj := NewRaw() + + creat := uint64(228) + obj.setCreationEpoch(creat) + + require.Equal(t, creat, obj.CreationEpoch()) +} + +func TestRawObject_SetPayloadChecksum(t *testing.T) { + obj := NewRaw() + + cs := checksum.New() + cs.SetSHA256(randSHA256Checksum(t)) + + obj.SetPayloadChecksum(cs) + + require.Equal(t, cs, obj.PayloadChecksum()) +} + +func TestRawObject_SetPayloadHomomorphicHash(t *testing.T) { + obj := NewRaw() + + cs := checksum.New() + cs.SetTillichZemor(randTZChecksum(t)) + + obj.SetPayloadHomomorphicHash(cs) + + require.Equal(t, cs, obj.PayloadHomomorphicHash()) +} + +func TestRawObject_SetAttributes(t *testing.T) { + obj := NewRaw() + + a1 := NewAttribute() + a1.SetKey("key1") + a1.SetValue("val1") + + a2 := NewAttribute() + a2.SetKey("key2") + a2.SetValue("val2") + + obj.SetAttributes(a1, a2) + + require.Equal(t, []*Attribute{a1, a2}, obj.Attributes()) +} + +func TestRawObject_SetPreviousID(t *testing.T) { + obj := NewRaw() + + prev := randID(t) + + obj.SetPreviousID(prev) + + require.Equal(t, prev, obj.PreviousID()) +} + +func TestRawObject_SetChildren(t *testing.T) { + obj := NewRaw() + + id1 := randID(t) + id2 := randID(t) + + obj.SetChildren(id1, id2) + + require.Equal(t, []*ID{id1, id2}, obj.Children()) +} + +func TestRawObject_SetSplitID(t *testing.T) { + obj := NewRaw() + + require.Nil(t, obj.SplitID()) + + splitID := NewSplitID() + obj.SetSplitID(splitID) + + require.Equal(t, obj.SplitID(), splitID) +} + +func TestRawObject_SetParent(t *testing.T) { + obj := NewRaw() + + require.Nil(t, obj.Parent()) + + par := NewRaw() + par.SetID(randID(t)) + par.SetContainerID(cidtest.GenerateID()) + par.SetSignature(signature.New()) + + parObj := par.Object() + + obj.SetParent(parObj) + + require.Equal(t, parObj, obj.Parent()) +} + +func TestRawObject_ToV2(t *testing.T) { + objV2 := new(object.Object) + objV2.SetPayload([]byte{1, 2, 3}) + + obj := NewRawFromV2(objV2) + + require.Equal(t, objV2, obj.ToV2()) +} + +func TestRawObject_SetSessionToken(t *testing.T) { + obj := NewRaw() + + tok := sessiontest.Generate() + + obj.SetSessionToken(tok) + + require.Equal(t, tok, obj.SessionToken()) +} + +func TestRawObject_SetType(t *testing.T) { + obj := NewRaw() + + typ := TypeStorageGroup + + obj.SetType(typ) + + require.Equal(t, typ, obj.Type()) +} + +func TestRawObject_CutPayload(t *testing.T) { + o1 := NewRaw() + + p1 := []byte{12, 3} + o1.SetPayload(p1) + + sz := uint64(13) + o1.SetPayloadSize(sz) + + o2 := o1.CutPayload() + + require.Equal(t, sz, o2.PayloadSize()) + require.Empty(t, o2.Payload()) + + sz++ + o1.SetPayloadSize(sz) + + require.Equal(t, sz, o1.PayloadSize()) + require.Equal(t, sz, o2.PayloadSize()) + + p2 := []byte{4, 5, 6} + o2.SetPayload(p2) + + require.Equal(t, p2, o2.Payload()) + require.Equal(t, p1, o1.Payload()) +} + +func TestRawObject_SetParentID(t *testing.T) { + obj := NewRaw() + + id := randID(t) + obj.setParentID(id) + + require.Equal(t, id, obj.ParentID()) +} + +func TestRawObject_ResetRelations(t *testing.T) { + obj := NewRaw() + + obj.SetPreviousID(randID(t)) + + obj.ResetRelations() + + require.Nil(t, obj.PreviousID()) +} + +func TestRwObject_HasParent(t *testing.T) { + obj := NewRaw() + + obj.InitRelations() + + require.True(t, obj.HasParent()) + + obj.ResetRelations() + + require.False(t, obj.HasParent()) +} + +func TestRWObjectEncoding(t *testing.T) { + o := NewRaw() + o.SetID(randID(t)) + + t.Run("binary", func(t *testing.T) { + data, err := o.Marshal() + require.NoError(t, err) + + o2 := NewRaw() + require.NoError(t, o2.Unmarshal(data)) + + require.Equal(t, o, o2) + }) + + t.Run("json", func(t *testing.T) { + data, err := o.MarshalJSON() + require.NoError(t, err) + + o2 := NewRaw() + require.NoError(t, o2.UnmarshalJSON(data)) + + require.Equal(t, o, o2) + }) +} diff --git a/object/rw.go b/object/rw.go new file mode 100644 index 00000000..014e22e2 --- /dev/null +++ b/object/rw.go @@ -0,0 +1,388 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/session" + "github.com/nspcc-dev/neofs-sdk-go/signature" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// wrapper over v2 Object that provides +// public getter and private setters. +type rwObject object.Object + +// ToV2 converts Object to v2 Object message. +func (o *rwObject) ToV2() *object.Object { + return (*object.Object)(o) +} + +func (o *rwObject) setHeaderField(setter func(*object.Header)) { + obj := (*object.Object)(o) + h := obj.GetHeader() + + if h == nil { + h = new(object.Header) + obj.SetHeader(h) + } + + setter(h) +} + +func (o *rwObject) setSplitFields(setter func(*object.SplitHeader)) { + o.setHeaderField(func(h *object.Header) { + split := h.GetSplit() + if split == nil { + split = new(object.SplitHeader) + h.SetSplit(split) + } + + setter(split) + }) +} + +// ID returns object identifier. +func (o *rwObject) ID() *ID { + return NewIDFromV2( + (*object.Object)(o). + GetObjectID(), + ) +} + +func (o *rwObject) setID(v *ID) { + (*object.Object)(o). + SetObjectID(v.ToV2()) +} + +// Signature returns signature of the object identifier. +func (o *rwObject) Signature() *signature.Signature { + return signature.NewFromV2( + (*object.Object)(o).GetSignature()) +} + +func (o *rwObject) setSignature(v *signature.Signature) { + (*object.Object)(o).SetSignature(v.ToV2()) +} + +// Payload returns payload bytes. +func (o *rwObject) Payload() []byte { + return (*object.Object)(o).GetPayload() +} + +func (o *rwObject) setPayload(v []byte) { + (*object.Object)(o).SetPayload(v) +} + +// Version returns version of the object. +func (o *rwObject) Version() *version.Version { + return version.NewFromV2( + (*object.Object)(o). + GetHeader(). + GetVersion(), + ) +} + +func (o *rwObject) setVersion(v *version.Version) { + o.setHeaderField(func(h *object.Header) { + h.SetVersion(v.ToV2()) + }) +} + +// PayloadSize returns payload length of the object. +func (o *rwObject) PayloadSize() uint64 { + return (*object.Object)(o). + GetHeader(). + GetPayloadLength() +} + +func (o *rwObject) setPayloadSize(v uint64) { + o.setHeaderField(func(h *object.Header) { + h.SetPayloadLength(v) + }) +} + +// ContainerID returns identifier of the related container. +func (o *rwObject) ContainerID() *cid.ID { + return cid.NewFromV2( + (*object.Object)(o). + GetHeader(). + GetContainerID(), + ) +} + +func (o *rwObject) setContainerID(v *cid.ID) { + o.setHeaderField(func(h *object.Header) { + h.SetContainerID(v.ToV2()) + }) +} + +// OwnerID returns identifier of the object owner. +func (o *rwObject) OwnerID() *owner.ID { + return owner.NewIDFromV2( + (*object.Object)(o). + GetHeader(). + GetOwnerID(), + ) +} + +func (o *rwObject) setOwnerID(v *owner.ID) { + o.setHeaderField(func(h *object.Header) { + h.SetOwnerID(v.ToV2()) + }) +} + +// CreationEpoch returns epoch number in which object was created. +func (o *rwObject) CreationEpoch() uint64 { + return (*object.Object)(o). + GetHeader(). + GetCreationEpoch() +} + +func (o *rwObject) setCreationEpoch(v uint64) { + o.setHeaderField(func(h *object.Header) { + h.SetCreationEpoch(v) + }) +} + +// PayloadChecksum returns checksum of the object payload. +func (o *rwObject) PayloadChecksum() *checksum.Checksum { + return checksum.NewFromV2( + (*object.Object)(o). + GetHeader(). + GetPayloadHash(), + ) +} + +func (o *rwObject) setPayloadChecksum(v *checksum.Checksum) { + o.setHeaderField(func(h *object.Header) { + h.SetPayloadHash(v.ToV2()) + }) +} + +// PayloadHomomorphicHash returns homomorphic hash of the object payload. +func (o *rwObject) PayloadHomomorphicHash() *checksum.Checksum { + return checksum.NewFromV2( + (*object.Object)(o). + GetHeader(). + GetHomomorphicHash(), + ) +} + +func (o *rwObject) setPayloadHomomorphicHash(v *checksum.Checksum) { + o.setHeaderField(func(h *object.Header) { + h.SetHomomorphicHash(v.ToV2()) + }) +} + +// Attributes returns object attributes. +func (o *rwObject) Attributes() []*Attribute { + attrs := (*object.Object)(o). + GetHeader(). + GetAttributes() + + res := make([]*Attribute, 0, len(attrs)) + + for i := range attrs { + res = append(res, NewAttributeFromV2(attrs[i])) + } + + return res +} + +func (o *rwObject) setAttributes(v ...*Attribute) { + attrs := make([]*object.Attribute, 0, len(v)) + + for i := range v { + attrs = append(attrs, v[i].ToV2()) + } + + o.setHeaderField(func(h *object.Header) { + h.SetAttributes(attrs) + }) +} + +// PreviousID returns identifier of the previous sibling object. +func (o *rwObject) PreviousID() *ID { + return NewIDFromV2( + (*object.Object)(o). + GetHeader(). + GetSplit(). + GetPrevious(), + ) +} + +func (o *rwObject) setPreviousID(v *ID) { + o.setSplitFields(func(split *object.SplitHeader) { + split.SetPrevious(v.ToV2()) + }) +} + +// Children return list of the identifiers of the child objects. +func (o *rwObject) Children() []*ID { + ids := (*object.Object)(o). + GetHeader(). + GetSplit(). + GetChildren() + + res := make([]*ID, 0, len(ids)) + + for i := range ids { + res = append(res, NewIDFromV2(ids[i])) + } + + return res +} + +func (o *rwObject) setChildren(v ...*ID) { + ids := make([]*refs.ObjectID, 0, len(v)) + + for i := range v { + ids = append(ids, v[i].ToV2()) + } + + o.setSplitFields(func(split *object.SplitHeader) { + split.SetChildren(ids) + }) +} + +// SplitID return split identity of split object. If object is not split +// returns nil. +func (o *rwObject) SplitID() *SplitID { + return NewSplitIDFromV2( + (*object.Object)(o). + GetHeader(). + GetSplit(). + GetSplitID(), + ) +} + +func (o *rwObject) setSplitID(id *SplitID) { + o.setSplitFields(func(split *object.SplitHeader) { + split.SetSplitID(id.ToV2()) + }) +} + +// ParentID returns identifier of the parent object. +func (o *rwObject) ParentID() *ID { + return NewIDFromV2( + (*object.Object)(o). + GetHeader(). + GetSplit(). + GetParent(), + ) +} + +func (o *rwObject) setParentID(v *ID) { + o.setSplitFields(func(split *object.SplitHeader) { + split.SetParent(v.ToV2()) + }) +} + +// Parent returns parent object w/o payload. +func (o *rwObject) Parent() *Object { + h := (*object.Object)(o). + GetHeader(). + GetSplit() + + parSig := h.GetParentSignature() + parHdr := h.GetParentHeader() + + if parSig == nil && parHdr == nil { + return nil + } + + oV2 := new(object.Object) + oV2.SetObjectID(h.GetParent()) + oV2.SetSignature(parSig) + oV2.SetHeader(parHdr) + + return NewFromV2(oV2) +} + +func (o *rwObject) setParent(v *Object) { + o.setSplitFields(func(split *object.SplitHeader) { + split.SetParent((*object.Object)(v.rwObject).GetObjectID()) + split.SetParentSignature((*object.Object)(v.rwObject).GetSignature()) + split.SetParentHeader((*object.Object)(v.rwObject).GetHeader()) + }) +} + +func (o *rwObject) initRelations() { + o.setHeaderField(func(h *object.Header) { + h.SetSplit(new(object.SplitHeader)) + }) +} + +func (o *rwObject) resetRelations() { + o.setHeaderField(func(h *object.Header) { + h.SetSplit(nil) + }) +} + +// SessionToken returns token of the session +// within which object was created. +func (o *rwObject) SessionToken() *session.Token { + return session.NewTokenFromV2( + (*object.Object)(o). + GetHeader(). + GetSessionToken(), + ) +} + +func (o *rwObject) setSessionToken(v *session.Token) { + o.setHeaderField(func(h *object.Header) { + h.SetSessionToken(v.ToV2()) + }) +} + +// Type returns type of the object. +func (o *rwObject) Type() Type { + return TypeFromV2( + (*object.Object)(o). + GetHeader(). + GetObjectType(), + ) +} + +func (o *rwObject) setType(t Type) { + o.setHeaderField(func(h *object.Header) { + h.SetObjectType(t.ToV2()) + }) +} + +func (o *rwObject) cutPayload() *rwObject { + ov2 := new(object.Object) + *ov2 = *(*object.Object)(o) + ov2.SetPayload(nil) + + return (*rwObject)(ov2) +} + +func (o *rwObject) HasParent() bool { + return (*object.Object)(o). + GetHeader(). + GetSplit() != nil +} + +// Marshal marshals object into a protobuf binary form. +func (o *rwObject) Marshal() ([]byte, error) { + return (*object.Object)(o).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of object. +func (o *rwObject) Unmarshal(data []byte) error { + return (*object.Object)(o).Unmarshal(data) +} + +// MarshalJSON encodes object to protobuf JSON format. +func (o *rwObject) MarshalJSON() ([]byte, error) { + return (*object.Object)(o).MarshalJSON() +} + +// UnmarshalJSON decodes object from protobuf JSON format. +func (o *rwObject) UnmarshalJSON(data []byte) error { + return (*object.Object)(o).UnmarshalJSON(data) +} diff --git a/object/search.go b/object/search.go new file mode 100644 index 00000000..3c6dd918 --- /dev/null +++ b/object/search.go @@ -0,0 +1,299 @@ +package object + +import ( + "encoding/json" + "fmt" + + v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// SearchMatchType indicates match operation on specified header. +type SearchMatchType uint32 + +const ( + MatchUnknown SearchMatchType = iota + MatchStringEqual + MatchStringNotEqual + MatchNotPresent + MatchCommonPrefix +) + +func (m SearchMatchType) ToV2() v2object.MatchType { + switch m { + case MatchStringEqual: + return v2object.MatchStringEqual + case MatchStringNotEqual: + return v2object.MatchStringNotEqual + case MatchNotPresent: + return v2object.MatchNotPresent + case MatchCommonPrefix: + return v2object.MatchCommonPrefix + default: + return v2object.MatchUnknown + } +} + +func SearchMatchFromV2(t v2object.MatchType) (m SearchMatchType) { + switch t { + case v2object.MatchStringEqual: + m = MatchStringEqual + case v2object.MatchStringNotEqual: + m = MatchStringNotEqual + case v2object.MatchNotPresent: + m = MatchNotPresent + case v2object.MatchCommonPrefix: + m = MatchCommonPrefix + default: + m = MatchUnknown + } + + return m +} + +// String returns string representation of SearchMatchType. +// +// String mapping: +// * MatchStringEqual: STRING_EQUAL; +// * MatchStringNotEqual: STRING_NOT_EQUAL; +// * MatchNotPresent: NOT_PRESENT; +// * MatchCommonPrefix: COMMON_PREFIX; +// * MatchUnknown, default: MATCH_TYPE_UNSPECIFIED. +func (m SearchMatchType) String() string { + return m.ToV2().String() +} + +// FromString parses SearchMatchType from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (m *SearchMatchType) FromString(s string) bool { + var g v2object.MatchType + + ok := g.FromString(s) + + if ok { + *m = SearchMatchFromV2(g) + } + + return ok +} + +type SearchFilter struct { + header filterKey + value fmt.Stringer + op SearchMatchType +} + +type staticStringer string + +type filterKey struct { + typ filterKeyType + + str string +} + +// enumeration of reserved filter keys. +type filterKeyType int + +type SearchFilters []SearchFilter + +const ( + _ filterKeyType = iota + fKeyVersion + fKeyObjectID + fKeyContainerID + fKeyOwnerID + fKeyCreationEpoch + fKeyPayloadLength + fKeyPayloadHash + fKeyType + fKeyHomomorphicHash + fKeyParent + fKeySplitID + fKeyPropRoot + fKeyPropPhy +) + +func (k filterKey) String() string { + switch k.typ { + default: + return k.str + case fKeyVersion: + return v2object.FilterHeaderVersion + case fKeyObjectID: + return v2object.FilterHeaderObjectID + case fKeyContainerID: + return v2object.FilterHeaderContainerID + case fKeyOwnerID: + return v2object.FilterHeaderOwnerID + case fKeyCreationEpoch: + return v2object.FilterHeaderCreationEpoch + case fKeyPayloadLength: + return v2object.FilterHeaderPayloadLength + case fKeyPayloadHash: + return v2object.FilterHeaderPayloadHash + case fKeyType: + return v2object.FilterHeaderObjectType + case fKeyHomomorphicHash: + return v2object.FilterHeaderHomomorphicHash + case fKeyParent: + return v2object.FilterHeaderParent + case fKeySplitID: + return v2object.FilterHeaderSplitID + case fKeyPropRoot: + return v2object.FilterPropertyRoot + case fKeyPropPhy: + return v2object.FilterPropertyPhy + } +} + +func (s staticStringer) String() string { + return string(s) +} + +func (f *SearchFilter) Header() string { + return f.header.String() +} + +func (f *SearchFilter) Value() string { + return f.value.String() +} + +func (f *SearchFilter) Operation() SearchMatchType { + return f.op +} + +func NewSearchFilters() SearchFilters { + return SearchFilters{} +} + +func NewSearchFiltersFromV2(v2 []*v2object.SearchFilter) SearchFilters { + filters := make(SearchFilters, 0, len(v2)) + + for i := range v2 { + if v2[i] == nil { + continue + } + + filters.AddFilter( + v2[i].GetKey(), + v2[i].GetValue(), + SearchMatchFromV2(v2[i].GetMatchType()), + ) + } + + return filters +} + +func (f *SearchFilters) addFilter(op SearchMatchType, keyTyp filterKeyType, key string, val fmt.Stringer) { + if *f == nil { + *f = make(SearchFilters, 0, 1) + } + + *f = append(*f, SearchFilter{ + header: filterKey{ + typ: keyTyp, + str: key, + }, + value: val, + op: op, + }) +} + +func (f *SearchFilters) AddFilter(header, value string, op SearchMatchType) { + f.addFilter(op, 0, header, staticStringer(value)) +} + +func (f *SearchFilters) addReservedFilter(op SearchMatchType, keyTyp filterKeyType, val fmt.Stringer) { + f.addFilter(op, keyTyp, "", val) +} + +// addFlagFilters adds filters that works like flags: they don't need to have +// specific match type or value. They processed by NeoFS nodes by the fact +// of presence in search query. E.g.: PHY, ROOT. +func (f *SearchFilters) addFlagFilter(keyTyp filterKeyType) { + f.addFilter(MatchUnknown, keyTyp, "", staticStringer("")) +} + +func (f *SearchFilters) AddObjectVersionFilter(op SearchMatchType, v *version.Version) { + f.addReservedFilter(op, fKeyVersion, v) +} + +func (f *SearchFilters) AddObjectContainerIDFilter(m SearchMatchType, id *cid.ID) { + f.addReservedFilter(m, fKeyContainerID, id) +} + +func (f *SearchFilters) AddObjectOwnerIDFilter(m SearchMatchType, id *owner.ID) { + f.addReservedFilter(m, fKeyOwnerID, id) +} + +func (f SearchFilters) ToV2() []*v2object.SearchFilter { + result := make([]*v2object.SearchFilter, 0, len(f)) + + for i := range f { + v2 := new(v2object.SearchFilter) + v2.SetKey(f[i].header.String()) + v2.SetValue(f[i].value.String()) + v2.SetMatchType(f[i].op.ToV2()) + + result = append(result, v2) + } + + return result +} + +func (f *SearchFilters) addRootFilter() { + f.addFlagFilter(fKeyPropRoot) +} + +func (f *SearchFilters) AddRootFilter() { + f.addRootFilter() +} + +func (f *SearchFilters) addPhyFilter() { + f.addFlagFilter(fKeyPropPhy) +} + +func (f *SearchFilters) AddPhyFilter() { + f.addPhyFilter() +} + +// AddParentIDFilter adds filter by parent identifier. +func (f *SearchFilters) AddParentIDFilter(m SearchMatchType, id *ID) { + f.addReservedFilter(m, fKeyParent, id) +} + +// AddObjectIDFilter adds filter by object identifier. +func (f *SearchFilters) AddObjectIDFilter(m SearchMatchType, id *ID) { + f.addReservedFilter(m, fKeyObjectID, id) +} + +func (f *SearchFilters) AddSplitIDFilter(m SearchMatchType, id *SplitID) { + f.addReservedFilter(m, fKeySplitID, id) +} + +// AddTypeFilter adds filter by object type. +func (f *SearchFilters) AddTypeFilter(m SearchMatchType, typ Type) { + f.addReservedFilter(m, fKeyType, typ) +} + +// MarshalJSON encodes SearchFilters to protobuf JSON format. +func (f *SearchFilters) MarshalJSON() ([]byte, error) { + return json.Marshal(f.ToV2()) +} + +// UnmarshalJSON decodes SearchFilters from protobuf JSON format. +func (f *SearchFilters) UnmarshalJSON(data []byte) error { + var fsV2 []*v2object.SearchFilter + + if err := json.Unmarshal(data, &fsV2); err != nil { + return err + } + + *f = NewSearchFiltersFromV2(fsV2) + + return nil +} diff --git a/object/search_test.go b/object/search_test.go new file mode 100644 index 00000000..a9ec0cf4 --- /dev/null +++ b/object/search_test.go @@ -0,0 +1,208 @@ +package object_test + +import ( + "crypto/sha256" + "math/rand" + "testing" + + v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/stretchr/testify/require" +) + +var eqV2Matches = map[object.SearchMatchType]v2object.MatchType{ + object.MatchUnknown: v2object.MatchUnknown, + object.MatchStringEqual: v2object.MatchStringEqual, + object.MatchStringNotEqual: v2object.MatchStringNotEqual, + object.MatchNotPresent: v2object.MatchNotPresent, + object.MatchCommonPrefix: v2object.MatchCommonPrefix, +} + +func TestMatch(t *testing.T) { + t.Run("known matches", func(t *testing.T) { + for matchType, matchTypeV2 := range eqV2Matches { + require.Equal(t, matchTypeV2, matchType.ToV2()) + require.Equal(t, object.SearchMatchFromV2(matchTypeV2), matchType) + } + }) + + t.Run("unknown matches", func(t *testing.T) { + var unknownMatchType object.SearchMatchType + + for matchType := range eqV2Matches { + unknownMatchType += matchType + } + + unknownMatchType++ + + require.Equal(t, unknownMatchType.ToV2(), v2object.MatchUnknown) + + var unknownMatchTypeV2 v2object.MatchType + + for _, matchTypeV2 := range eqV2Matches { + unknownMatchTypeV2 += matchTypeV2 + } + + unknownMatchTypeV2++ + + require.Equal(t, object.SearchMatchFromV2(unknownMatchTypeV2), object.MatchUnknown) + }) +} + +func TestFilter(t *testing.T) { + inputs := [][]string{ + {"user-header", "user-value"}, + } + + filters := object.NewSearchFilters() + for i := range inputs { + filters.AddFilter(inputs[i][0], inputs[i][1], object.MatchStringEqual) + } + + require.Len(t, filters, len(inputs)) + for i := range inputs { + require.Equal(t, inputs[i][0], filters[i].Header()) + require.Equal(t, inputs[i][1], filters[i].Value()) + require.Equal(t, object.MatchStringEqual, filters[i].Operation()) + } + + v2 := filters.ToV2() + newFilters := object.NewSearchFiltersFromV2(v2) + require.Equal(t, filters, newFilters) +} + +func TestSearchFilters_AddRootFilter(t *testing.T) { + fs := new(object.SearchFilters) + + fs.AddRootFilter() + + require.Len(t, *fs, 1) + + f := (*fs)[0] + + require.Equal(t, object.MatchUnknown, f.Operation()) + require.Equal(t, v2object.FilterPropertyRoot, f.Header()) + require.Equal(t, "", f.Value()) +} + +func TestSearchFilters_AddPhyFilter(t *testing.T) { + fs := new(object.SearchFilters) + + fs.AddPhyFilter() + + require.Len(t, *fs, 1) + + f := (*fs)[0] + + require.Equal(t, object.MatchUnknown, f.Operation()) + require.Equal(t, v2object.FilterPropertyPhy, f.Header()) + require.Equal(t, "", f.Value()) +} + +func testOID() *object.ID { + cs := [sha256.Size]byte{} + + rand.Read(cs[:]) + + id := object.NewID() + id.SetSHA256(cs) + + return id +} + +func TestSearchFilters_AddParentIDFilter(t *testing.T) { + par := testOID() + + fs := object.SearchFilters{} + fs.AddParentIDFilter(object.MatchStringEqual, par) + + fsV2 := fs.ToV2() + + require.Len(t, fsV2, 1) + + require.Equal(t, v2object.FilterHeaderParent, fsV2[0].GetKey()) + require.Equal(t, par.String(), fsV2[0].GetValue()) + require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType()) +} + +func TestSearchFilters_AddObjectIDFilter(t *testing.T) { + id := testOID() + + fs := new(object.SearchFilters) + fs.AddObjectIDFilter(object.MatchStringEqual, id) + + t.Run("v2", func(t *testing.T) { + fsV2 := fs.ToV2() + + require.Len(t, fsV2, 1) + + require.Equal(t, v2object.FilterHeaderObjectID, fsV2[0].GetKey()) + require.Equal(t, id.String(), fsV2[0].GetValue()) + require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType()) + }) +} + +func TestSearchFilters_AddSplitIDFilter(t *testing.T) { + id := object.NewSplitID() + + fs := new(object.SearchFilters) + fs.AddSplitIDFilter(object.MatchStringEqual, id) + + t.Run("v2", func(t *testing.T) { + fsV2 := fs.ToV2() + + require.Len(t, fsV2, 1) + + require.Equal(t, v2object.FilterHeaderSplitID, fsV2[0].GetKey()) + require.Equal(t, id.String(), fsV2[0].GetValue()) + require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType()) + }) +} + +func TestSearchFilters_AddTypeFilter(t *testing.T) { + typ := object.TypeTombstone + + fs := new(object.SearchFilters) + fs.AddTypeFilter(object.MatchStringEqual, typ) + + t.Run("v2", func(t *testing.T) { + fsV2 := fs.ToV2() + + require.Len(t, fsV2, 1) + + require.Equal(t, v2object.FilterHeaderObjectType, fsV2[0].GetKey()) + require.Equal(t, typ.String(), fsV2[0].GetValue()) + require.Equal(t, v2object.MatchStringEqual, fsV2[0].GetMatchType()) + }) +} + +func TestSearchFiltersEncoding(t *testing.T) { + fs := object.NewSearchFilters() + fs.AddFilter("key 1", "value 2", object.MatchStringEqual) + fs.AddFilter("key 2", "value 2", object.MatchStringNotEqual) + fs.AddFilter("key 2", "value 2", object.MatchCommonPrefix) + + t.Run("json", func(t *testing.T) { + data, err := fs.MarshalJSON() + require.NoError(t, err) + + fs2 := object.NewSearchFilters() + require.NoError(t, fs2.UnmarshalJSON(data)) + + require.Equal(t, fs, fs2) + }) +} + +func TestSearchMatchType_String(t *testing.T) { + toPtr := func(v object.SearchMatchType) *object.SearchMatchType { + return &v + } + + testEnumStrings(t, new(object.SearchMatchType), []enumStringItem{ + {val: toPtr(object.MatchCommonPrefix), str: "COMMON_PREFIX"}, + {val: toPtr(object.MatchStringEqual), str: "STRING_EQUAL"}, + {val: toPtr(object.MatchStringNotEqual), str: "STRING_NOT_EQUAL"}, + {val: toPtr(object.MatchNotPresent), str: "NOT_PRESENT"}, + {val: toPtr(object.MatchUnknown), str: "MATCH_TYPE_UNSPECIFIED"}, + }) +} diff --git a/object/splitid.go b/object/splitid.go new file mode 100644 index 00000000..84288f29 --- /dev/null +++ b/object/splitid.go @@ -0,0 +1,80 @@ +package object + +import ( + "github.com/google/uuid" +) + +// SplitID is a UUIDv4 used as attribute in split objects. +type SplitID struct { + uuid uuid.UUID +} + +// NewSplitID returns UUID representation of splitID attribute. +// +// Defaults: +// - id: random UUID. +func NewSplitID() *SplitID { + return &SplitID{ + uuid: uuid.New(), + } +} + +// NewSplitIDFromV2 returns parsed UUID from bytes. +// If v is invalid UUIDv4 byte sequence, then function returns nil. +// +// Nil converts to nil. +func NewSplitIDFromV2(v []byte) *SplitID { + if v == nil { + return nil + } + + id := uuid.New() + + err := id.UnmarshalBinary(v) + if err != nil { + return nil + } + + return &SplitID{ + uuid: id, + } +} + +// Parse converts UUIDv4 string representation into SplitID. +func (id *SplitID) Parse(s string) (err error) { + id.uuid, err = uuid.Parse(s) + if err != nil { + return err + } + + return nil +} + +// String returns UUIDv4 string representation of SplitID. +func (id *SplitID) String() string { + if id == nil { + return "" + } + + return id.uuid.String() +} + +// SetUUID sets pre created UUID structure as SplitID. +func (id *SplitID) SetUUID(v uuid.UUID) { + if id != nil { + id.uuid = v + } +} + +// ToV2 converts SplitID to a representation of SplitID in neofs-api v2. +// +// Nil SplitID converts to nil. +func (id *SplitID) ToV2() []byte { + if id == nil { + return nil + } + + data, _ := id.uuid.MarshalBinary() // err is always nil + + return data +} diff --git a/object/splitid_test.go b/object/splitid_test.go new file mode 100644 index 00000000..6a15ac31 --- /dev/null +++ b/object/splitid_test.go @@ -0,0 +1,63 @@ +package object_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/stretchr/testify/require" +) + +func TestSplitID(t *testing.T) { + id := object.NewSplitID() + + t.Run("toV2/fromV2", func(t *testing.T) { + data := id.ToV2() + + newID := object.NewSplitIDFromV2(data) + require.NotNil(t, newID) + + require.Equal(t, id, newID) + }) + + t.Run("string/parse", func(t *testing.T) { + idStr := id.String() + + newID := object.NewSplitID() + require.NoError(t, newID.Parse(idStr)) + + require.Equal(t, id, newID) + }) + + t.Run("set UUID", func(t *testing.T) { + newUUID := uuid.New() + id.SetUUID(newUUID) + + require.Equal(t, newUUID.String(), id.String()) + }) + + t.Run("nil value", func(t *testing.T) { + var newID *object.SplitID + + require.NotPanics(t, func() { + require.Nil(t, newID.ToV2()) + require.Equal(t, "", newID.String()) + }) + }) +} + +func TestSplitID_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *object.SplitID + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewIDFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x []byte + + require.Nil(t, object.NewSplitIDFromV2(x)) + }) +} diff --git a/object/splitinfo.go b/object/splitinfo.go new file mode 100644 index 00000000..3dfc2212 --- /dev/null +++ b/object/splitinfo.go @@ -0,0 +1,66 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" +) + +type SplitInfo object.SplitInfo + +// NewSplitInfoFromV2 wraps v2 SplitInfo message to SplitInfo. +// +// Nil object.SplitInfo converts to nil. +func NewSplitInfoFromV2(v2 *object.SplitInfo) *SplitInfo { + return (*SplitInfo)(v2) +} + +// NewSplitInfo creates and initializes blank SplitInfo. +// +// Defaults: +// - splitID: nil; +// - lastPart nil; +// - link: nil. +func NewSplitInfo() *SplitInfo { + return NewSplitInfoFromV2(new(object.SplitInfo)) +} + +// ToV2 converts SplitInfo to v2 SplitInfo message. +// +// Nil SplitInfo converts to nil. +func (s *SplitInfo) ToV2() *object.SplitInfo { + return (*object.SplitInfo)(s) +} + +func (s *SplitInfo) SplitID() *SplitID { + return NewSplitIDFromV2( + (*object.SplitInfo)(s).GetSplitID()) +} + +func (s *SplitInfo) SetSplitID(v *SplitID) { + (*object.SplitInfo)(s).SetSplitID(v.ToV2()) +} + +func (s *SplitInfo) LastPart() *ID { + return NewIDFromV2( + (*object.SplitInfo)(s).GetLastPart()) +} + +func (s *SplitInfo) SetLastPart(v *ID) { + (*object.SplitInfo)(s).SetLastPart(v.ToV2()) +} + +func (s *SplitInfo) Link() *ID { + return NewIDFromV2( + (*object.SplitInfo)(s).GetLink()) +} + +func (s *SplitInfo) SetLink(v *ID) { + (*object.SplitInfo)(s).SetLink(v.ToV2()) +} + +func (s *SplitInfo) Marshal() ([]byte, error) { + return (*object.SplitInfo)(s).StableMarshal(nil) +} + +func (s *SplitInfo) Unmarshal(data []byte) error { + return (*object.SplitInfo)(s).Unmarshal(data) +} diff --git a/object/splitinfo_test.go b/object/splitinfo_test.go new file mode 100644 index 00000000..2754f2a1 --- /dev/null +++ b/object/splitinfo_test.go @@ -0,0 +1,88 @@ +package object_test + +import ( + "crypto/rand" + "testing" + + objv2 "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/stretchr/testify/require" +) + +func TestSplitInfo(t *testing.T) { + s := object.NewSplitInfo() + splitID := object.NewSplitID() + lastPart := generateID() + link := generateID() + + s.SetSplitID(splitID) + require.Equal(t, splitID, s.SplitID()) + + s.SetLastPart(lastPart) + require.Equal(t, lastPart, s.LastPart()) + + s.SetLink(link) + require.Equal(t, link, s.Link()) + + t.Run("to and from v2", func(t *testing.T) { + v2 := s.ToV2() + newS := object.NewSplitInfoFromV2(v2) + + require.Equal(t, s, newS) + }) + + t.Run("marshal and unmarshal", func(t *testing.T) { + data, err := s.Marshal() + require.NoError(t, err) + + newS := object.NewSplitInfo() + + err = newS.Unmarshal(data) + require.NoError(t, err) + require.Equal(t, s, newS) + }) +} + +func generateID() *object.ID { + var buf [32]byte + _, _ = rand.Read(buf[:]) + + id := object.NewID() + id.SetSHA256(buf) + + return id +} + +func TestNewSplitInfoFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *objv2.SplitInfo + + require.Nil(t, object.NewSplitInfoFromV2(x)) + }) +} + +func TestSplitInfo_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *object.SplitInfo + + require.Nil(t, x.ToV2()) + }) +} + +func TestNewSplitInfo(t *testing.T) { + t.Run("default values", func(t *testing.T) { + si := object.NewSplitInfo() + + // check initial values + require.Nil(t, si.SplitID()) + require.Nil(t, si.LastPart()) + require.Nil(t, si.Link()) + + // convert to v2 message + siV2 := si.ToV2() + + require.Nil(t, siV2.GetSplitID()) + require.Nil(t, siV2.GetLastPart()) + require.Nil(t, siV2.GetLink()) + }) +} diff --git a/object/test/generate.go b/object/test/generate.go new file mode 100644 index 00000000..4e5e5bda --- /dev/null +++ b/object/test/generate.go @@ -0,0 +1,143 @@ +package objecttest + +import ( + "crypto/sha256" + "math/rand" + + "github.com/google/uuid" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/nspcc-dev/neofs-sdk-go/object" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/signature" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// ID returns random object.ID. +func ID() *object.ID { + checksum := [sha256.Size]byte{} + + rand.Read(checksum[:]) + + return IDWithChecksum(checksum) +} + +// IDWithChecksum returns object.ID initialized +// with specified checksum. +func IDWithChecksum(cs [sha256.Size]byte) *object.ID { + id := object.NewID() + id.SetSHA256(cs) + + return id +} + +// Address returns random object.Address. +func Address() *object.Address { + x := object.NewAddress() + + x.SetContainerID(cidtest.GenerateID()) + x.SetObjectID(ID()) + + return x +} + +// Range returns random object.Range. +func Range() *object.Range { + x := object.NewRange() + + x.SetOffset(1024) + x.SetLength(2048) + + return x +} + +// Attribute returns random object.Attribute. +func Attribute() *object.Attribute { + x := object.NewAttribute() + + x.SetKey("key") + x.SetValue("value") + + return x +} + +// SplitID returns random object.SplitID. +func SplitID() *object.SplitID { + x := object.NewSplitID() + + x.SetUUID(uuid.New()) + + return x +} + +func generateRaw(withParent bool) *object.RawObject { + x := object.NewRaw() + + x.SetID(ID()) + x.SetSessionToken(sessiontest.Generate()) + x.SetPayload([]byte{1, 2, 3}) + x.SetOwnerID(ownertest.GenerateID()) + x.SetContainerID(cidtest.GenerateID()) + x.SetType(object.TypeTombstone) + x.SetVersion(version.Current()) + x.SetPayloadSize(111) + x.SetCreationEpoch(222) + x.SetPreviousID(ID()) + x.SetParentID(ID()) + x.SetChildren(ID(), ID()) + x.SetAttributes(Attribute(), Attribute()) + x.SetSplitID(SplitID()) + // TODO reuse generators + x.SetPayloadChecksum(checksum.New()) + x.SetPayloadHomomorphicHash(checksum.New()) + x.SetSignature(signature.New()) + + if withParent { + x.SetParent(generateRaw(false).Object()) + } + + return x +} + +// Raw returns random object.RawObject. +func Raw() *object.RawObject { + return generateRaw(true) +} + +// Object returns random object.Object. +func Object() *object.Object { + return Raw().Object() +} + +// Tombstone returns random object.Tombstone. +func Tombstone() *object.Tombstone { + x := object.NewTombstone() + + x.SetSplitID(SplitID()) + x.SetExpirationEpoch(13) + x.SetMembers([]*object.ID{ID(), ID()}) + + return x +} + +// SplitInfo returns random object.SplitInfo. +func SplitInfo() *object.SplitInfo { + x := object.NewSplitInfo() + + x.SetSplitID(SplitID()) + x.SetLink(ID()) + x.SetLastPart(ID()) + + return x +} + +// SearchFilters returns random object.SearchFilters. +func SearchFilters() object.SearchFilters { + x := object.NewSearchFilters() + + x.AddObjectIDFilter(object.MatchStringEqual, ID()) + x.AddObjectContainerIDFilter(object.MatchStringNotEqual, cidtest.GenerateID()) + + return x +} diff --git a/object/tombstone.go b/object/tombstone.go new file mode 100644 index 00000000..766b4a83 --- /dev/null +++ b/object/tombstone.go @@ -0,0 +1,114 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-api-go/v2/tombstone" +) + +// Tombstone represents v2-compatible tombstone structure. +type Tombstone tombstone.Tombstone + +// NewTombstoneFromV2 wraps v2 Tombstone message to Tombstone. +// +// Nil tombstone.Tombstone converts to nil. +func NewTombstoneFromV2(tV2 *tombstone.Tombstone) *Tombstone { + return (*Tombstone)(tV2) +} + +// NewTombstone creates and initializes blank Tombstone. +// +// Defaults: +// - exp: 0; +// - splitID: nil; +// - members: nil. +func NewTombstone() *Tombstone { + return NewTombstoneFromV2(new(tombstone.Tombstone)) +} + +// ToV2 converts Tombstone to v2 Tombstone message. +// +// Nil Tombstone converts to nil. +func (t *Tombstone) ToV2() *tombstone.Tombstone { + return (*tombstone.Tombstone)(t) +} + +// ExpirationEpoch return number of tombstone expiration epoch. +func (t *Tombstone) ExpirationEpoch() uint64 { + return (*tombstone.Tombstone)(t).GetExpirationEpoch() +} + +// SetExpirationEpoch sets number of tombstone expiration epoch. +func (t *Tombstone) SetExpirationEpoch(v uint64) { + (*tombstone.Tombstone)(t).SetExpirationEpoch(v) +} + +// SplitID returns identifier of object split hierarchy. +func (t *Tombstone) SplitID() *SplitID { + return NewSplitIDFromV2( + (*tombstone.Tombstone)(t).GetSplitID()) +} + +// SetSplitID sets identifier of object split hierarchy. +func (t *Tombstone) SetSplitID(v *SplitID) { + (*tombstone.Tombstone)(t).SetSplitID(v.ToV2()) +} + +// Members returns list of objects to be deleted. +func (t *Tombstone) Members() []*ID { + msV2 := (*tombstone.Tombstone)(t). + GetMembers() + + if msV2 == nil { + return nil + } + + ms := make([]*ID, 0, len(msV2)) + + for i := range msV2 { + ms = append(ms, NewIDFromV2(msV2[i])) + } + + return ms +} + +// SetMembers sets list of objects to be deleted. +func (t *Tombstone) SetMembers(v []*ID) { + var ms []*refs.ObjectID + + if v != nil { + ms = (*tombstone.Tombstone)(t). + GetMembers() + + if ln := len(v); cap(ms) >= ln { + ms = ms[:0] + } else { + ms = make([]*refs.ObjectID, 0, ln) + } + + for i := range v { + ms = append(ms, v[i].ToV2()) + } + } + + (*tombstone.Tombstone)(t).SetMembers(ms) +} + +// Marshal marshals Tombstone into a protobuf binary form. +func (t *Tombstone) Marshal() ([]byte, error) { + return (*tombstone.Tombstone)(t).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of Tombstone. +func (t *Tombstone) Unmarshal(data []byte) error { + return (*tombstone.Tombstone)(t).Unmarshal(data) +} + +// MarshalJSON encodes Tombstone to protobuf JSON format. +func (t *Tombstone) MarshalJSON() ([]byte, error) { + return (*tombstone.Tombstone)(t).MarshalJSON() +} + +// UnmarshalJSON decodes Tombstone from protobuf JSON format. +func (t *Tombstone) UnmarshalJSON(data []byte) error { + return (*tombstone.Tombstone)(t).UnmarshalJSON(data) +} diff --git a/object/tombstone_test.go b/object/tombstone_test.go new file mode 100644 index 00000000..c074fe01 --- /dev/null +++ b/object/tombstone_test.go @@ -0,0 +1,92 @@ +package object + +import ( + "crypto/sha256" + "math/rand" + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/tombstone" + "github.com/stretchr/testify/require" +) + +func generateIDList(sz int) []*ID { + res := make([]*ID, sz) + cs := [sha256.Size]byte{} + + for i := 0; i < sz; i++ { + res[i] = NewID() + rand.Read(cs[:]) + res[i].SetSHA256(cs) + } + + return res +} + +func TestTombstone(t *testing.T) { + ts := NewTombstone() + + exp := uint64(13) + ts.SetExpirationEpoch(exp) + require.Equal(t, exp, ts.ExpirationEpoch()) + + splitID := NewSplitID() + ts.SetSplitID(splitID) + require.Equal(t, splitID, ts.SplitID()) + + members := generateIDList(3) + ts.SetMembers(members) + require.Equal(t, members, ts.Members()) +} + +func TestTombstoneEncoding(t *testing.T) { + ts := NewTombstone() + ts.SetExpirationEpoch(13) + ts.SetSplitID(NewSplitID()) + ts.SetMembers(generateIDList(5)) + + t.Run("binary", func(t *testing.T) { + data, err := ts.Marshal() + require.NoError(t, err) + + ts2 := NewTombstone() + require.NoError(t, ts2.Unmarshal(data)) + + require.Equal(t, ts, ts2) + }) + + t.Run("json", func(t *testing.T) { + data, err := ts.MarshalJSON() + require.NoError(t, err) + + ts2 := NewTombstone() + require.NoError(t, ts2.UnmarshalJSON(data)) + + require.Equal(t, ts, ts2) + }) +} + +func TestNewTombstoneFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *tombstone.Tombstone + + require.Nil(t, NewTombstoneFromV2(x)) + }) +} + +func TestNewTombstone(t *testing.T) { + t.Run("default values", func(t *testing.T) { + ts := NewTombstone() + + // check initial values + require.Nil(t, ts.SplitID()) + require.Nil(t, ts.Members()) + require.Zero(t, ts.ExpirationEpoch()) + + // convert to v2 message + tsV2 := ts.ToV2() + + require.Nil(t, tsV2.GetSplitID()) + require.Nil(t, tsV2.GetMembers()) + require.Zero(t, tsV2.GetExpirationEpoch()) + }) +} diff --git a/object/type.go b/object/type.go new file mode 100644 index 00000000..71c8032a --- /dev/null +++ b/object/type.go @@ -0,0 +1,61 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/v2/object" +) + +type Type uint8 + +const ( + TypeRegular Type = iota + TypeTombstone + TypeStorageGroup +) + +func (t Type) ToV2() object.Type { + switch t { + case TypeTombstone: + return object.TypeTombstone + case TypeStorageGroup: + return object.TypeStorageGroup + default: + return object.TypeRegular + } +} + +func TypeFromV2(t object.Type) Type { + switch t { + case object.TypeTombstone: + return TypeTombstone + case object.TypeStorageGroup: + return TypeStorageGroup + default: + return TypeRegular + } +} + +// String returns string representation of Type. +// +// String mapping: +// * TypeTombstone: TOMBSTONE; +// * TypeStorageGroup: STORAGE_GROUP; +// * TypeRegular, default: REGULAR. +func (t Type) String() string { + return t.ToV2().String() +} + +// FromString parses Type from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (t *Type) FromString(s string) bool { + var g object.Type + + ok := g.FromString(s) + + if ok { + *t = TypeFromV2(g) + } + + return ok +} diff --git a/object/type_test.go b/object/type_test.go new file mode 100644 index 00000000..f97c985b --- /dev/null +++ b/object/type_test.go @@ -0,0 +1,79 @@ +package object_test + +import ( + "testing" + + v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/stretchr/testify/require" +) + +func TestType_ToV2(t *testing.T) { + typs := []struct { + t object.Type + t2 v2object.Type + }{ + { + t: object.TypeRegular, + t2: v2object.TypeRegular, + }, + { + t: object.TypeTombstone, + t2: v2object.TypeTombstone, + }, + { + t: object.TypeStorageGroup, + t2: v2object.TypeStorageGroup, + }, + } + + for _, item := range typs { + t2 := item.t.ToV2() + + require.Equal(t, item.t2, t2) + + require.Equal(t, item.t, object.TypeFromV2(item.t2)) + } +} + +func TestType_String(t *testing.T) { + toPtr := func(v object.Type) *object.Type { + return &v + } + + testEnumStrings(t, new(object.Type), []enumStringItem{ + {val: toPtr(object.TypeTombstone), str: "TOMBSTONE"}, + {val: toPtr(object.TypeStorageGroup), str: "STORAGE_GROUP"}, + {val: toPtr(object.TypeRegular), str: "REGULAR"}, + }) +} + +type enumIface interface { + FromString(string) bool + String() string +} + +type enumStringItem struct { + val enumIface + str string +} + +func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) { + for _, item := range items { + require.Equal(t, item.str, item.val.String()) + + s := item.val.String() + + require.True(t, e.FromString(s), s) + + require.EqualValues(t, item.val, e, item.val) + } + + // incorrect strings + for _, str := range []string{ + "some string", + "undefined", + } { + require.False(t, e.FromString(str)) + } +} diff --git a/object/wellknown_attributes.go b/object/wellknown_attributes.go new file mode 100644 index 00000000..9e1793ee --- /dev/null +++ b/object/wellknown_attributes.go @@ -0,0 +1,19 @@ +package object + +const ( + // AttributeName is an attribute key that is commonly used to denote + // human-friendly name. + AttributeName = "Name" + + // AttributeFileName is an attribute key that is commonly used to denote + // file name to be associated with the object on saving. + AttributeFileName = "FileName" + + // AttributeTimestamp is an attribute key that is commonly used to denote + // user-defined local time of object creation in Unix Timestamp format. + AttributeTimestamp = "Timestamp" + + // AttributeTimestamp is an attribute key that is commonly used to denote + // MIME Content Type of object's payload. + AttributeContentType = "Content-Type" +)