diff --git a/pkg/util/attributes/parser.go b/pkg/util/attributes/parser.go new file mode 100644 index 0000000000..04aecaf0da --- /dev/null +++ b/pkg/util/attributes/parser.go @@ -0,0 +1,77 @@ +package attributes + +import ( + "errors" + "fmt" + "strings" + + "github.com/nspcc-dev/neofs-api-go/v2/netmap" +) + +const ( + pairSeparator = "/" + keyValueSeparator = ":" +) + +var ( + errEmptyChain = errors.New("empty attribute chain") + errNonUniqueBucket = errors.New("attributes must contain unique keys") + errUnexpectedKey = errors.New("attributes contain unexpected key") +) + +// ParseV2Attributes parses strings like "K1:V1/K2:V2/K3:V3" into netmap +// attributes. +func ParseV2Attributes(attrs []string, excl []string) ([]*netmap.Attribute, error) { + restricted := make(map[string]struct{}, len(excl)) + for i := range excl { + restricted[excl[i]] = struct{}{} + } + + cache := make(map[string]*netmap.Attribute, len(attrs)) + + for i := range attrs { + line := strings.Trim(attrs[i], pairSeparator) + chain := strings.Split(line, pairSeparator) + if len(chain) == 0 { + return nil, errEmptyChain + } + + var parentKey string // backtrack parents in next pairs + + for j := range chain { + pair := strings.Split(chain[j], keyValueSeparator) + if len(pair) != 2 { + return nil, fmt.Errorf("incorrect attribute pair %s", chain[j]) + } + + key := pair[0] + value := pair[1] + + if at, ok := cache[key]; ok && at.GetValue() != value { + return nil, errNonUniqueBucket + } + + if _, ok := restricted[key]; ok { + return nil, errUnexpectedKey + } + + attribute := new(netmap.Attribute) + attribute.SetKey(key) + attribute.SetValue(value) + + if parentKey != "" { + attribute.SetParents([]string{parentKey}) + } + + parentKey = key + cache[key] = attribute + } + } + + result := make([]*netmap.Attribute, 0, len(cache)) + for _, v := range cache { + result = append(result, v) + } + + return result, nil +} diff --git a/pkg/util/attributes/parser_test.go b/pkg/util/attributes/parser_test.go new file mode 100644 index 0000000000..b789cc385a --- /dev/null +++ b/pkg/util/attributes/parser_test.go @@ -0,0 +1,60 @@ +package attributes_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-node/pkg/util/attributes" + "github.com/stretchr/testify/require" +) + +func TestParseV2Attributes(t *testing.T) { + t.Run("empty", func(t *testing.T) { + attrs, err := attributes.ParseV2Attributes(nil, nil) + require.NoError(t, err) + require.Len(t, attrs, 0) + }) + + t.Run("non unique bucket keys", func(t *testing.T) { + good := []string{ + "StorageType:HDD/RPM:7200", + "StorageType:HDD/SMR:True", + } + _, err := attributes.ParseV2Attributes(good, nil) + require.NoError(t, err) + + bad := append(good, "StorageType:SSD/Cell:QLC") + _, err = attributes.ParseV2Attributes(bad, nil) + require.Error(t, err) + + }) + + t.Run("malformed", func(t *testing.T) { + _, err := attributes.ParseV2Attributes([]string{"..."}, nil) + require.Error(t, err) + + _, err = attributes.ParseV2Attributes([]string{"a:b", ""}, nil) + require.Error(t, err) + + _, err = attributes.ParseV2Attributes([]string{"//"}, nil) + require.Error(t, err) + }) + + t.Run("unexpected", func(t *testing.T) { + unexpectedBucket := []string{ + "Location:Europe/City:Moscow", + "Price:100", + } + _, err := attributes.ParseV2Attributes(unexpectedBucket, []string{"Price"}) + require.Error(t, err) + }) + + t.Run("correct", func(t *testing.T) { + from := []string{ + "/Location:Europe/Country:Sweden/City:Stockholm", + "/StorageType:HDD/RPM:7200", + } + attrs, err := attributes.ParseV2Attributes(from, nil) + require.NoError(t, err) + require.Len(t, attrs, 5) + }) +}