From 34ba06cf6106b7f99f298a56561eb569dc353502 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 1 Dec 2022 21:44:01 -0700 Subject: [PATCH] add suffix and pathSuffix filters (#1659) ## Description Preparing for handling sharepoint webUrl selectors, we will need suffix-matching filters. ## Type of change - [x] :sunflower: Feature ## Issue(s) * #1616 ## Test Plan - [x] :zap: Unit test --- src/pkg/filters/filters.go | 80 +++++++++++++++++++++++++ src/pkg/filters/filters_test.go | 101 ++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/src/pkg/filters/filters.go b/src/pkg/filters/filters.go index 0e6697d2e..cc08fd780 100644 --- a/src/pkg/filters/filters.go +++ b/src/pkg/filters/filters.go @@ -28,10 +28,14 @@ const ( IdentityValue // "foo" is a prefix of "foobarbaz" TargetPrefixes + // "baz" is a suffix of "foobarbaz" + TargetSuffixes // "foo" equals any complete element prefix of "foo/bar/baz" TargetPathPrefix // "foo" equals any complete element in "foo/bar/baz" TargetPathContains + // "baz" equals any complete element suffix of "foo/bar/baz" + TargetPathSuffix ) func norm(s string) string { @@ -161,6 +165,18 @@ func NotPrefix(target string) Filter { return newFilter(TargetPrefixes, target, true) } +// Suffix creates a filter where Compare(v) is true if +// target.Suffix(v) +func Suffix(target string) Filter { + return newFilter(TargetSuffixes, target, false) +} + +// NotSuffix creates a filter where Compare(v) is true if +// !target.Suffix(v) +func NotSuffix(target string) Filter { + return newFilter(TargetSuffixes, target, true) +} + // PathPrefix creates a filter where Compare(v) is true if // target.Prefix(v) && // split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 @@ -241,6 +257,44 @@ func NotPathContains(targets []string) Filter { return newSliceFilter(TargetPathContains, targets, tgts, true) } +// PathSuffix creates a filter where Compare(v) is true if +// target.Suffix(v) && +// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 +// ex: target "/bar/baz" returns true for input "/foo/bar/baz", +// but false for "/foobar/baz" +// +// Unlike single-target filters, this filter accepts a +// slice of targets, will compare an input against each target +// independently, and returns true if one or more of the +// comparisons succeed. +func PathSuffix(targets []string) Filter { + tgts := make([]string, len(targets)) + for i := range targets { + tgts[i] = normPathElem(targets[i]) + } + + return newSliceFilter(TargetPathSuffix, targets, tgts, false) +} + +// NotPathSuffix creates a filter where Compare(v) is true if +// !target.Suffix(v) || +// !split(target)[i].Equals(split(v)[i]) for _any_ i in 0..len(target)-1 +// ex: target "/bar/baz" returns false for input "/foo/bar/baz", +// but true for "/foobar/baz" +// +// Unlike single-target filters, this filter accepts a +// slice of targets, will compare an input against each target +// independently, and returns true if one or more of the +// comparisons succeed. +func NotPathSuffix(targets []string) Filter { + tgts := make([]string, len(targets)) + for i := range targets { + tgts[i] = normPathElem(targets[i]) + } + + return newSliceFilter(TargetPathSuffix, targets, tgts, true) +} + // newFilter is the standard filter constructor. func newFilter(c comparator, target string, negate bool) Filter { return Filter{ @@ -302,12 +356,17 @@ func (f Filter) Compare(input string) bool { cmp = in case TargetPrefixes: cmp = prefixed + case TargetSuffixes: + cmp = suffixed case TargetPathPrefix: cmp = pathPrefix hasSlice = true case TargetPathContains: cmp = pathContains hasSlice = true + case TargetPathSuffix: + cmp = pathSuffix + hasSlice = true case Passes: return true case Fails: @@ -364,6 +423,11 @@ func prefixed(target, input string) bool { return strings.HasPrefix(input, target) } +// true if target has input as a suffix. +func suffixed(target, input string) bool { + return strings.HasSuffix(input, target) +} + // true if target is an _element complete_ prefix match // on the input. Element complete means we do not // succeed on partial element matches (ex: "/foo" does @@ -393,6 +457,20 @@ func pathContains(target, input string) bool { return strings.Contains(normPathElem(input), target) } +// true if target is an _element complete_ suffix match +// on the input. Element complete means we do not +// succeed on partial element matches (ex: "/bar" does +// not match "/foobar"). +// +// As a precondition, assumes the target value has been +// passed through normPathElem(). +// +// The input is assumed to be the complete path that may +// have the target as a suffix. +func pathSuffix(target, input string) bool { + return strings.HasSuffix(normPathElem(input), target) +} + // ---------------------------------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------------------------------- @@ -405,8 +483,10 @@ var prefixString = map[comparator]string{ TargetContains: "cont:", TargetIn: "in:", TargetPrefixes: "pfx:", + TargetSuffixes: "sfx:", TargetPathPrefix: "pathPfx:", TargetPathContains: "pathCont:", + TargetPathSuffix: "pathSfx:", } func (f Filter) String() string { diff --git a/src/pkg/filters/filters_test.go b/src/pkg/filters/filters_test.go index 99b94467c..d6b361db4 100644 --- a/src/pkg/filters/filters_test.go +++ b/src/pkg/filters/filters_test.go @@ -206,6 +206,31 @@ func (suite *FiltersSuite) TestPrefixes() { } } +func (suite *FiltersSuite) TestSuffixes() { + target := "folderB" + f := filters.Suffix(target) + nf := filters.NotSuffix(target) + + table := []struct { + name string + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"Exact match - same case", "folderB", assert.True, assert.False}, + {"Exact match - different case", "Folderb", assert.True, assert.False}, + {"Suffix match - same case", "folderA/folderB", assert.True, assert.False}, + {"Suffix match - different case", "Foldera/folderb", assert.True, assert.False}, + {"Should not match substring", "folderB/folder1", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expectF(t, f.Compare(test.input), "filter") + test.expectNF(t, nf.Compare(test.input), "negated filter") + }) + } +} + func (suite *FiltersSuite) TestPathPrefix() { table := []struct { name string @@ -360,3 +385,79 @@ func (suite *FiltersSuite) TestPathContains_NormalizedTargets() { }) } } + +func (suite *FiltersSuite) TestPathSuffix() { + table := []struct { + name string + targets []string + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"Exact - same case", []string{"fA"}, "/fA", assert.True, assert.False}, + {"Exact - different case", []string{"fa"}, "/fA", assert.True, assert.False}, + {"Suffix - same case", []string{"fB"}, "/fA/fB", assert.True, assert.False}, + {"Suffix - different case", []string{"fb"}, "/fA/fB", assert.True, assert.False}, + {"Exact - multiple folders", []string{"fA/fB"}, "/fA/fB", assert.True, assert.False}, + {"Suffix - single folder partial", []string{"f"}, "/fA/fB", assert.False, assert.True}, + {"Suffix - multi folder partial", []string{"A/fB"}, "/fA/fB", assert.False, assert.True}, + {"Target Longer - single folder", []string{"fA"}, "/f", assert.False, assert.True}, + {"Target Longer - multi folder", []string{"fA/fB"}, "/fA/f", assert.False, assert.True}, + {"Not suffix - single folder", []string{"fA"}, "/af", assert.False, assert.True}, + {"Not suffix - multi folder", []string{"fA/fB"}, "/Af/fB", assert.False, assert.True}, + {"Exact - target variations - none", []string{"fA"}, "/fA", assert.True, assert.False}, + {"Exact - target variations - prefix", []string{"/fA"}, "/fA", assert.True, assert.False}, + {"Exact - target variations - suffix", []string{"fA/"}, "/fA", assert.True, assert.False}, + {"Exact - target variations - both", []string{"/fA/"}, "/fA", assert.True, assert.False}, + {"Exact - input variations - none", []string{"fA"}, "fA", assert.True, assert.False}, + {"Exact - input variations - prefix", []string{"fA"}, "/fA", assert.True, assert.False}, + {"Exact - input variations - suffix", []string{"fA"}, "fA/", assert.True, assert.False}, + {"Exact - input variations - both", []string{"fA"}, "/fA/", assert.True, assert.False}, + {"Suffix - target variations - none", []string{"fb"}, "/fA/fb", assert.True, assert.False}, + {"Suffix - target variations - prefix", []string{"/fb"}, "/fA/fb", assert.True, assert.False}, + {"Suffix - target variations - suffix", []string{"fb/"}, "/fA/fb", assert.True, assert.False}, + {"Suffix - target variations - both", []string{"/fb/"}, "/fA/fb", assert.True, assert.False}, + {"Suffix - input variations - none", []string{"fb"}, "fA/fb", assert.True, assert.False}, + {"Suffix - input variations - prefix", []string{"fb"}, "/fA/fb", assert.True, assert.False}, + {"Suffix - input variations - suffix", []string{"fb"}, "fA/fb/", assert.True, assert.False}, + {"Suffix - input variations - both", []string{"fb"}, "/fA/fb/", assert.True, assert.False}, + {"Slice - one matches", []string{"foo", "fa/f", "fb"}, "/fA/fb", assert.True, assert.True}, + {"Slice - none match", []string{"foo", "fa/f", "f"}, "/fA/fb", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + f := filters.PathSuffix(test.targets) + nf := filters.NotPathSuffix(test.targets) + + test.expectF(t, f.Compare(test.input), "filter") + test.expectNF(t, nf.Compare(test.input), "negated filter") + }) + } +} + +func (suite *FiltersSuite) TestPathSuffix_NormalizedTargets() { + table := []struct { + name string + targets []string + expect []string + }{ + {"Single - no slash", []string{"fA"}, []string{"/fA/"}}, + {"Single - pre slash", []string{"/fA"}, []string{"/fA/"}}, + {"Single - suff slash", []string{"fA/"}, []string{"/fA/"}}, + {"Single - both slashes", []string{"/fA/"}, []string{"/fA/"}}, + {"Multipath - no slash", []string{"fA/fB"}, []string{"/fA/fB/"}}, + {"Multipath - pre slash", []string{"/fA/fB"}, []string{"/fA/fB/"}}, + {"Multipath - suff slash", []string{"fA/fB/"}, []string{"/fA/fB/"}}, + {"Multipath - both slashes", []string{"/fA/fB/"}, []string{"/fA/fB/"}}, + {"Multi input - no slash", []string{"fA", "fB"}, []string{"/fA/", "/fB/"}}, + {"Multi input - pre slash", []string{"/fA", "/fB"}, []string{"/fA/", "/fB/"}}, + {"Multi input - suff slash", []string{"fA/", "fB/"}, []string{"/fA/", "/fB/"}}, + {"Multi input - both slashes", []string{"/fA/", "/fB/"}, []string{"/fA/", "/fB/"}}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + f := filters.PathSuffix(test.targets) + assert.Equal(t, test.expect, f.NormalizedTargets) + }) + } +}