diff --git a/client/container.go b/client/container.go index a1f7dff..ed5337f 100644 --- a/client/container.go +++ b/client/container.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "fmt" v2container "github.com/nspcc-dev/neofs-api-go/v2/container" @@ -98,13 +99,14 @@ func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResCon // TODO: check private key is set before forming the request // sign container - cnr := prm.cnr.ToV2() + var cnr v2container.Container + prm.cnr.WriteToV2(&cnr) var sig neofscrypto.Signature - err := sig.Calculate(neofsecdsa.SignerRFC6979(c.prm.key), cnr.StableMarshal(nil)) + err := container.CalculateSignature(&sig, prm.cnr, c.prm.key) if err != nil { - return nil, fmt.Errorf("calculate signature: %w", err) + return nil, fmt.Errorf("calculate container signature: %w", err) } var sigv2 refs.Signature @@ -113,7 +115,7 @@ func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResCon // form request body reqBody := new(v2container.PutRequestBody) - reqBody.SetContainer(prm.cnr.ToV2()) + reqBody.SetContainer(&cnr) reqBody.SetSignature(&sigv2) // form meta header @@ -188,17 +190,17 @@ func (x *PrmContainerGet) SetContainer(id cid.ID) { type ResContainerGet struct { statusRes - cnr *container.Container + cnr container.Container } // Container returns structured information about the requested container. // // Client doesn't retain value so modification is safe. -func (x ResContainerGet) Container() *container.Container { +func (x ResContainerGet) Container() container.Container { return x.cnr } -func (x *ResContainerGet) setContainer(cnr *container.Container) { +func (x *ResContainerGet) setContainer(cnr container.Container) { x.cnr = cnr } @@ -253,9 +255,19 @@ func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResCon cc.result = func(r responseV2) { resp := r.(*v2container.GetResponse) - body := resp.GetBody() + cnrV2 := resp.GetBody().GetContainer() + if cnrV2 == nil { + cc.err = errors.New("missing container in response") + return + } - cnr := container.NewContainerFromV2(body.GetContainer()) + var cnr container.Container + + err := cnr.ReadFromV2(*cnrV2) + if err != nil { + cc.err = fmt.Errorf("invalid container in response: %w", err) + return + } res.setContainer(cnr) } @@ -724,15 +736,15 @@ func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) type PrmAnnounceSpace struct { prmCommonMeta - announcements []container.UsedSpaceAnnouncement + announcements []container.SizeEstimation } // SetValues sets values describing volume of space that is used for the container objects. // Required parameter. Must not be empty. // // Must not be mutated before the end of the operation. -func (x *PrmAnnounceSpace) SetValues(announcements []container.UsedSpaceAnnouncement) { - x.announcements = announcements +func (x *PrmAnnounceSpace) SetValues(vs []container.SizeEstimation) { + x.announcements = vs } // ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation. @@ -770,7 +782,7 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounce // convert list of SDK announcement structures into NeoFS-API v2 list v2announce := make([]v2container.UsedSpaceAnnouncement, len(prm.announcements)) for i := range prm.announcements { - v2announce[i] = *prm.announcements[i].ToV2() + prm.announcements[i].WriteToV2(&v2announce[i]) } // prepare body of the NeoFS-API v2 request and request itself diff --git a/container/announcement.go b/container/announcement.go deleted file mode 100644 index 4b3d626..0000000 --- a/container/announcement.go +++ /dev/null @@ -1,113 +0,0 @@ -package container - -import ( - "errors" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" -) - -// UsedSpaceAnnouncement is an announcement message used by storage nodes to -// estimate actual container sizes. -type UsedSpaceAnnouncement container.UsedSpaceAnnouncement - -// NewAnnouncement initialize empty UsedSpaceAnnouncement message. -// -// Defaults: -// - epoch: 0; -// - usedSpace: 0; -// - cid: nil. -func NewAnnouncement() *UsedSpaceAnnouncement { - return NewAnnouncementFromV2(new(container.UsedSpaceAnnouncement)) -} - -// NewAnnouncementFromV2 wraps protocol dependent version of -// UsedSpaceAnnouncement message. -// -// Nil container.UsedSpaceAnnouncement converts to nil. -func NewAnnouncementFromV2(v *container.UsedSpaceAnnouncement) *UsedSpaceAnnouncement { - return (*UsedSpaceAnnouncement)(v) -} - -// Epoch of the announcement. -func (a *UsedSpaceAnnouncement) Epoch() uint64 { - return (*container.UsedSpaceAnnouncement)(a).GetEpoch() -} - -// SetEpoch sets announcement epoch value. -func (a *UsedSpaceAnnouncement) SetEpoch(epoch uint64) { - (*container.UsedSpaceAnnouncement)(a).SetEpoch(epoch) -} - -// ContainerID of the announcement. -func (a *UsedSpaceAnnouncement) ContainerID() (cID cid.ID, isSet bool) { - v2 := (*container.UsedSpaceAnnouncement)(a) - - cidV2 := v2.GetContainerID() - if cidV2 == nil { - return - } - - _ = cID.ReadFromV2(*cidV2) - isSet = true - - return -} - -// SetContainerID sets announcement container value. -func (a *UsedSpaceAnnouncement) SetContainerID(cnr cid.ID) { - var cidV2 refs.ContainerID - cnr.WriteToV2(&cidV2) - - (*container.UsedSpaceAnnouncement)(a).SetContainerID(&cidV2) -} - -// UsedSpace in container. -func (a *UsedSpaceAnnouncement) UsedSpace() uint64 { - return (*container.UsedSpaceAnnouncement)(a).GetUsedSpace() -} - -// SetUsedSpace sets used space value by specified container. -func (a *UsedSpaceAnnouncement) SetUsedSpace(value uint64) { - (*container.UsedSpaceAnnouncement)(a).SetUsedSpace(value) -} - -// ToV2 returns protocol dependent version of UsedSpaceAnnouncement message. -// -// Nil UsedSpaceAnnouncement converts to nil. -func (a *UsedSpaceAnnouncement) ToV2() *container.UsedSpaceAnnouncement { - return (*container.UsedSpaceAnnouncement)(a) -} - -// Marshal marshals UsedSpaceAnnouncement into a protobuf binary form. -func (a *UsedSpaceAnnouncement) Marshal() ([]byte, error) { - return a.ToV2().StableMarshal(nil), nil -} - -var errCIDNotSet = errors.New("container ID is not set") - -// Unmarshal unmarshals protobuf binary representation of UsedSpaceAnnouncement. -func (a *UsedSpaceAnnouncement) Unmarshal(data []byte) error { - err := a.ToV2().Unmarshal(data) - if err != nil { - return err - } - - // format checks - - var cID cid.ID - - cidV2 := a.ToV2().GetContainerID() - if cidV2 == nil { - return errCIDNotSet - } - - err = cID.ReadFromV2(*cidV2) - if err != nil { - return fmt.Errorf("could not convert V2 container ID: %w", err) - } - - return nil -} diff --git a/container/announcement_test.go b/container/announcement_test.go deleted file mode 100644 index 40084d9..0000000 --- a/container/announcement_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package container_test - -import ( - "crypto/sha256" - "testing" - - containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-sdk-go/container" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" - containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" - "github.com/stretchr/testify/require" -) - -func TestAnnouncement(t *testing.T) { - const epoch, usedSpace uint64 = 10, 100 - - cidValue := [sha256.Size]byte{1, 2, 3} - id := cidtest.IDWithChecksum(cidValue) - - a := container.NewAnnouncement() - a.SetEpoch(epoch) - a.SetContainerID(id) - a.SetUsedSpace(usedSpace) - - require.Equal(t, epoch, a.Epoch()) - require.Equal(t, usedSpace, a.UsedSpace()) - cID, set := a.ContainerID() - require.True(t, set) - require.Equal(t, id, cID) - - t.Run("test v2", func(t *testing.T) { - const newEpoch, newUsedSpace uint64 = 20, 200 - - newCidValue := [32]byte{4, 5, 6} - newCID := new(refs.ContainerID) - newCID.SetValue(newCidValue[:]) - - v2 := a.ToV2() - require.Equal(t, usedSpace, v2.GetUsedSpace()) - require.Equal(t, epoch, v2.GetEpoch()) - require.Equal(t, cidValue[:], v2.GetContainerID().GetValue()) - - v2.SetEpoch(newEpoch) - v2.SetUsedSpace(newUsedSpace) - v2.SetContainerID(newCID) - - newA := container.NewAnnouncementFromV2(v2) - - var cID cid.ID - _ = cID.ReadFromV2(*newCID) - - require.Equal(t, newEpoch, newA.Epoch()) - require.Equal(t, newUsedSpace, newA.UsedSpace()) - cIDNew, set := newA.ContainerID() - require.True(t, set) - require.Equal(t, cID, cIDNew) - }) -} - -func TestUsedSpaceEncoding(t *testing.T) { - a := containertest.UsedSpaceAnnouncement() - - t.Run("binary", func(t *testing.T) { - data, err := a.Marshal() - require.NoError(t, err) - - a2 := container.NewAnnouncement() - require.NoError(t, a2.Unmarshal(data)) - - require.Equal(t, a, a2) - }) -} - -func TestUsedSpaceAnnouncement_ToV2(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *container.UsedSpaceAnnouncement - - require.Nil(t, x.ToV2()) - }) - - t.Run("default values", func(t *testing.T) { - announcement := container.NewAnnouncement() - - // check initial values - require.Zero(t, announcement.Epoch()) - require.Zero(t, announcement.UsedSpace()) - _, set := announcement.ContainerID() - require.False(t, set) - - // convert to v2 message - announcementV2 := announcement.ToV2() - - require.Zero(t, announcementV2.GetEpoch()) - require.Zero(t, announcementV2.GetUsedSpace()) - require.Nil(t, announcementV2.GetContainerID()) - }) -} - -func TestNewAnnouncementFromV2(t *testing.T) { - t.Run("from nil", func(t *testing.T) { - var x *containerv2.UsedSpaceAnnouncement - - require.Nil(t, container.NewAnnouncementFromV2(x)) - }) -} diff --git a/container/attribute.go b/container/attribute.go deleted file mode 100644 index 1fe4233..0000000 --- a/container/attribute.go +++ /dev/null @@ -1,139 +0,0 @@ -package container - -import ( - "github.com/nspcc-dev/neofs-api-go/v2/container" -) - -type ( - Attribute container.Attribute - Attributes []Attribute -) - -// NewAttribute creates and initializes blank Attribute. -// -// Defaults: -// - key: ""; -// - value: "". -func NewAttribute() *Attribute { - return NewAttributeFromV2(new(container.Attribute)) -} - -func (a *Attribute) SetKey(v string) { - (*container.Attribute)(a).SetKey(v) -} - -func (a *Attribute) SetValue(v string) { - (*container.Attribute)(a).SetValue(v) -} - -func (a *Attribute) Key() string { - return (*container.Attribute)(a).GetKey() -} - -func (a *Attribute) Value() string { - return (*container.Attribute)(a).GetValue() -} - -// NewAttributeFromV2 wraps protocol dependent version of -// Attribute message. -// -// Nil container.Attribute converts to nil. -func NewAttributeFromV2(v *container.Attribute) *Attribute { - return (*Attribute)(v) -} - -// ToV2 converts Attribute to v2 Attribute message. -// -// Nil Attribute converts to nil. -func (a *Attribute) ToV2() *container.Attribute { - return (*container.Attribute)(a) -} - -func NewAttributesFromV2(v []container.Attribute) Attributes { - if v == nil { - return nil - } - - attrs := make(Attributes, len(v)) - for i := range v { - attrs[i] = *NewAttributeFromV2(&v[i]) - } - - return attrs -} - -func (a Attributes) ToV2() []container.Attribute { - if a == nil { - return nil - } - - attrs := make([]container.Attribute, len(a)) - for i := range a { - attrs[i] = *a[i].ToV2() - } - - return attrs -} - -// sets value of the attribute by key. -func setAttribute(c *Container, key, value string) { - attrs := c.Attributes() - found := false - - for i := range attrs { - if attrs[i].Key() == key { - attrs[i].SetValue(value) - found = true - break - } - } - - if !found { - index := len(attrs) - attrs = append(attrs, Attribute{}) - attrs[index].SetKey(key) - attrs[index].SetValue(value) - } - - c.SetAttributes(attrs) -} - -// iterates over container attributes. Stops at f's true return. -// -// Handler must not be nil. -func iterateAttributes(c *Container, f func(*Attribute) bool) { - attrs := c.Attributes() - for i := range attrs { - if f(&attrs[i]) { - return - } - } -} - -// SetNativeNameWithZone sets container native name and its zone. -// -// Use SetNativeName to set default zone. -func SetNativeNameWithZone(c *Container, name, zone string) { - setAttribute(c, container.SysAttributeName, name) - setAttribute(c, container.SysAttributeZone, zone) -} - -// SetNativeName sets container native name with default zone (container). -func SetNativeName(c *Container, name string) { - SetNativeNameWithZone(c, name, container.SysAttributeZoneDefault) -} - -// GetNativeNameWithZone returns container native name and its zone. -func GetNativeNameWithZone(c *Container) (name string, zone string) { - iterateAttributes(c, func(a *Attribute) bool { - if key := a.Key(); key == container.SysAttributeName { - name = a.Value() - } else if key == container.SysAttributeZone { - zone = a.Value() - } - - return name != "" && zone != "" - }) - - return -} diff --git a/container/attribute_test.go b/container/attribute_test.go deleted file mode 100644 index 69b02ab..0000000 --- a/container/attribute_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package container_test - -import ( - "testing" - - containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-sdk-go/container" - "github.com/stretchr/testify/require" -) - -func TestAttribute(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *container.Attribute - - require.Nil(t, x.ToV2()) - }) - - t.Run("default values", func(t *testing.T) { - attr := container.NewAttribute() - - // check initial values - require.Empty(t, attr.Key()) - require.Empty(t, attr.Value()) - - // convert to v2 message - attrV2 := attr.ToV2() - require.Empty(t, attrV2.GetKey()) - require.Empty(t, attrV2.GetValue()) - }) - - const ( - key = "key" - value = "value" - ) - - attr := container.NewAttribute() - attr.SetKey(key) - attr.SetValue(value) - - require.Equal(t, key, attr.Key()) - require.Equal(t, value, attr.Value()) - - t.Run("test v2", func(t *testing.T) { - const ( - newKey = "newKey" - newValue = "newValue" - ) - - v2 := attr.ToV2() - require.Equal(t, key, v2.GetKey()) - require.Equal(t, value, v2.GetValue()) - - v2.SetKey(newKey) - v2.SetValue(newValue) - - newAttr := container.NewAttributeFromV2(v2) - - require.Equal(t, newKey, newAttr.Key()) - require.Equal(t, newValue, newAttr.Value()) - }) -} - -func TestAttributes(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x container.Attributes - - require.Nil(t, x.ToV2()) - - require.Nil(t, container.NewAttributesFromV2(nil)) - }) - - var ( - keys = []string{"key1", "key2", "key3"} - vals = []string{"val1", "val2", "val3"} - ) - - attrs := make(container.Attributes, len(keys)) - - for i := range keys { - attrs[i].SetKey(keys[i]) - attrs[i].SetValue(vals[i]) - } - - t.Run("test v2", func(t *testing.T) { - const postfix = "x" - - v2 := attrs.ToV2() - require.Len(t, v2, len(keys)) - - for i := range v2 { - k := v2[i].GetKey() - v := v2[i].GetValue() - - require.Equal(t, keys[i], k) - require.Equal(t, vals[i], v) - - v2[i].SetKey(k + postfix) - v2[i].SetValue(v + postfix) - } - - newAttrs := container.NewAttributesFromV2(v2) - require.Len(t, newAttrs, len(keys)) - - for i := range newAttrs { - require.Equal(t, keys[i]+postfix, newAttrs[i].Key()) - require.Equal(t, vals[i]+postfix, newAttrs[i].Value()) - } - }) -} - -func TestNewAttributeFromV2(t *testing.T) { - t.Run("from nil", func(t *testing.T) { - var x *containerv2.Attribute - - require.Nil(t, container.NewAttributeFromV2(x)) - }) -} - -func TestGetNameWithZone(t *testing.T) { - c := container.New() - - for _, item := range [...]struct { - name, zone string - }{ - {"name1", ""}, - {"name1", "zone1"}, - {"name2", "zone1"}, - {"name2", "zone2"}, - {"", "zone2"}, - {"", ""}, - } { - container.SetNativeNameWithZone(c, item.name, item.zone) - - name, zone := container.GetNativeNameWithZone(c) - - require.Equal(t, item.name, name, item.name) - require.Equal(t, item.zone, zone, item.zone) - } -} - -func TestSetNativeName(t *testing.T) { - c := container.New() - - const nameDefZone = "some name" - - container.SetNativeName(c, nameDefZone) - - name, zone := container.GetNativeNameWithZone(c) - - require.Equal(t, nameDefZone, name) - require.Equal(t, containerv2.SysAttributeZoneDefault, zone) -} diff --git a/container/container.go b/container/container.go index 3834cdd..4d84f27 100644 --- a/container/container.go +++ b/container/container.go @@ -1,7 +1,12 @@ package container import ( + "crypto/ecdsa" "crypto/sha256" + "errors" + "fmt" + "strconv" + "time" "github.com/google/uuid" "github.com/nspcc-dev/neofs-api-go/v2/container" @@ -9,193 +14,507 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/netmap" + subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/version" ) +// Container represents descriptor of the NeoFS container. Container logically +// stores NeoFS objects. Container is one of the basic and at the same time +// necessary data storage units in the NeoFS. Container includes data about the +// owner, rules for placing objects and other information necessary for the +// system functioning. +// +// Container type instances can represent different container states in the +// system, depending on the context. To create new container in NeoFS zero +// instance be created, initialized using Init method and filled using +// dedicated methods. Once container is saved in the NeoFS network, it can't be +// changed: containers stored in the system are immutable, and NeoFS is a CAS +// of containers that are identified by a fixed length value (see cid.ID type). +// Instances for existing containers can be initialized using decoding methods +// (e.g Unmarshal). +// +// Container is mutually compatible with github.com/nspcc-dev/neofs-api-go/v2/container.Container +// message. See ReadFromV2 / WriteToV2 methods. type Container struct { v2 container.Container } -// New creates, initializes and returns blank Container instance. +const ( + attributeName = "Name" + attributeTimestamp = "Timestamp" +) + +// reads Container from the container.Container message. If checkFieldPresence is set, +// returns an error on absence of any protocol-required field. +func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) error { + var err error + + ownerV2 := m.GetOwnerID() + if ownerV2 != nil { + var owner user.ID + + err = owner.ReadFromV2(*ownerV2) + if err != nil { + return fmt.Errorf("invalid owner: %w", err) + } + } else if checkFieldPresence { + return errors.New("missing owner") + } + + binNonce := m.GetNonce() + if len(binNonce) > 0 { + var nonce uuid.UUID + + err = nonce.UnmarshalBinary(binNonce) + if err != nil { + return fmt.Errorf("invalid nonce: %w", err) + } else if ver := nonce.Version(); ver != 4 { + return fmt.Errorf("invalid nonce UUID version %d", ver) + } + } else if checkFieldPresence { + return errors.New("missing nonce") + } + + ver := m.GetVersion() + if checkFieldPresence && ver == nil { + return errors.New("missing version") + } + + policyV2 := m.GetPlacementPolicy() + if policyV2 != nil { + var policy netmap.PlacementPolicy + + err = policy.ReadFromV2(*policyV2) + if err != nil { + return fmt.Errorf("invalid placement policy: %w", err) + } + } else if checkFieldPresence { + return errors.New("missing placement policy") + } + + attrs := m.GetAttributes() + mAttr := make(map[string]struct{}, len(attrs)) + var key, val string + var was bool + + for i := range attrs { + key = attrs[i].GetKey() + if key == "" { + return errors.New("empty attribute key") + } + + _, was = mAttr[key] + if was { + return fmt.Errorf("duplicated attribute %s", key) + } + + val = attrs[i].GetValue() + if val == "" { + return fmt.Errorf("empty attribute value %s", key) + } + + switch key { + case container.SysAttributeSubnet: + err = new(subnetid.ID).DecodeString(val) + case attributeTimestamp: + _, err = strconv.ParseInt(val, 10, 64) + } + + if err != nil { + return fmt.Errorf("invalid attribute value %s: %s (%w)", key, val, err) + } + + mAttr[key] = struct{}{} + } + + x.v2 = m + + return nil +} + +// ReadFromV2 reads Container from the container.Container message. Checks if the +// message conforms to NeoFS API V2 protocol. // -// Defaults: -// - token: nil; -// - sig: nil; -// - basicACL: Private; -// - version: version.Current; -// - nonce: random UUID; -// - attr: nil; -// - policy: nil; -// - ownerID: nil. -func New(opts ...Option) *Container { - cnrOptions := defaultContainerOptions() - - for i := range opts { - opts[i](&cnrOptions) - } - - cnr := new(Container) - cnr.SetNonceUUID(cnrOptions.nonce) - cnr.SetBasicACL(cnrOptions.acl) - - if cnrOptions.owner != nil { - cnr.SetOwnerID(cnrOptions.owner) - } - - if cnrOptions.policy != nil { - cnr.SetPlacementPolicy(cnrOptions.policy) - } - - cnr.SetAttributes(cnrOptions.attributes) - ver := version.Current() - cnr.SetVersion(&ver) - - return cnr +// See also WriteToV2. +func (x *Container) ReadFromV2(m container.Container) error { + return x.readFromV2(m, true) } -// ToV2 returns the v2 Container message. +// WriteToV2 writes Container into the container.Container message. +// The message MUST NOT be nil. // -// Nil Container converts to nil. -func (c *Container) ToV2() *container.Container { - if c == nil { - return nil - } - - return &c.v2 +// See also ReadFromV2. +func (x Container) WriteToV2(m *container.Container) { + *m = x.v2 } -// NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message. +// Marshal encodes Container into a binary format of the NeoFS API protocol +// (Protocol Buffers with direct field order). // -// Does not perform if message meets NeoFS API V2 specification. To do this -// use NewVerifiedFromV2 constructor. -func NewContainerFromV2(c *container.Container) *Container { - cnr := new(Container) +// See also Unmarshal. +func (x Container) Marshal() []byte { + return x.v2.StableMarshal(nil) +} - if c != nil { - cnr.v2 = *c +// Unmarshal decodes NeoFS API protocol binary format into the Container +// (Protocol Buffers with direct field order). Returns an error describing +// a format violation. +// +// See also Marshal. +func (x *Container) Unmarshal(data []byte) error { + var m container.Container + + err := m.Unmarshal(data) + if err != nil { + return err } - return cnr + return x.readFromV2(m, false) } -// CalculateID calculates container identifier -// based on its structure. -func CalculateID(c *Container) cid.ID { - var id cid.ID - id.SetSHA256(sha256.Sum256(c.ToV2().StableMarshal(nil))) - - return id +// MarshalJSON encodes Container into a JSON format of the NeoFS API protocol +// (Protocol Buffers JSON). +// +// See also UnmarshalJSON. +func (x Container) MarshalJSON() ([]byte, error) { + return x.v2.MarshalJSON() } -func (c *Container) Version() *version.Version { - var ver version.Version - if v2ver := c.v2.GetVersion(); v2ver != nil { - ver.ReadFromV2(*c.v2.GetVersion()) - } - return &ver +// UnmarshalJSON decodes NeoFS API protocol JSON format into the Container +// (Protocol Buffers JSON). Returns an error describing a format violation. +// +// See also MarshalJSON. +func (x *Container) UnmarshalJSON(data []byte) error { + return x.v2.UnmarshalJSON(data) } -func (c *Container) SetVersion(v *version.Version) { - var verV2 refs.Version - v.WriteToV2(&verV2) - c.v2.SetVersion(&verV2) -} +// Init initializes all internal data of the Container required by NeoFS API +// protocol. Init MUST be called when creating a new container. Init SHOULD NOT +// be called multiple times. Init SHOULD NOT be called if the Container instance +// is used for decoding only. +func (x *Container) Init() { + var ver refs.Version + version.Current().WriteToV2(&ver) -func (c *Container) OwnerID() *user.ID { - m := c.v2.GetOwnerID() - if m == nil { - return nil + x.v2.SetVersion(&ver) + + nonce, err := uuid.New().MarshalBinary() + if err != nil { + panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err)) } - var id user.ID - - _ = id.ReadFromV2(*m) - - return &id + x.v2.SetNonce(nonce) } -func (c *Container) SetOwnerID(v *user.ID) { +// SetOwner specifies the owner of the Container. Each Container has exactly +// one owner, so SetOwner MUST be called for instances to be saved in the +// NeoFS. +// +// See also Owner. +func (x *Container) SetOwner(owner user.ID) { var m refs.OwnerID - v.WriteToV2(&m) + owner.WriteToV2(&m) - c.v2.SetOwnerID(&m) + x.v2.SetOwnerID(&m) } -// Returns container nonce in UUID format. +// Owner returns owner of the Container set using SetOwner. // -// Returns error if container nonce is not a valid UUID. -func (c *Container) NonceUUID() (uuid.UUID, error) { - return uuid.FromBytes(c.v2.GetNonce()) -} +// Zero Container has no owner which is incorrect according to NeoFS API +// protocol. +func (x Container) Owner() (res user.ID) { + m := x.v2.GetOwnerID() + if m != nil { + err := res.ReadFromV2(*m) + if err != nil { + panic(fmt.Sprintf("unexpected error from user.ID.ReadFromV2: %v", err)) + } + } -// SetNonceUUID sets container nonce as UUID. -func (c *Container) SetNonceUUID(v uuid.UUID) { - data, _ := v.MarshalBinary() - c.v2.SetNonce(data) -} - -func (c *Container) BasicACL() (res acl.Basic) { - res.FromBits(c.v2.GetBasicACL()) return } -func (c *Container) SetBasicACL(v acl.Basic) { - c.v2.SetBasicACL(v.Bits()) +// SetBasicACL specifies basic part of the Container ACL. Basic ACL is used +// to control access inside container storage. +// +// See also BasicACL. +func (x *Container) SetBasicACL(basicACL acl.Basic) { + x.v2.SetBasicACL(basicACL.Bits()) } -func (c *Container) Attributes() Attributes { - return NewAttributesFromV2(c.v2.GetAttributes()) +// BasicACL returns basic ACL set using SetBasicACL. +// +// Zero Container has zero basic ACL which structurally correct but doesn't +// make sense since it denies any access to any party. +func (x Container) BasicACL() (res acl.Basic) { + res.FromBits(x.v2.GetBasicACL()) + return } -func (c *Container) SetAttributes(v Attributes) { - c.v2.SetAttributes(v.ToV2()) +// SetPlacementPolicy sets placement policy for the objects within the Container. +// NeoFS storage layer strives to follow the specified policy. +// +// See also PlacementPolicy. +func (x *Container) SetPlacementPolicy(policy netmap.PlacementPolicy) { + var m v2netmap.PlacementPolicy + policy.WriteToV2(&m) + + x.v2.SetPlacementPolicy(&m) } -func (c *Container) PlacementPolicy() *netmap.PlacementPolicy { - m := c.v2.GetPlacementPolicy() - if m == nil { - return nil +// PlacementPolicy returns placement policy set using SetPlacementPolicy. +// +// Zero Container has no placement policy which is incorrect according to +// NeoFS API protocol. +func (x Container) PlacementPolicy() (res netmap.PlacementPolicy) { + m := x.v2.GetPlacementPolicy() + if m != nil { + err := res.ReadFromV2(*m) + if err != nil { + panic(fmt.Sprintf("unexpected error from PlacementPolicy.ReadFromV2: %v", err)) + } } - var p netmap.PlacementPolicy - // FIXME(@cthulhu-rider): #225 handle error - err := p.ReadFromV2(*m) - if err != nil { - panic(err) + return +} + +// SetAttribute sets Container attribute value by key. Both key and value +// MUST NOT be empty. Attributes set by the creator (owner) are most commonly +// ignored by the NeoFS system and used for application layer. Some attributes +// are so-called system or well-known attributes: they are reserved for system +// needs. System attributes SHOULD NOT be modified using SetAttribute, use +// corresponding methods/functions. List of the reserved keys is documented +// in the particular protocol version. +// +// SetAttribute overwrites existing attribute value. +// +// See also Attribute, IterateAttributes. +func (x *Container) SetAttribute(key, value string) { + if key == "" { + panic("empty attribute key") + } else if value == "" { + panic("empty attribute value") } - return &p -} + attrs := x.v2.GetAttributes() + ln := len(attrs) -func (c *Container) SetPlacementPolicy(v *netmap.PlacementPolicy) { - var m *v2netmap.PlacementPolicy - - if v != nil { - m = new(v2netmap.PlacementPolicy) - v.WriteToV2(m) + for i := 0; i < ln; i++ { + if attrs[i].GetKey() == key { + attrs[i].SetValue(value) + return + } } - c.v2.SetPlacementPolicy(m) + attrs = append(attrs, container.Attribute{}) + attrs[ln].SetKey(key) + attrs[ln].SetValue(value) + + x.v2.SetAttributes(attrs) } -// Marshal marshals Container into a protobuf binary form. -func (c *Container) Marshal() ([]byte, error) { - return c.v2.StableMarshal(nil), nil +// Attribute reads value of the Container attribute by key. Empty result means +// attribute absence. +// +// See also SetAttribute, IterateAttributes. +func (x Container) Attribute(key string) string { + attrs := x.v2.GetAttributes() + for i := range attrs { + if attrs[i].GetKey() == key { + return attrs[i].GetValue() + } + } + + return "" } -// Unmarshal unmarshals protobuf binary representation of Container. -func (c *Container) Unmarshal(data []byte) error { - return c.v2.Unmarshal(data) +// IterateAttributes iterates over all Container attributes and passes them +// into f. The handler MUST NOT be nil. +// +// See also SetAttribute, Attribute. +func (x Container) IterateAttributes(f func(key, val string)) { + attrs := x.v2.GetAttributes() + for i := range attrs { + f(attrs[i].GetKey(), attrs[i].GetValue()) + } } -// MarshalJSON encodes Container to protobuf JSON format. -func (c *Container) MarshalJSON() ([]byte, error) { - return c.v2.MarshalJSON() +// SetName sets human-readable name of the Container. Name MUST NOT be empty. +// +// See also Name. +func SetName(cnr *Container, name string) { + cnr.SetAttribute(attributeName, name) } -// UnmarshalJSON decodes Container from protobuf JSON format. -func (c *Container) UnmarshalJSON(data []byte) error { - return c.v2.UnmarshalJSON(data) +// Name returns container name set using SetName. +// +// Zero Container has no name. +func Name(cnr Container) string { + return cnr.Attribute(attributeName) +} + +// SetCreationTime writes container's creation time in Unix Timestamp format. +// +// See also CreatedAt. +func SetCreationTime(cnr *Container, t time.Time) { + cnr.SetAttribute(attributeTimestamp, strconv.FormatInt(t.Unix(), 10)) +} + +// CreatedAt returns container's creation time set using SetCreationTime. +// +// Zero Container has zero timestamp (in seconds). +func CreatedAt(cnr Container) time.Time { + var sec int64 + + attr := cnr.Attribute(attributeTimestamp) + if attr != "" { + var err error + + sec, err = strconv.ParseInt(cnr.Attribute(attributeTimestamp), 10, 64) + if err != nil { + panic(fmt.Sprintf("parse container timestamp: %v", err)) + } + } + + return time.Unix(sec, 0) +} + +// SetSubnet places the Container on the specified NeoFS subnet. If called, +// container nodes will only be selected from the given subnet, otherwise from +// the entire network. +func SetSubnet(cnr *Container, subNet subnetid.ID) { + cnr.SetAttribute(container.SysAttributeSubnet, subNet.EncodeToString()) +} + +// Subnet return container subnet set using SetSubnet. +// +// Zero Container is bound to zero subnet. +func Subnet(cnr Container) (res subnetid.ID) { + val := cnr.Attribute(container.SysAttributeSubnet) + if val != "" { + err := res.DecodeString(val) + if err != nil { + panic(fmt.Sprintf("invalid subnet attribute: %s (%v)", val, err)) + } + } + + return +} + +const attributeHomoHashEnabled = "true" + +// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the +// Container data. +// +// See also IsHomomorphicHashingDisabled. +func DisableHomomorphicHashing(cnr *Container) { + cnr.SetAttribute(container.SysAttributeHomomorphicHashing, attributeHomoHashEnabled) +} + +// IsHomomorphicHashingDisabled checks if DisableHomomorphicHashing was called. +// +// Zero Container has enabled hashing. +func IsHomomorphicHashingDisabled(cnr Container) bool { + return cnr.Attribute(container.SysAttributeHomomorphicHashing) == attributeHomoHashEnabled +} + +// Domain represents information about container domain registered in the NNS +// contract deployed in the NeoFS network. +type Domain struct { + name, zone string +} + +// SetName sets human-friendly container domain name. +func (x *Domain) SetName(name string) { + x.name = name +} + +// Name returns name set using SetName. +// +// Zero Domain has zero name. +func (x Domain) Name() string { + return x.name +} + +// SetZone sets zone which is used as a TLD of a domain name in NNS contract. +func (x *Domain) SetZone(zone string) { + x.zone = zone +} + +// Zone returns domain zone set using SetZone. +// +// Zero Domain has "container" zone. +func (x Domain) Zone() string { + if x.zone != "" { + return x.zone + } + + return "container" +} + +// WriteDomain writes Domain into the Container. Name MUST NOT be empty. +func WriteDomain(cnr *Container, domain Domain) { + cnr.SetAttribute(container.SysAttributeName, domain.Name()) + cnr.SetAttribute(container.SysAttributeZone, domain.Zone()) +} + +// ReadDomain reads Domain from the Container. Returns value with empty name +// if domain is not specified. +func ReadDomain(cnr Container) (res Domain) { + name := cnr.Attribute(container.SysAttributeName) + if name != "" { + res.SetName(name) + res.SetZone(cnr.Attribute(container.SysAttributeZone)) + } + + return +} + +// CalculateSignature calculates signature of the Container using provided signer +// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature +// is expected to be called after all the Container data is filled and before +// saving the Container in the NeoFS network. Note that мany subsequent change +// will most likely break the signature. +// +// See also VerifySignature. +func CalculateSignature(dst *neofscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error { + return dst.Calculate(neofsecdsa.SignerRFC6979(signer), cnr.Marshal()) +} + +// VerifySignature verifies Container signature calculated using CalculateSignature. +// Result means signature correctness. +func VerifySignature(sig neofscrypto.Signature, cnr Container) bool { + return sig.Verify(cnr.Marshal()) +} + +// CalculateIDFromBinary calculates identifier of the binary-encoded container +// in CAS of the NeoFS containers and writes it into dst. ID instance MUST NOT +// be nil. +// +// See also CalculateID, AssertID. +func CalculateIDFromBinary(dst *cid.ID, cnr []byte) { + dst.SetSHA256(sha256.Sum256(cnr)) +} + +// CalculateID encodes the given Container and passes the result into +// CalculateIDFromBinary. +// +// See also Container.Marshal, AssertID. +func CalculateID(dst *cid.ID, cnr Container) { + CalculateIDFromBinary(dst, cnr.Marshal()) +} + +// AssertID checks if the given Container matches its identifier in CAS of the +// NeoFS containers. +// +// See also CalculateID. +func AssertID(id cid.ID, cnr Container) bool { + var id2 cid.ID + CalculateID(&id2, cnr) + + return id2.Equals(id) } diff --git a/container/container_test.go b/container/container_test.go index 1d5ecef..c5ed84e 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -1,117 +1,351 @@ package container_test import ( + "crypto/sha256" + "strconv" "testing" + "time" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + v2container "github.com/nspcc-dev/neofs-api-go/v2/container" + v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/container" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" + subnetid "github.com/nspcc-dev/neofs-sdk-go/subnet/id" + subnetidtest "github.com/nspcc-dev/neofs-sdk-go/subnet/id/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/nspcc-dev/neofs-sdk-go/version" - versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" "github.com/stretchr/testify/require" ) -func TestNewContainer(t *testing.T) { - c := container.New() - - nonce := uuid.New() - - ownerID := usertest.ID() - policy := netmaptest.PlacementPolicy() - - c.SetBasicACL(acl.PublicRW) - - attrs := containertest.Attributes() - c.SetAttributes(attrs) - - c.SetPlacementPolicy(&policy) - c.SetNonceUUID(nonce) - c.SetOwnerID(ownerID) - - ver := versiontest.Version() - c.SetVersion(&ver) - - v2 := c.ToV2() - newContainer := container.NewContainerFromV2(v2) - - require.EqualValues(t, newContainer.PlacementPolicy(), &policy) - require.EqualValues(t, newContainer.Attributes(), attrs) - require.EqualValues(t, newContainer.BasicACL(), acl.PublicRW) - - newNonce, err := newContainer.NonceUUID() - require.NoError(t, err) - - require.EqualValues(t, newNonce, nonce) - require.EqualValues(t, newContainer.OwnerID(), ownerID) - require.EqualValues(t, *newContainer.Version(), ver) -} - -func TestContainerEncoding(t *testing.T) { - c := containertest.Container() +func TestPlacementPolicyEncoding(t *testing.T) { + v := containertest.Container() t.Run("binary", func(t *testing.T) { - data, err := c.Marshal() - require.NoError(t, err) + var v2 container.Container + require.NoError(t, v2.Unmarshal(v.Marshal())) - c2 := container.New() - require.NoError(t, c2.Unmarshal(data)) - - require.Equal(t, c, c2) + require.Equal(t, v, v2) }) t.Run("json", func(t *testing.T) { - data, err := c.MarshalJSON() + data, err := v.MarshalJSON() require.NoError(t, err) - c2 := container.New() - require.NoError(t, c2.UnmarshalJSON(data)) + var v2 container.Container + require.NoError(t, v2.UnmarshalJSON(data)) - require.Equal(t, c, c2) + require.Equal(t, v, v2) }) } -func TestContainer_ToV2(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *container.Container +func TestContainer_Init(t *testing.T) { + val := containertest.Container() - require.Nil(t, x.ToV2()) - }) + val.Init() - t.Run("default values", func(t *testing.T) { - cnt := container.New() + var msg v2container.Container + val.WriteToV2(&msg) - // check initial values - require.Nil(t, cnt.Attributes()) - require.Nil(t, cnt.PlacementPolicy()) - require.Nil(t, cnt.OwnerID()) + binNonce := msg.GetNonce() - require.EqualValues(t, acl.Private, cnt.BasicACL()) - require.Equal(t, version.Current(), *cnt.Version()) + var nonce uuid.UUID + require.NoError(t, nonce.UnmarshalBinary(binNonce)) + require.EqualValues(t, 4, nonce.Version()) - nonce, err := cnt.NonceUUID() - require.NoError(t, err) - require.NotNil(t, nonce) + verV2 := msg.GetVersion() + require.NotNil(t, verV2) - // convert to v2 message - cntV2 := cnt.ToV2() + var ver version.Version + ver.ReadFromV2(*verV2) - nonceV2, err := uuid.FromBytes(cntV2.GetNonce()) - require.NoError(t, err) + require.Equal(t, version.Current(), ver) - require.Equal(t, nonce.String(), nonceV2.String()) + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) - require.Nil(t, cntV2.GetAttributes()) - require.Nil(t, cntV2.GetPlacementPolicy()) - require.Nil(t, cntV2.GetOwnerID()) - - require.EqualValues(t, 0x1C8C8CCC, cntV2.GetBasicACL()) - - var verV2 refs.Version - version.Current().WriteToV2(&verV2) - require.Equal(t, verV2, *cntV2.GetVersion()) - }) + require.Equal(t, val, val2) +} + +func TestContainer_Owner(t *testing.T) { + var val container.Container + + require.Zero(t, val.Owner()) + + val = containertest.Container() + + owner := *usertest.ID() + + val.SetOwner(owner) + + var msg v2container.Container + val.WriteToV2(&msg) + + var msgOwner refs.OwnerID + owner.WriteToV2(&msgOwner) + + require.Equal(t, &msgOwner, msg.GetOwnerID()) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.True(t, val2.Owner().Equals(owner)) +} + +func TestContainer_BasicACL(t *testing.T) { + var val container.Container + + require.Zero(t, val.BasicACL()) + + val = containertest.Container() + + basicACL := containertest.BasicACL() + val.SetBasicACL(basicACL) + + var msg v2container.Container + val.WriteToV2(&msg) + + require.EqualValues(t, basicACL.Bits(), msg.GetBasicACL()) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, basicACL, val2.BasicACL()) +} + +func TestContainer_PlacementPolicy(t *testing.T) { + var val container.Container + + require.Zero(t, val.PlacementPolicy()) + + val = containertest.Container() + + pp := netmaptest.PlacementPolicy() + val.SetPlacementPolicy(pp) + + var msgPolicy v2netmap.PlacementPolicy + pp.WriteToV2(&msgPolicy) + + var msg v2container.Container + val.WriteToV2(&msg) + + require.Equal(t, &msgPolicy, msg.GetPlacementPolicy()) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, pp, val2.PlacementPolicy()) +} + +func assertContainsAttribute(t *testing.T, m v2container.Container, key, val string) { + var msgAttr v2container.Attribute + + msgAttr.SetKey(key) + msgAttr.SetValue(val) + require.Contains(t, m.GetAttributes(), msgAttr) +} + +func TestContainer_Attribute(t *testing.T) { + const attrKey1, attrKey2 = "key1", "key2" + const attrVal1, attrVal2 = "val1", "val2" + + val := containertest.Container() + + val.SetAttribute(attrKey1, attrVal1) + val.SetAttribute(attrKey2, attrVal2) + + var msg v2container.Container + val.WriteToV2(&msg) + + require.GreaterOrEqual(t, len(msg.GetAttributes()), 2) + assertContainsAttribute(t, msg, attrKey1, attrVal1) + assertContainsAttribute(t, msg, attrKey2, attrVal2) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, attrVal1, val2.Attribute(attrKey1)) + require.Equal(t, attrVal2, val2.Attribute(attrKey2)) + + m := map[string]string{} + + val2.IterateAttributes(func(key, val string) { + m[key] = val + }) + + require.GreaterOrEqual(t, len(m), 2) + require.Equal(t, attrVal1, m[attrKey1]) + require.Equal(t, attrVal2, m[attrKey2]) + + val2.SetAttribute(attrKey1, attrVal1+"_") + require.Equal(t, attrVal1+"_", val2.Attribute(attrKey1)) +} + +func TestSetName(t *testing.T) { + var val container.Container + + require.Panics(t, func() { + container.SetName(&val, "") + }) + + val = containertest.Container() + + const name = "some name" + + container.SetName(&val, name) + + var msg v2container.Container + val.WriteToV2(&msg) + + assertContainsAttribute(t, msg, "Name", name) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, name, container.Name(val2)) +} + +func TestSetCreationTime(t *testing.T) { + var val container.Container + + require.Zero(t, container.CreatedAt(val).Unix()) + + val = containertest.Container() + + creat := time.Now() + + container.SetCreationTime(&val, creat) + + var msg v2container.Container + val.WriteToV2(&msg) + + assertContainsAttribute(t, msg, "Timestamp", strconv.FormatInt(creat.Unix(), 10)) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, creat.Unix(), container.CreatedAt(val2).Unix()) +} + +func TestSetSubnet(t *testing.T) { + var val container.Container + + require.True(t, subnetid.IsZero(container.Subnet(val))) + + val = containertest.Container() + + sub := subnetidtest.ID() + + container.SetSubnet(&val, sub) + + var msg v2container.Container + val.WriteToV2(&msg) + + assertContainsAttribute(t, msg, v2container.SysAttributeSubnet, sub.EncodeToString()) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, sub, container.Subnet(val)) +} + +func TestDisableHomomorphicHashing(t *testing.T) { + var val container.Container + + require.False(t, container.IsHomomorphicHashingDisabled(val)) + + val = containertest.Container() + + container.DisableHomomorphicHashing(&val) + + var msg v2container.Container + val.WriteToV2(&msg) + + assertContainsAttribute(t, msg, v2container.SysAttributePrefix+"DISABLE_HOMOMORPHIC_HASHING", "true") + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.True(t, container.IsHomomorphicHashingDisabled(val2)) +} + +func TestWriteDomain(t *testing.T) { + var val container.Container + + require.Zero(t, container.ReadDomain(val).Name()) + + val = containertest.Container() + + const name = "domain name" + + var d container.Domain + d.SetName(name) + + container.WriteDomain(&val, d) + + var msg v2container.Container + val.WriteToV2(&msg) + + assertContainsAttribute(t, msg, v2container.SysAttributeName, name) + assertContainsAttribute(t, msg, v2container.SysAttributeZone, "container") + + const zone = "domain zone" + + d.SetZone(zone) + + container.WriteDomain(&val, d) + + val.WriteToV2(&msg) + + assertContainsAttribute(t, msg, v2container.SysAttributeZone, zone) + + var val2 container.Container + require.NoError(t, val2.ReadFromV2(msg)) + + require.Equal(t, d, container.ReadDomain(val2)) +} + +func TestCalculateID(t *testing.T) { + val := containertest.Container() + + require.False(t, container.AssertID(cidtest.ID(), val)) + + var id cid.ID + container.CalculateID(&id, val) + + var msg refs.ContainerID + id.WriteToV2(&msg) + + h := sha256.Sum256(val.Marshal()) + require.Equal(t, h[:], msg.GetValue()) + + var id2 cid.ID + require.NoError(t, id2.ReadFromV2(msg)) + + require.True(t, container.AssertID(id2, val)) +} + +func TestCalculateSignature(t *testing.T) { + key, err := keys.NewPrivateKey() + require.NoError(t, err) + + val := containertest.Container() + + var sig neofscrypto.Signature + + require.NoError(t, container.CalculateSignature(&sig, val, key.PrivateKey)) + + var msg refs.Signature + sig.WriteToV2(&msg) + + var sig2 neofscrypto.Signature + sig2.ReadFromV2(msg) + + require.True(t, container.VerifySignature(sig2, val)) } diff --git a/container/doc.go b/container/doc.go new file mode 100644 index 0000000..79646a0 --- /dev/null +++ b/container/doc.go @@ -0,0 +1,46 @@ +/* +Package container provides functionality related to the NeoFS containers. + +The base type is Container. To create new container in the NeoFS network +Container instance should be initialized + var cnr Container + cnr.Init() + // fill all the fields + + // encode cnr and send + +After the container is persisted in the NeoFS network, applications can process +it using the instance of Container types + // recv binary container + + var cnr Container + + err := cnr.Unmarshal(bin) + // ... + + // process the container data + +Instances can be also used to process NeoFS API V2 protocol messages +(see neo.fs.v2.container package in https://github.com/nspcc-dev/neofs-api). + +On client side: + import "github.com/nspcc-dev/neofs-api-go/v2/container" + + var msg container.Container + cnr.WriteToV2(&msg) + + // send msg + +On server side: + // recv msg + + var cnr Container + cnr.ReadFromV2(msg) + + // process cnr + +Using package types in an application is recommended to potentially work with +different protocol versions with which these types are compatible. + +*/ +package container diff --git a/container/opts.go b/container/opts.go deleted file mode 100644 index 52fdebd..0000000 --- a/container/opts.go +++ /dev/null @@ -1,89 +0,0 @@ -package container - -import ( - "crypto/ecdsa" - - "github.com/google/uuid" - "github.com/nspcc-dev/neofs-sdk-go/container/acl" - "github.com/nspcc-dev/neofs-sdk-go/netmap" - "github.com/nspcc-dev/neofs-sdk-go/user" -) - -type ( - Option func(*containerOptions) - - containerOptions struct { - acl acl.Basic - policy *netmap.PlacementPolicy - attributes Attributes - owner *user.ID - nonce uuid.UUID - } -) - -func defaultContainerOptions() containerOptions { - rand, err := uuid.NewRandom() - if err != nil { - panic("can't create new random " + err.Error()) - } - - return containerOptions{ - acl: acl.Private, - nonce: rand, - } -} - -func WithPublicBasicACL() Option { - return func(option *containerOptions) { - option.acl = acl.PublicRW - } -} - -func WithReadOnlyBasicACL() Option { - return func(option *containerOptions) { - option.acl = acl.PublicRO - } -} - -func WithCustomBasicACL(acl acl.Basic) Option { - return func(option *containerOptions) { - option.acl = acl - } -} - -func WithNonce(nonce uuid.UUID) Option { - return func(option *containerOptions) { - option.nonce = nonce - } -} - -func WithOwnerID(id *user.ID) Option { - return func(option *containerOptions) { - option.owner = id - } -} - -func WithOwnerPublicKey(pub *ecdsa.PublicKey) Option { - return func(option *containerOptions) { - if option.owner == nil { - option.owner = new(user.ID) - } - - user.IDFromKey(option.owner, *pub) - } -} - -func WithPolicy(policy *netmap.PlacementPolicy) Option { - return func(option *containerOptions) { - option.policy = policy - } -} - -func WithAttribute(key, value string) Option { - return func(option *containerOptions) { - index := len(option.attributes) - option.attributes = append(option.attributes, Attribute{}) - option.attributes[index].SetKey(key) - option.attributes[index].SetValue(value) - } -} diff --git a/container/size.go b/container/size.go new file mode 100644 index 0000000..2402a5f --- /dev/null +++ b/container/size.go @@ -0,0 +1,104 @@ +package container + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neofs-api-go/v2/container" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" +) + +// SizeEstimation groups information about estimation of the size of the data +// stored in the NeoFS container. +// +// SizeEstimation is mutually compatible with github.com/nspcc-dev/neofs-api-go/v2/container.UsedSpaceAnnouncement +// message. See ReadFromV2 / WriteToV2 methods. +type SizeEstimation struct { + m container.UsedSpaceAnnouncement +} + +// ReadFromV2 reads SizeEstimation from the container.UsedSpaceAnnouncement message. +// Checks if the message conforms to NeoFS API V2 protocol. +// +// See also WriteToV2. +func (x *SizeEstimation) ReadFromV2(m container.UsedSpaceAnnouncement) error { + cnrV2 := m.GetContainerID() + if cnrV2 == nil { + return errors.New("missing container") + } + + var cnr cid.ID + + err := cnr.ReadFromV2(*cnrV2) + if err != nil { + return fmt.Errorf("invalid container: %w", err) + } + + x.m = m + + return nil +} + +// WriteToV2 writes SizeEstimation into the container.UsedSpaceAnnouncement message. +// The message MUST NOT be nil. +// +// See also ReadFromV2. +func (x SizeEstimation) WriteToV2(m *container.UsedSpaceAnnouncement) { + *m = x.m +} + +// SetEpoch sets epoch when estimation of the container data size was calculated. +// +// See also Epoch. +func (x *SizeEstimation) SetEpoch(epoch uint64) { + x.m.SetEpoch(epoch) +} + +// Epoch return epoch set using SetEpoch. +// +// Zero SizeEstimation represents estimation in zero epoch. +func (x SizeEstimation) Epoch() uint64 { + return x.m.GetEpoch() +} + +// SetContainer specifies the container for which the amount of data is estimated. +// Required by the NeoFS API protocol. +// +// See also Container. +func (x *SizeEstimation) SetContainer(cnr cid.ID) { + var cidV2 refs.ContainerID + cnr.WriteToV2(&cidV2) + + x.m.SetContainerID(&cidV2) +} + +// Container returns container set using SetContainer. +// +// Zero SizeEstimation is not bound to any container (returns zero) which is +// incorrect according to NeoFS API protocol. +func (x SizeEstimation) Container() (res cid.ID) { + m := x.m.GetContainerID() + if m != nil { + err := res.ReadFromV2(*m) + if err != nil { + panic(fmt.Errorf("unexpected error from cid.ID.ReadFromV2: %w", err)) + } + } + + return +} + +// SetValue sets estimated amount of data (in bytes) in the specified container. +// +// See also Value. +func (x *SizeEstimation) SetValue(value uint64) { + x.m.SetUsedSpace(value) +} + +// Value returns data size estimation set using SetValue. +// +// Zero SizeEstimation has zero value. +func (x SizeEstimation) Value() uint64 { + return x.m.GetUsedSpace() +} diff --git a/container/size_test.go b/container/size_test.go new file mode 100644 index 0000000..97cf49d --- /dev/null +++ b/container/size_test.go @@ -0,0 +1,94 @@ +package container_test + +import ( + "crypto/sha256" + "testing" + + v2container "github.com/nspcc-dev/neofs-api-go/v2/container" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-sdk-go/container" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/stretchr/testify/require" +) + +func TestSizeEstimation_Epoch(t *testing.T) { + var val container.SizeEstimation + + require.Zero(t, val.Epoch()) + + const epoch = 123 + + val.SetEpoch(epoch) + require.EqualValues(t, epoch, val.Epoch()) + + var msg v2container.UsedSpaceAnnouncement + val.WriteToV2(&msg) + + require.EqualValues(t, epoch, msg.GetEpoch()) +} + +func TestSizeEstimation_Container(t *testing.T) { + var val container.SizeEstimation + + require.Zero(t, val.Container()) + + cnr := cidtest.ID() + + val.SetContainer(cnr) + require.True(t, val.Container().Equals(cnr)) + + var msg v2container.UsedSpaceAnnouncement + val.WriteToV2(&msg) + + var msgCnr refs.ContainerID + cnr.WriteToV2(&msgCnr) + + require.Equal(t, &msgCnr, msg.GetContainerID()) +} + +func TestSizeEstimation_Value(t *testing.T) { + var val container.SizeEstimation + + require.Zero(t, val.Value()) + + const value = 876 + + val.SetValue(value) + require.EqualValues(t, value, val.Value()) + + var msg v2container.UsedSpaceAnnouncement + val.WriteToV2(&msg) + + require.EqualValues(t, value, msg.GetUsedSpace()) +} + +func TestSizeEstimation_ReadFromV2(t *testing.T) { + const epoch = 654 + const value = 903 + var cnrMsg refs.ContainerID + + var msg v2container.UsedSpaceAnnouncement + + var val container.SizeEstimation + + require.Error(t, val.ReadFromV2(msg)) + + msg.SetContainerID(&cnrMsg) + + require.Error(t, val.ReadFromV2(msg)) + + cnrMsg.SetValue(make([]byte, sha256.Size)) + + var cnr cid.ID + require.NoError(t, cnr.ReadFromV2(cnrMsg)) + + msg.SetEpoch(epoch) + msg.SetUsedSpace(value) + + require.NoError(t, val.ReadFromV2(msg)) + + require.EqualValues(t, epoch, val.Epoch()) + require.EqualValues(t, value, val.Value()) + require.EqualValues(t, cnr, val.Container()) +} diff --git a/container/test/generate.go b/container/test/generate.go index 8bd7a58..aa68dfc 100644 --- a/container/test/generate.go +++ b/container/test/generate.go @@ -8,46 +8,26 @@ import ( cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" - versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" ) -// Attribute returns random container.Attribute. -func Attribute() *container.Attribute { - x := container.NewAttribute() - - x.SetKey("key") - x.SetValue("value") - - return x -} - -// Attributes returns random container.Attributes. -func Attributes() container.Attributes { - return container.Attributes{*Attribute(), *Attribute()} -} - // Container returns random container.Container. -func Container() *container.Container { - x := container.New() - ver := versiontest.Version() +func Container() (x container.Container) { + owner := usertest.ID() - x.SetVersion(&ver) - x.SetAttributes(Attributes()) - x.SetOwnerID(usertest.ID()) + x.Init() + x.SetAttribute("some attribute", "value") + x.SetOwner(*owner) x.SetBasicACL(BasicACL()) - p := netmaptest.PlacementPolicy() - x.SetPlacementPolicy(&p) + x.SetPlacementPolicy(netmaptest.PlacementPolicy()) return x } -// UsedSpaceAnnouncement returns random container.UsedSpaceAnnouncement. -func UsedSpaceAnnouncement() *container.UsedSpaceAnnouncement { - x := container.NewAnnouncement() - - x.SetContainerID(cidtest.ID()) - x.SetEpoch(55) - x.SetUsedSpace(999) +// SizeEstimation returns random container.SizeEstimation. +func SizeEstimation() (x container.SizeEstimation) { + x.SetContainer(cidtest.ID()) + x.SetEpoch(rand.Uint64()) + x.SetValue(rand.Uint64()) return x } diff --git a/container/wellknown_attributes.go b/container/wellknown_attributes.go deleted file mode 100644 index 241c745..0000000 --- a/container/wellknown_attributes.go +++ /dev/null @@ -1,11 +0,0 @@ -package container - -const ( - // AttributeName is an attribute key that is commonly used to denote - // human-friendly name. - AttributeName = "Name" - - // AttributeTimestamp is an attribute key that is commonly used to denote - // user-defined local time of container creation in Unix Timestamp format. - AttributeTimestamp = "Timestamp" -) diff --git a/go.mod b/go.mod index 6bb7a8f..b0186a0 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/mr-tron/base58 v1.2.0 github.com/nspcc-dev/hrw v1.0.9 github.com/nspcc-dev/neo-go v0.98.2 - github.com/nspcc-dev/neofs-api-go/v2 v2.12.3-0.20220621170933-dd233c3fbc84 + github.com/nspcc-dev/neofs-api-go/v2 v2.12.3-0.20220630100506-c6f7ab3ef1bf github.com/nspcc-dev/neofs-contract v0.15.1 github.com/nspcc-dev/tzhash v1.5.2 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index 9d5e583..20ef995 100644 --- a/go.sum +++ b/go.sum @@ -182,8 +182,8 @@ github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321113211-526c423a6152 h1:JK github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321113211-526c423a6152/go.mod h1:QBE0I30F2kOAISNpT5oks82yF4wkkUq3SCfI3Hqgx/Y= github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211201134523-3604d96f3fe1/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs= github.com/nspcc-dev/neofs-api-go/v2 v2.11.1/go.mod h1:oS8dycEh8PPf2Jjp6+8dlwWyEv2Dy77h/XhhcdxYEFs= -github.com/nspcc-dev/neofs-api-go/v2 v2.12.3-0.20220621170933-dd233c3fbc84 h1:4tZSQ2DL/oatbte35+vDSt2nYpQ0G2et1DrpxodGwRM= -github.com/nspcc-dev/neofs-api-go/v2 v2.12.3-0.20220621170933-dd233c3fbc84/go.mod h1:73j09Xa7I2zQbM3HCvAHnDHPYiiWnEHa1d6Z6RDMBLU= +github.com/nspcc-dev/neofs-api-go/v2 v2.12.3-0.20220630100506-c6f7ab3ef1bf h1:QRPx+DdyN2KmJ5/oDYH4c86Bl81d1ZacQL5Q9IC9wZA= +github.com/nspcc-dev/neofs-api-go/v2 v2.12.3-0.20220630100506-c6f7ab3ef1bf/go.mod h1:73j09Xa7I2zQbM3HCvAHnDHPYiiWnEHa1d6Z6RDMBLU= github.com/nspcc-dev/neofs-contract v0.15.1 h1:1r27t4SGKF7W1PRPOIfircEXHvALThNYNagT+SIabcA= github.com/nspcc-dev/neofs-contract v0.15.1/go.mod h1:kxO5ZTqdzFnRM5RMvM+Fhd+3GGrJo6AmG2ZyA9OCqqQ= github.com/nspcc-dev/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA= diff --git a/pool/pool.go b/pool/pool.go index 04d3a49..b51d21a 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -133,7 +133,10 @@ func (c *clientWrapper) containerGet(ctx context.Context, prm PrmContainerGet) ( if err != nil { return nil, err } - return res.Container(), nil + + cnr := res.Container() + + return &cnr, nil } func (c *clientWrapper) containerList(ctx context.Context, prm PrmContainerList) ([]cid.ID, error) {