From 3ddc60d649e3972ce7e861f1a0577b02cb56a8d4 Mon Sep 17 00:00:00 2001 From: Alberto Ricart Date: Thu, 3 Oct 2024 14:52:04 -0500 Subject: [PATCH] [RETRACT] 2.7.0/2.7.1 [CHANGE] tag list is case-preserving but case-insensitive for matches [FEAT] added FilterTag() to filter a set of entries by the tag name (tag name is considered case-insensitive) [FEAT] added TagValueFor() to return all values matching a particular tag name (tag name is considered case-insensitive) Signed-off-by: Alberto Ricart --- v2/go.mod | 5 ++ v2/operator_claims_test.go | 1 + v2/types.go | 156 ++++++++++++++++++++++++------------- v2/types_test.go | 148 ++++++++++++++--------------------- 4 files changed, 169 insertions(+), 141 deletions(-) diff --git a/v2/go.mod b/v2/go.mod index 63452bd..f417ec2 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -4,6 +4,11 @@ go 1.18 require github.com/nats-io/nkeys v0.4.7 +retract ( + v2.7.1 // contains retractions only + v2.7.0 // includes case insensitive changes to tags that break jetstream placement +) + require ( golang.org/x/crypto v0.19.0 // indirect golang.org/x/sys v0.17.0 // indirect diff --git a/v2/operator_claims_test.go b/v2/operator_claims_test.go index 04529ae..ed7df4f 100644 --- a/v2/operator_claims_test.go +++ b/v2/operator_claims_test.go @@ -459,6 +459,7 @@ func TestTags(t *testing.T) { } AssertTrue(oc.GenericFields.Tags.Contains("one"), t) + AssertTrue(oc.GenericFields.Tags.Contains("ONE"), t) AssertTrue(oc.GenericFields.Tags.Contains("TWO"), t) AssertTrue(oc.GenericFields.Tags.Contains("three"), t) } diff --git a/v2/types.go b/v2/types.go index f0049ab..37f13d0 100644 --- a/v2/types.go +++ b/v2/types.go @@ -17,10 +17,12 @@ package jwt import ( "encoding/json" + "errors" "fmt" "net" "net/url" "reflect" + "sort" "strconv" "strings" "time" @@ -421,96 +423,146 @@ func (u *StringList) Remove(p ...string) { } } -// TagList is a unique array of lower case strings -// All tag list methods lower case the strings in the arguments +// TagList is a case-preserving case-insensitive Set of strings. +// Entries that contain a colon, are considered to be a name/value pair. +// The name portion of the tag is case-insensitive for purposes of matching. type TagList []string -// Contains returns true if the list contains the tags -func (u *TagList) Contains(p string) bool { - return u.find(p) != -1 -} - -func (u *TagList) Equals(other *TagList) bool { - if len(*u) != len(*other) { - return false - } - for _, v := range *u { - if other.find(v) == -1 { - return false - } - } - return true -} - func (u *TagList) find(p string) int { for idx, t := range *u { - if p == t { + if strings.EqualFold(t, p) { return idx } } return -1 } -// Add appends 1 or more tags to a list +// Contains returns true if the list contains the tag. Note +// that contains is case-insensitive +func (u *TagList) Contains(p string) bool { + p = strings.TrimSpace(p) + return u.find(p) != -1 +} + +// Add appends 1 or more tags to a list. Note that Add is +// case preserving. But tests for equality in a case-insensitive +// way, if the value exists, it is replaced with the new value func (u *TagList) Add(p ...string) { for _, v := range p { v = strings.TrimSpace(v) - if v == "" { - continue - } - if !u.Contains(v) { + idx := u.find(v) + if idx != -1 { + a := *u + a[idx] = v + } else { *u = append(*u, v) } } } -// Remove removes 1 or more tags from a list -func (u *TagList) Remove(p ...string) error { +// Remove removes 1 or more tags from a list, removal is case-insensitive +func (u *TagList) Remove(p ...string) { for _, v := range p { v = strings.TrimSpace(v) idx := u.find(v) if idx != -1 { a := *u *u = append(a[:idx], a[idx+1:]...) - } else { - return fmt.Errorf("unable to remove tag: %q - not found", v) } } - return nil } -type CIDRList []string +// FilterTag returns entries that start with the specified name +// followed by a colon (:). Tag names are case-insensitive for +// matches. This function returns the entire entry (name+:+value) +func (u *TagList) FilterTag(v string) (*TagList, error) { + var matches TagList -func (c *CIDRList) Contains(p string) bool { - p = strings.ToLower(strings.TrimSpace(p)) - for _, t := range *c { - if t == p { - return true + v, err := u.tagToken(v) + if err != nil { + return nil, err + } + + for _, t := range *u { + // entry has to have a ':' in it, to be considered as a tag + idx := strings.Index(t, ":") + if idx != -1 { + tagName := t[:idx+1] + value := t[idx+1:] + // to be a valid tag, it must match the name and have a value + if strings.EqualFold(v, tagName) && len(value) > 0 { + matches = append(matches, t) + } } } - return false + return &matches, nil } -func (c *CIDRList) Add(p ...string) { - for _, v := range p { - v = strings.ToLower(strings.TrimSpace(v)) - if !c.Contains(v) && v != "" { - *c = append(*c, v) - } +// TagToken takes a name, and returns name+":". Note that it illegal +// to have a colon in the name of a tag. +func (u *TagList) tagToken(v string) (string, error) { + v = strings.TrimSpace(v) + idx := strings.Index(v, ":") + if idx != -1 { + return "", errors.New("tag names cannot contain ':'") } + if len(v) == 0 { + return "", errors.New("tag name is required") + } + return fmt.Sprintf("%s:", v), nil } -func (c *CIDRList) Remove(p ...string) { - for _, v := range p { - v = strings.ToLower(strings.TrimSpace(v)) - for i, t := range *c { - if t == v { - a := *c - *c = append(a[:i], a[i+1:]...) - break - } +// TagValueFor finds entries that start with the specified name +// followed by a colon (:). Names are case-insensitive for +// matches. This function returns the value portion of the entry +func (u *TagList) TagValueFor(v string) (*TagList, error) { + tags, err := u.FilterTag(v) + if err != nil { + return nil, err + } + + // error would have happened already + v, _ = u.tagToken(v) + + start := len(v) + a := *tags + for idx, t := range a { + a[idx] = t[start:] + } + + return &a, nil +} + +func (u *TagList) Equals(other *TagList) bool { + if len(*u) != len(*other) { + return false + } + + a := sort.StringSlice(*u) + sort.Sort(a) + b := sort.StringSlice(*other) + sort.Sort(b) + + for i, v := range a { + if v != b[i] { + return false } } + return true +} + +type CIDRList TagList + +func (c *CIDRList) Contains(p string) bool { + return (*TagList)(c).Contains(p) +} + +func (c *CIDRList) Add(p ...string) { + (*TagList)(c).Add(p...) +} + +func (c *CIDRList) Remove(p ...string) { + (*TagList)(c).Remove(p...) } func (c *CIDRList) Set(values string) { diff --git a/v2/types_test.go b/v2/types_test.go index 9d8a205..6349717 100644 --- a/v2/types_test.go +++ b/v2/types_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 The NATS Authors + * Copyright 2018 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -111,27 +111,71 @@ func TestTimeRangeValidation(t *testing.T) { } } -func TestTagList(t *testing.T) { +func TestTagAdd(t *testing.T) { tags := TagList{} + tags.Add("ONE") + AssertEquals("ONE", tags[0], t) + // dupes are not allowed + tags.Add("ONE") + AssertEquals(1, len(tags), t) tags.Add("one") - - AssertEquals(true, tags.Contains("one"), t) - AssertEquals(false, tags.Contains("ONE"), t) AssertEquals("one", tags[0], t) + AssertEquals(1, len(tags), t) +} - tags.Add("TWO") +func TestTagAddMultiple(t *testing.T) { + tags := TagList{} + tags.Add("ONE", "two", "Three", "tHree") + AssertEquals(3, len(tags), t) + AssertEquals("ONE", tags[0], t) + AssertEquals("two", tags[1], t) + AssertEquals("tHree", tags[2], t) +} - AssertEquals(false, tags.Contains("two"), t) - AssertEquals(true, tags.Contains("TWO"), t) - AssertEquals("TWO", tags[1], t) +func TestTagRemoveMultiple(t *testing.T) { + tags := TagList{} + tags.Add("ONE", "TWo", "Three") + AssertEquals(3, len(tags), t) + AssertEquals("ONE", tags[0], t) + AssertEquals("TWo", tags[1], t) + AssertEquals("Three", tags[2], t) + + tags.Remove("onE", "threE") + AssertEquals(1, len(tags), t) + AssertEquals("TWo", tags[0], t) +} - err := tags.Remove("ONE") - if err == nil { - t.Fatal("removing tag that doesn't exist should have failed") - } - AssertEquals("one", tags[0], t) - AssertEquals(true, tags.Contains("TWO"), t) +func TestTagList_Contains(t *testing.T) { + tags := TagList{} + tags.Add("ONE", "TwO", "three") + AssertTrue(tags.Contains("one"), t) + AssertTrue(tags.Contains("tWo"), t) + AssertTrue(tags.Contains("THREE"), t) +} + +func TestTagList_FilterTag(t *testing.T) { + tags := TagList{"A:hello", "a:hola", "b:hi"} + matches, err := tags.FilterTag("a") + AssertNoError(err, t) + AssertTrue(matches.Equals(&TagList{"A:hello", "a:hola"}), t) + + matches, err = tags.FilterTag("x") + AssertNoError(err, t) + AssertEquals(0, len(*matches), t) + AssertTrue(matches.Equals(&TagList{}), t) +} + +func TestTagList_GetTagValueFor(t *testing.T) { + tags := TagList{"A:Hello", "a:hola", "b:hi"} + matches, err := tags.TagValueFor("a") + AssertNoError(err, t) + AssertTrue(matches.Equals(&TagList{"Hello", "hola"}), t) + + matches, err = tags.TagValueFor("x") + AssertNoError(err, t) + AssertEquals(0, len(*matches), t) + AssertTrue(matches.Equals(&TagList{}), t) } func TestStringList(t *testing.T) { @@ -429,77 +473,3 @@ func TestInvalidInfo(t *testing.T) { } } } - -func TestTagList_CasePreservingContains(t *testing.T) { - type test struct { - v string - a TagList - ok bool - } - - tests := []test{ - {v: "A", a: TagList{}, ok: false}, - {v: "A", a: TagList{"A"}, ok: true}, - {v: "a", a: TagList{"A"}, ok: false}, - {v: "a", a: TagList{"a:hello"}, ok: false}, - {v: "a:a", a: TagList{"a:c"}, ok: false}, - } - - for idx, test := range tests { - found := test.a.Contains(test.v) - if !found && test.ok { - t.Errorf("[%d] expected to contain %q", idx, test.v) - } - } -} - -func TestTagList_Add(t *testing.T) { - type test struct { - v string - a TagList - shouldBe TagList - } - - tests := []test{ - {v: "A", a: TagList{}, shouldBe: TagList{"A"}}, - {v: "A", a: TagList{"A"}, shouldBe: TagList{"A"}}, - {v: "a", a: TagList{"A"}, shouldBe: TagList{"A", "a"}}, - {v: "a", a: TagList{"a:hello"}, shouldBe: TagList{"a", "a:hello"}}, - {v: "a:Hello", a: TagList{"a:hello"}, shouldBe: TagList{"a:hello", "a:Hello"}}, - {v: "a:a", a: TagList{"a:c"}, shouldBe: TagList{"a:a", "a:c"}}, - } - - for idx, test := range tests { - test.a.Add(test.v) - if !test.a.Equals(&test.shouldBe) { - t.Errorf("[%d] expected lists to be equal: %v", idx, test.a) - } - } -} - -func TestTagList_Delete(t *testing.T) { - type test struct { - v string - a TagList - shouldBe TagList - shouldFail bool - } - - tests := []test{ - {v: "A", a: TagList{}, shouldBe: TagList{}, shouldFail: true}, - {v: "A", a: TagList{"A"}, shouldBe: TagList{}}, - {v: "a", a: TagList{"A"}, shouldBe: TagList{"A"}, shouldFail: true}, - {v: "a:Hello", a: TagList{"a:hello"}, shouldBe: TagList{"a:hello"}, shouldFail: true}, - {v: "a:a", a: TagList{"a:A"}, shouldBe: TagList{"a:A"}, shouldFail: true}, - } - - for idx, test := range tests { - err := test.a.Remove(test.v) - if test.shouldFail && err == nil { - t.Fatalf("[%d] expected delete to fail: %v", idx, test.a) - } - if !test.a.Equals(&test.shouldBe) { - t.Fatalf("[%d] expected lists to be equal: %v", idx, test.a) - } - } -}