Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 44 additions & 21 deletions collections/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,38 +128,33 @@ func MatchItem(item string, patterns ...string) (matches, negated bool) {
return true, false
}

patterns = normalizeMatchPatterns(patterns)
if len(patterns) == 0 {
return false, false
}

slices.SortFunc(patterns, sortPatterns)

//process negations first
for _, p := range patterns {
pattern, err := url.QueryUnescape(strings.TrimSpace(p))
if err != nil {
continue
}

if strings.HasPrefix(pattern, "!") {
if matchPattern(item, strings.TrimPrefix(pattern, "!")) {
if strings.HasPrefix(p, "!") {
if matchPattern(item, strings.TrimPrefix(p, "!")) {
return false, true
}
}

}

// then normal filters
for _, p := range patterns {
pattern, err := url.QueryUnescape(strings.TrimSpace(p))
if err != nil {
continue
}

for _, pattern := range patterns {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if matchPattern(item, pattern) {
return true, false
}
}

//nolint:gosimple
//lint:ignore S1008 ...
if IsExclusionOnlyPatterns(patterns) {
if isExclusionOnly(patterns) {
// If all the filters were exlusions, and none of the exclusions excluded the item, then it's a match
return true, false
}
Expand All @@ -177,14 +172,14 @@ func MatchItems(item string, patterns ...string) bool {
return true
}

slices.SortFunc(patterns, sortPatterns)
patterns = normalizeMatchPatterns(patterns)
if len(patterns) == 0 {
return false
}

for _, p := range patterns {
pattern, err := url.QueryUnescape(strings.TrimSpace(p))
if err != nil {
continue
}
slices.SortFunc(patterns, sortPatterns)

for _, pattern := range patterns {
if strings.HasPrefix(pattern, "!") {
if matchPattern(item, strings.TrimPrefix(pattern, "!")) {
return false
Expand All @@ -200,7 +195,7 @@ func MatchItems(item string, patterns ...string) bool {

//nolint:gosimple
//lint:ignore S1008 ...
if IsExclusionOnlyPatterns(patterns) {
if isExclusionOnly(patterns) {
// If all the filters were exlusions, and none of the exclusions excluded the item, then it's a match
return true
}
Expand Down Expand Up @@ -241,6 +236,26 @@ func matchPattern(item, pattern string) bool {
return false
}

func normalizeMatchPatterns(patterns []string) []string {
var normalized []string
for _, pattern := range patterns {
parts := strings.Split(pattern, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" && len(parts) > 1 {
continue
}

decoded, err := url.QueryUnescape(part)
if err != nil {
continue
}
normalized = append(normalized, decoded)
}
}
return normalized
}

// sortPatterns defines the priority for sorting:
// exclusions ("!") have higher priority than other patterns.
func sortPatterns(a, b string) int {
Expand All @@ -260,6 +275,14 @@ func sortPatterns(a, b string) int {
}

func IsExclusionOnlyPatterns(patterns []string) bool {
return isExclusionOnly(normalizeMatchPatterns(patterns))
}

// isExclusionOnly reports whether every pattern excludes, over patterns that
// normalizeMatchPatterns has already split and decoded. Normalizing again would
// split a decoded %2C as though it were a separator, turning one exclusion into
// an exclusion plus an inclusion.
func isExclusionOnly(patterns []string) bool {
if len(patterns) == 0 {
return false
}
Expand Down
29 changes: 29 additions & 0 deletions collections/slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,38 @@ var _ = Describe("MatchItems", func() {
Entry("case insensitive suffix wildcard", "Apple", []string{"*PLE"}, true),
Entry("case insensitive glob", "Apple", []string{"*PPL*"}, true),
Entry("case insensitive exclusion", "Apple", []string{"!apple"}, false),
Entry("comma-separated include after exclusion matches", "Item2", []string{"!Item,Item2"}, true),
Entry("comma-separated exclusion wins", "Item", []string{"!Item,Item2"}, false),
Entry("comma-separated wildcard exclusion wins", "Item99", []string{"!Item*,Other"}, false),
Entry("comma-separated wildcard exclusion allows include", "Other", []string{"!Item*,Other"}, true),
Entry("comma-separated values trim raw token whitespace", "Item2", []string{" !Item , Item2 "}, true),
Entry("comma-separated malformed URL token is skipped", "Item2", []string{"!Item,%zz"}, true),
Entry("URL encoded comma remains a literal pattern character", "A,B", []string{"A%2CB,C"}, true),
Entry("URL encoded comma in an exclusion is one non-matching pattern", "A", []string{"!A%2CB"}, true),
Entry("URL encoded comma in an exclusion still excludes what it names", "A,B", []string{"!A%2CB"}, false),
)
})

var _ = Describe("MatchItem", func() {
It("expands comma-separated include and exclude patterns", func() {
matches, negated := MatchItem("Item2", "!Item,Item2")
Expect(matches).To(BeTrue())
Expect(negated).To(BeFalse())

matches, negated = MatchItem("Item", "!Item,Item2")
Expect(matches).To(BeFalse())
Expect(negated).To(BeTrue())
})
})

var _ = Describe("IsExclusionOnlyPatterns", func() {
It("expands comma-separated patterns before checking exclusions", func() {
Expect(IsExclusionOnlyPatterns([]string{"!Item,!Item2"})).To(BeTrue())
Expect(IsExclusionOnlyPatterns([]string{"!Item,Item2"})).To(BeFalse())
Expect(IsExclusionOnlyPatterns([]string{"!Item,"})).To(BeTrue())
})
})

var _ = Describe("Append", func() {
It("should append string slices", func() {
slices := [][]any{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/flanksource/commons
go 1.25.1

require (
dario.cat/mergo v1.0.2
github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.9
github.com/aws/aws-sdk-go-v2/credentials v1.19.9
Expand Down
9 changes: 3 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
Expand Down Expand Up @@ -255,8 +257,6 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
Expand Down Expand Up @@ -334,8 +334,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88=
github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
Expand Down Expand Up @@ -378,9 +376,8 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
Expand Down
9 changes: 5 additions & 4 deletions http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,14 @@ func TestHTTP(t *testing.T) {
server := headerEchoServer(t)
defer server.Close()

const hostOverride = "httpbin.example.com"
resp, err := http.NewClient().
TraceToStdout(http.TraceAll).
R(context.Background()).
Header("Host", "example.test").
Get(server.URL)
if err != nil {
t.Error(err)
t.Fatal(err)
}

headers := responseHeaders(t, resp)
Expand All @@ -171,14 +172,14 @@ func TestHTTP(t *testing.T) {

resp, err := http.NewClient().R(context.Background()).Header("Hello", "World").Get(server.URL)
if err != nil {
t.Error(err)
t.Fatal(err)
}
headers := responseHeaders(t, resp)
if headers["Hello"] != "World" {
t.Errorf("Expected response headers %s", headers)
t.Errorf("expected Hello header %q, got %v", "World", headers["Hello"])
}
if v, ok := headers["Authorization"]; ok {
t.Errorf("Expecting blank authentication got %s", v)
t.Errorf("expected no Authorization header, got %v", v)
}
})

Expand Down
Loading
Loading