From 456351c1f1a3dbe4783ac9738c43157b6cb8644e Mon Sep 17 00:00:00 2001 From: jub0bs Date: Wed, 27 Mar 2024 09:46:21 +0100 Subject: [PATCH 1/6] Benchmark maliciously long ACRH header --- bench_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bench_test.go b/bench_test.go index 6859382..01c2240 100644 --- a/bench_test.go +++ b/bench_test.go @@ -2,6 +2,7 @@ package cors import ( "net/http" + "strings" "testing" ) @@ -97,6 +98,21 @@ func BenchmarkPreflightHeader(b *testing.B) { } } +func BenchmarkPreflightAdversarialACRH(b *testing.B) { + resps := makeFakeResponses(b.N) + req, _ := http.NewRequest(http.MethodOptions, dummyEndpoint, nil) + req.Header.Add(headerOrigin, dummyOrigin) + req.Header.Add(headerACRM, http.MethodGet) + req.Header.Add(headerACRH, strings.Repeat(",", 1024)) + handler := Default().Handler(testHandler) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(resps[i], req) + } +} + func makeFakeResponses(n int) []*FakeResponse { resps := make([]*FakeResponse, n) for i := 0; i < n; i++ { From 32887921ac113d9e87dbb8552a19d638074937dd Mon Sep 17 00:00:00 2001 From: jub0bs Date: Wed, 27 Mar 2024 10:10:52 +0100 Subject: [PATCH 2/6] Add a sorted-set data structure --- internal/sortedset.go | 133 +++++++++++++++++++++++++++++++++++++ internal/sortedset_test.go | 119 +++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 internal/sortedset.go create mode 100644 internal/sortedset_test.go diff --git a/internal/sortedset.go b/internal/sortedset.go new file mode 100644 index 0000000..e37c792 --- /dev/null +++ b/internal/sortedset.go @@ -0,0 +1,133 @@ +// adapted from github.com/jub0bs/cors +package internal + +import ( + "sort" + "strings" +) + +// A SortedSet represents a mathematical set of strings sorted in +// lexicographical order. +// Each element has a unique position ranging from 0 (inclusive) +// to the set's cardinality (exclusive). +// The zero value represents an empty set. +type SortedSet struct { + m map[string]int + maxLen int +} + +// NewSortedSet returns a SortedSet that contains all of elems, +// but no other elements. +func NewSortedSet(elems ...string) SortedSet { + sort.Strings(elems) + elems = compact(elems) + m := make(map[string]int) + var maxLen int + for i, s := range elems { + maxLen = max(maxLen, len(s)) + m[s] = i + } + res := SortedSet{ + m: m, + maxLen: maxLen, + } + return res +} + +// Size returns the cardinality of set. +func (set SortedSet) Size() int { + return len(set.m) +} + +// String sorts joins the elements of set (in lexicographical order) +// with a comma and returns the resulting string. +func (set SortedSet) String() string { + elems := make([]string, len(set.m)) + for elem, i := range set.m { + elems[i] = elem // safe indexing, by construction of SortedSet + } + return strings.Join(elems, ",") +} + +// Subsumes reports whether csv is a sequence of comma-separated names that are +// - all elements of set, +// - sorted in lexicographically order, +// - unique. +func (set SortedSet) Subsumes(csv string) bool { + if csv == "" { + return true + } + posOfLastNameSeen := -1 + chunkSize := set.maxLen + 1 // (to accommodate for at least one comma) + for { + // As a defense against maliciously long names in csv, + // we only process at most chunkSize bytes per iteration. + end := min(len(csv), chunkSize) + comma := strings.IndexByte(csv[:end], ',') + var name string + if comma == -1 { + name = csv + } else { + name = csv[:comma] + } + pos, found := set.m[name] + if !found { + return false + } + // The names in csv are expected to be sorted in lexicographical order + // and appear at most once in csv. + // Therefore, the positions (in set) of the names that + // appear in csv should form a strictly increasing sequence. + // If that's not actually the case, bail out. + if pos <= posOfLastNameSeen { + return false + } + posOfLastNameSeen = pos + if comma < 0 { // We've now processed all the names in csv. + break + } + csv = csv[comma+1:] + } + return true +} + +// adapted from https://pkg.go.dev/slices#Compact +// TODO: when updating go directive to 1.21 or later, +// use slices.Compact instead. +func compact(s []string) []string { + if len(s) < 2 { + return s + } + i := 1 + for k := 1; k < len(s); k++ { + if s[k] != s[k-1] { + if i != k { + s[i] = s[k] + } + i++ + } + } + // zero/nil out the obsolete elements, for GC + for j := i; j < len(s); j++ { + s[j] = "" + } + return s[:i] +} + +// TODO: when updating go directive to 1.21 or later, +// use min builtin instead. +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// TODO: when updating go directive to 1.21 or later, +// use max builtin instead. +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/internal/sortedset_test.go b/internal/sortedset_test.go new file mode 100644 index 0000000..9727686 --- /dev/null +++ b/internal/sortedset_test.go @@ -0,0 +1,119 @@ +package internal + +import ( + "testing" +) + +func TestSortedSet(t *testing.T) { + cases := []struct { + desc string + elems []string + combined string + subsets []string + notSubsets []string + wantSize int + }{ + { + desc: "empty set", + combined: "", + notSubsets: []string{ + "bar", + "bar,foo", + }, + wantSize: 0, + }, { + desc: "singleton set", + elems: []string{"foo"}, + combined: "foo", + subsets: []string{ + "", + "foo", + }, + notSubsets: []string{ + "bar", + "bar,foo", + }, + wantSize: 1, + }, { + desc: "no dupes", + elems: []string{"foo", "bar", "baz"}, + combined: "bar,baz,foo", + subsets: []string{ + "", + "bar", + "baz", + "foo", + "bar,baz", + "bar,foo", + "baz,foo", + "bar,baz,foo", + }, + notSubsets: []string{ + "qux", + "bar,baz,baz", + "qux,baz", + "qux,foo", + "quxbaz,foo", + }, + wantSize: 3, + }, { + desc: "some dupes", + elems: []string{"foo", "bar", "bar", "foo", "e"}, + combined: "bar,e,foo", + subsets: []string{ + "", + "bar", + "e", + "foo", + "bar,foo", + "bar,e", + "e,foo", + "bar,e,foo", + }, + notSubsets: []string{ + "qux", + "qux,bar", + "qux,foo", + "qux,baz,foo", + }, + wantSize: 3, + }, + } + for _, tc := range cases { + f := func(t *testing.T) { + elems := clone(tc.elems) + s := NewSortedSet(tc.elems...) + size := s.Size() + if s.Size() != tc.wantSize { + const tmpl = "NewSortedSet(%#v...).Size(): got %d; want %d" + t.Errorf(tmpl, elems, size, tc.wantSize) + } + combined := s.String() + if combined != tc.combined { + const tmpl = "NewSortedSet(%#v...).String(): got %q; want %q" + t.Errorf(tmpl, elems, combined, tc.combined) + } + for _, sub := range tc.subsets { + if !s.Subsumes(sub) { + const tmpl = "%q is not a subset of %q, but should be" + t.Errorf(tmpl, sub, s) + } + } + for _, notSub := range tc.notSubsets { + if s.Subsumes(notSub) { + const tmpl = "%q is a subset of %q, but should not be" + t.Errorf(tmpl, notSub, s) + } + } + } + t.Run(tc.desc, f) + } +} + +// adapted from https://pkg.go.dev/slices#Clone +// TODO: when updating go directive to 1.21 or later, +// use slices.Clone instead. +func clone(s []string) []string { + // The s[:0:0] preserves nil in case it matters. + return append(s[:0:0], s...) +} From dbcbaf7a142ed98e62d3960d349f62b9be8d9e07 Mon Sep 17 00:00:00 2001 From: jub0bs Date: Wed, 27 Mar 2024 09:48:58 +0100 Subject: [PATCH 3/6] Store allowed headers in a sorted set (#170) --- bench_test.go | 2 +- cors.go | 57 +++++++++++++++---------------------- cors_test.go | 78 +++++++-------------------------------------------- utils.go | 39 ++++++-------------------- utils_test.go | 36 ------------------------ 5 files changed, 42 insertions(+), 170 deletions(-) diff --git a/bench_test.go b/bench_test.go index 01c2240..688d2b2 100644 --- a/bench_test.go +++ b/bench_test.go @@ -88,7 +88,7 @@ func BenchmarkPreflightHeader(b *testing.B) { req, _ := http.NewRequest(http.MethodOptions, dummyEndpoint, nil) req.Header.Add(headerOrigin, dummyOrigin) req.Header.Add(headerACRM, http.MethodGet) - req.Header.Add(headerACRH, "Accept") + req.Header.Add(headerACRH, "accept") handler := Default().Handler(testHandler) b.ReportAllocs() diff --git a/cors.go b/cors.go index 08aff84..da80d34 100644 --- a/cors.go +++ b/cors.go @@ -26,6 +26,8 @@ import ( "os" "strconv" "strings" + + "github.com/rs/cors/internal" ) var headerVaryOrigin = []string{"Origin"} @@ -111,7 +113,11 @@ type Cors struct { // Optional origin validator function allowOriginFunc func(r *http.Request, origin string) (bool, []string) // Normalized list of allowed headers - allowedHeaders []string + // Note: the Fetch standard guarantees that CORS-unsafe request-header names + // (i.e. the values listed in the Access-Control-Request-Headers header) + // are unique and sorted; + // see https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names. + allowedHeaders internal.SortedSet // Normalized list of allowed methods allowedMethods []string // Pre-computed normalized list of exposed headers @@ -183,15 +189,19 @@ func New(options Options) *Cors { } // Allowed Headers + // Note: the Fetch standard guarantees that CORS-unsafe request-header names + // (i.e. the values listed in the Access-Control-Request-Headers header) + // are lowercase; see https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names. if len(options.AllowedHeaders) == 0 { // Use sensible defaults - c.allowedHeaders = []string{"Accept", "Content-Type", "X-Requested-With"} + c.allowedHeaders = internal.NewSortedSet("accept", "content-type", "x-requested-with") } else { - c.allowedHeaders = convert(options.AllowedHeaders, http.CanonicalHeaderKey) + normalized := convert(options.AllowedHeaders, strings.ToLower) + c.allowedHeaders = internal.NewSortedSet(normalized...) for _, h := range options.AllowedHeaders { if h == "*" { c.allowedHeadersAll = true - c.allowedHeaders = nil + c.allowedHeaders = internal.SortedSet{} break } } @@ -351,10 +361,12 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { c.logf(" Preflight aborted: method '%s' not allowed", reqMethod) return } - reqHeadersRaw := r.Header["Access-Control-Request-Headers"] - reqHeaders, reqHeadersEdited := convertDidCopy(splitHeaderValues(reqHeadersRaw), http.CanonicalHeaderKey) - if !c.areHeadersAllowed(reqHeaders) { - c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders) + // Note: the Fetch standard guarantees that at most one + // Access-Control-Request-Headers header is present in the preflight request; + // see step 5.2 in https://fetch.spec.whatwg.org/#cors-preflight-fetch-0. + reqHeaders, found := first(r.Header, "Access-Control-Request-Headers") + if found && !c.allowedHeadersAll && !c.allowedHeaders.Subsumes(reqHeaders[0]) { + c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders[0]) return } if c.allowedOriginsAll { @@ -365,14 +377,10 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { // Spec says: Since the list of methods can be unbounded, simply returning the method indicated // by Access-Control-Request-Method (if supported) can be enough headers["Access-Control-Allow-Methods"] = r.Header["Access-Control-Request-Method"] - if len(reqHeaders) > 0 { + if found && len(reqHeaders[0]) > 0 { // Spec says: Since the list of headers can be unbounded, simply returning supported headers // from Access-Control-Request-Headers can be enough - if reqHeadersEdited || len(reqHeaders) != len(reqHeadersRaw) { - headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) - } else { - headers["Access-Control-Allow-Headers"] = reqHeadersRaw - } + headers["Access-Control-Allow-Headers"] = reqHeaders } if c.allowCredentials { headers["Access-Control-Allow-Credentials"] = headerTrue @@ -492,24 +500,3 @@ func (c *Cors) isMethodAllowed(method string) bool { } return false } - -// areHeadersAllowed checks if a given list of headers are allowed to used within -// a cross-domain request. -func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { - if c.allowedHeadersAll || len(requestedHeaders) == 0 { - return true - } - for _, header := range requestedHeaders { - found := false - for _, h := range c.allowedHeaders { - if h == header { - found = true - break - } - } - if !found { - return false - } - } - return true -} diff --git a/cors_test.go b/cors_test.go index c17dee2..a3c0aab 100644 --- a/cors_test.go +++ b/cors_test.go @@ -303,19 +303,19 @@ func TestSpec(t *testing.T) { "AllowedHeaders", Options{ AllowedOrigins: []string{"http://foobar.com"}, - AllowedHeaders: []string{"X-Header-1", "x-header-2"}, + AllowedHeaders: []string{"X-Header-1", "x-header-2", "X-HEADER-3"}, }, "OPTIONS", map[string]string{ "Origin": "http://foobar.com", "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "X-Header-2, X-HEADER-1", + "Access-Control-Request-Headers": "x-header-1,x-header-2", }, map[string]string{ "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Access-Control-Allow-Origin": "http://foobar.com", "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "X-Header-2, X-Header-1", + "Access-Control-Allow-Headers": "x-header-1,x-header-2", }, true, }, @@ -329,13 +329,13 @@ func TestSpec(t *testing.T) { map[string]string{ "Origin": "http://foobar.com", "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "X-Requested-With", + "Access-Control-Request-Headers": "x-requested-with", }, map[string]string{ "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Access-Control-Allow-Origin": "http://foobar.com", "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "X-Requested-With", + "Access-Control-Allow-Headers": "x-requested-with", }, true, }, @@ -349,13 +349,13 @@ func TestSpec(t *testing.T) { map[string]string{ "Origin": "http://foobar.com", "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "X-Header-2, X-HEADER-1", + "Access-Control-Request-Headers": "x-header-1,x-header-2", }, map[string]string{ "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Access-Control-Allow-Origin": "http://foobar.com", "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "X-Header-2, X-Header-1", + "Access-Control-Allow-Headers": "x-header-1,x-header-2", }, true, }, @@ -369,7 +369,7 @@ func TestSpec(t *testing.T) { map[string]string{ "Origin": "http://foobar.com", "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "X-Header-3, X-Header-1", + "Access-Control-Request-Headers": "x-header-1,x-header-3", }, map[string]string{ "Vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", @@ -577,8 +577,8 @@ func TestDefault(t *testing.T) { if !s.allowedOriginsAll { t.Error("c.allowedOriginsAll should be true when Default") } - if s.allowedHeaders == nil { - t.Error("c.allowedHeaders should be nil when Default") + if s.allowedHeaders.Size() == 0 { + t.Error("c.allowedHeaders should be empty when Default") } if s.allowedMethods == nil { t.Error("c.allowedMethods should be nil when Default") @@ -712,64 +712,6 @@ func TestOptionsSuccessStatusCodeOverride(t *testing.T) { }) } -func TestCorsAreHeadersAllowed(t *testing.T) { - cases := []struct { - name string - allowedHeaders []string - requestedHeaders []string - want bool - }{ - { - name: "nil allowedHeaders", - allowedHeaders: nil, - requestedHeaders: []string{"X-PINGOTHER, Content-Type"}, - want: false, - }, - { - name: "star allowedHeaders", - allowedHeaders: []string{"*"}, - requestedHeaders: []string{"X-PINGOTHER, Content-Type"}, - want: true, - }, - { - name: "empty reqHeader", - allowedHeaders: nil, - requestedHeaders: []string{}, - want: true, - }, - { - name: "match allowedHeaders", - allowedHeaders: []string{"Content-Type", "X-PINGOTHER", "X-APP-KEY"}, - requestedHeaders: []string{"X-PINGOTHER, Content-Type"}, - want: true, - }, - { - name: "not matched allowedHeaders", - allowedHeaders: []string{"X-PINGOTHER"}, - requestedHeaders: []string{"X-API-KEY, Content-Type"}, - want: false, - }, - { - name: "allowedHeaders should be a superset of requestedHeaders", - allowedHeaders: []string{"X-PINGOTHER"}, - requestedHeaders: []string{"X-PINGOTHER, Content-Type"}, - want: false, - }, - } - - for _, tt := range cases { - tt := tt - - t.Run(tt.name, func(t *testing.T) { - c := New(Options{AllowedHeaders: tt.allowedHeaders}) - have := c.areHeadersAllowed(convert(splitHeaderValues(tt.requestedHeaders), http.CanonicalHeaderKey)) - if have != tt.want { - t.Errorf("Cors.areHeadersAllowed() have: %t want: %t", have, tt.want) - } - }) - } -} - func TestAccessControlExposeHeadersPresence(t *testing.T) { cases := []struct { name string diff --git a/utils.go b/utils.go index ca9983d..85c2d45 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,7 @@ package cors import ( + "net/http" "strings" ) @@ -15,36 +16,6 @@ func (w wildcard) match(s string) bool { return len(s) >= len(w.prefix)+len(w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) } -// split compounded header values ["foo, bar", "baz"] -> ["foo", "bar", "baz"] -func splitHeaderValues(values []string) []string { - out := values - copied := false - for i, v := range values { - needsSplit := strings.IndexByte(v, ',') != -1 - if !copied { - if needsSplit { - split := strings.Split(v, ",") - out = make([]string, i, len(values)+len(split)-1) - copy(out, values[:i]) - for _, s := range split { - out = append(out, strings.TrimSpace(s)) - } - copied = true - } - } else { - if needsSplit { - split := strings.Split(v, ",") - for _, s := range split { - out = append(out, strings.TrimSpace(s)) - } - } else { - out = append(out, v) - } - } - } - return out -} - // convert converts a list of string using the passed converter function func convert(s []string, c converter) []string { out, _ := convertDidCopy(s, c) @@ -70,3 +41,11 @@ func convertDidCopy(s []string, c converter) ([]string, bool) { } return out, copied } + +func first(hdrs http.Header, k string) ([]string, bool) { + v, found := hdrs[k] + if !found || len(v) == 0 { + return nil, false + } + return v[:1], true +} diff --git a/utils_test.go b/utils_test.go index f6a83ec..409f24e 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,7 +1,6 @@ package cors import ( - "reflect" "strings" "testing" ) @@ -24,41 +23,6 @@ func TestWildcard(t *testing.T) { } } -func TestSplitHeaderValues(t *testing.T) { - testCases := []struct { - input []string - expected []string - }{ - { - input: []string{}, - expected: []string{}, - }, - { - input: []string{"foo"}, - expected: []string{"foo"}, - }, - { - input: []string{"foo, bar, baz"}, - expected: []string{"foo", "bar", "baz"}, - }, - { - input: []string{"abc", "def, ghi", "jkl"}, - expected: []string{"abc", "def", "ghi", "jkl"}, - }, - { - input: []string{"foo, bar", "baz, qux", "quux, corge"}, - expected: []string{"foo", "bar", "baz", "qux", "quux", "corge"}, - }, - } - - for _, testCase := range testCases { - output := splitHeaderValues(testCase.input) - if !reflect.DeepEqual(output, testCase.expected) { - t.Errorf("Input: %v, Expected: %v, Got: %v", testCase.input, testCase.expected, output) - } - } -} - func TestConvert(t *testing.T) { s := convert([]string{"A", "b", "C"}, strings.ToLower) e := []string{"a", "b", "c"} From 034e69ae56f01ae2faa518e9a3885631b8d5e14a Mon Sep 17 00:00:00 2001 From: jub0bs Date: Wed, 27 Mar 2024 11:01:46 +0100 Subject: [PATCH 4/6] Simplify convert --- utils.go | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/utils.go b/utils.go index 85c2d45..d54b61d 100644 --- a/utils.go +++ b/utils.go @@ -5,8 +5,6 @@ import ( "strings" ) -type converter func(string) string - type wildcard struct { prefix string suffix string @@ -17,29 +15,12 @@ func (w wildcard) match(s string) bool { } // convert converts a list of string using the passed converter function -func convert(s []string, c converter) []string { - out, _ := convertDidCopy(s, c) - return out -} - -// convertDidCopy is same as convert but returns true if it copied the slice -func convertDidCopy(s []string, c converter) ([]string, bool) { - out := s - copied := false - for i, v := range s { - if !copied { - v2 := c(v) - if v2 != v { - out = make([]string, len(s)) - copy(out, s[:i]) - out[i] = v2 - copied = true - } - } else { - out[i] = c(v) - } +func convert(s []string, f func(string) string) []string { + out := make([]string, len(s)) + for i := range s { + out[i] = f(s[i]) } - return out, copied + return out } func first(hdrs http.Header, k string) ([]string, bool) { From aebaa1fd0cf1633fce211bf970c4a55e96707516 Mon Sep 17 00:00:00 2001 From: jub0bs Date: Wed, 27 Mar 2024 11:02:30 +0100 Subject: [PATCH 5/6] Cosmetic change (shorten line) --- utils.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils.go b/utils.go index d54b61d..7019f45 100644 --- a/utils.go +++ b/utils.go @@ -11,7 +11,9 @@ type wildcard struct { } func (w wildcard) match(s string) bool { - return len(s) >= len(w.prefix)+len(w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) + return len(s) >= len(w.prefix)+len(w.suffix) && + strings.HasPrefix(s, w.prefix) && + strings.HasSuffix(s, w.suffix) } // convert converts a list of string using the passed converter function From 4f7d39d433ff1fe7cb24b8717f920a62e2cf9cab Mon Sep 17 00:00:00 2001 From: jub0bs Date: Wed, 24 Apr 2024 07:39:02 +0200 Subject: [PATCH 6/6] Merge compact and maxLen scanning --- internal/sortedset.go | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/internal/sortedset.go b/internal/sortedset.go index e37c792..513da20 100644 --- a/internal/sortedset.go +++ b/internal/sortedset.go @@ -20,18 +20,21 @@ type SortedSet struct { // but no other elements. func NewSortedSet(elems ...string) SortedSet { sort.Strings(elems) - elems = compact(elems) m := make(map[string]int) var maxLen int - for i, s := range elems { - maxLen = max(maxLen, len(s)) + i := 0 + for _, s := range elems { + if _, exists := m[s]; exists { + continue + } m[s] = i + i++ + maxLen = max(maxLen, len(s)) } - res := SortedSet{ + return SortedSet{ m: m, maxLen: maxLen, } - return res } // Size returns the cardinality of set. @@ -91,29 +94,6 @@ func (set SortedSet) Subsumes(csv string) bool { return true } -// adapted from https://pkg.go.dev/slices#Compact -// TODO: when updating go directive to 1.21 or later, -// use slices.Compact instead. -func compact(s []string) []string { - if len(s) < 2 { - return s - } - i := 1 - for k := 1; k < len(s); k++ { - if s[k] != s[k-1] { - if i != k { - s[i] = s[k] - } - i++ - } - } - // zero/nil out the obsolete elements, for GC - for j := i; j < len(s); j++ { - s[j] = "" - } - return s[:i] -} - // TODO: when updating go directive to 1.21 or later, // use min builtin instead. func min(a, b int) int {