add pathEquals to filter set (#2395)

## Description

Adds a pathEquals filter, and a corresponding
option for setting pathEquals in the scope options.
This will enable tests to restrict calendar lookups
to only the primary calendar folder, instead of
including all children underneath.

## Does this PR need a docs update or release note?

- [x]  No 

## Type of change

- [x] 🤖 Test

## Issue(s)

* #2388

## Test Plan

- [x]  Unit test
This commit is contained in:
Keepers 2023-02-08 14:13:45 -07:00 committed by GitHub
parent a7fd90b2f8
commit 47f5ca1d95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 1 deletions

View File

@ -28,9 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add versions to backups so that we can understand/handle older backup formats - Add versions to backups so that we can understand/handle older backup formats
### Fixed ### Fixed
- Backing up a calendar that has the same name as the default calendar
- Added additional backoff-retry to all OneDrive queries. - Added additional backoff-retry to all OneDrive queries.
- Users with `null` userType values are no longer excluded from user queries. - Users with `null` userType values are no longer excluded from user queries.
- Fix bug when backing up a calendar that has the same name as the default calendar
### Known Issues ### Known Issues

View File

@ -36,6 +36,8 @@ const (
TargetPathContains TargetPathContains
// "baz" equals any complete element suffix of "foo/bar/baz" // "baz" equals any complete element suffix of "foo/bar/baz"
TargetPathSuffix TargetPathSuffix
// "foo/bar/baz" equals the complete path "foo/bar/baz"
TargetPathEquals
) )
func norm(s string) string { func norm(s string) string {
@ -295,6 +297,44 @@ func NotPathSuffix(targets []string) Filter {
return newSliceFilter(TargetPathSuffix, targets, tgts, true) return newSliceFilter(TargetPathSuffix, targets, tgts, true)
} }
// PathEquals creates a filter where Compare(v) is true if
// target.Equals(v) &&
// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1
// ex: target "foo" returns true for inputs "/foo/", "/foo", and "foo/"
// but false for "/foo/bar", "bar/foo/", and "/foobar/"
//
// 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 PathEquals(targets []string) Filter {
tgts := make([]string, len(targets))
for i := range targets {
tgts[i] = normPathElem(targets[i])
}
return newSliceFilter(TargetPathEquals, targets, tgts, false)
}
// NotPathEquals creates a filter where Compare(v) is true if
// !target.Equals(v) ||
// !split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1
// ex: target "foo" returns true "/foo/bar", "bar/foo/", and "/foobar/"
// but false for for inputs "/foo/", "/foo", and "foo/"
//
// 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 NotPathEquals(targets []string) Filter {
tgts := make([]string, len(targets))
for i := range targets {
tgts[i] = normPathElem(targets[i])
}
return newSliceFilter(TargetPathEquals, targets, tgts, true)
}
// newFilter is the standard filter constructor. // newFilter is the standard filter constructor.
func newFilter(c comparator, target string, negate bool) Filter { func newFilter(c comparator, target string, negate bool) Filter {
return Filter{ return Filter{
@ -367,6 +407,9 @@ func (f Filter) Compare(input string) bool {
case TargetPathSuffix: case TargetPathSuffix:
cmp = pathSuffix cmp = pathSuffix
hasSlice = true hasSlice = true
case TargetPathEquals:
cmp = pathEquals
hasSlice = true
case Passes: case Passes:
return true return true
case Fails: case Fails:
@ -471,6 +514,20 @@ func pathSuffix(target, input string) bool {
return strings.HasSuffix(normPathElem(input), target) return strings.HasSuffix(normPathElem(input), target)
} }
// true if target is an _exact_ match on the input, excluding
// path delmiters. 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
// match the target.
func pathEquals(target, input string) bool {
return normPathElem(input) == target
}
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// Helpers // Helpers
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
@ -487,6 +544,7 @@ var prefixString = map[comparator]string{
TargetPathPrefix: "pathPfx:", TargetPathPrefix: "pathPfx:",
TargetPathContains: "pathCont:", TargetPathContains: "pathCont:",
TargetPathSuffix: "pathSfx:", TargetPathSuffix: "pathSfx:",
TargetPathEquals: "pathEq:",
} }
func (f Filter) String() string { func (f Filter) String() string {

View File

@ -461,3 +461,70 @@ func (suite *FiltersSuite) TestPathSuffix_NormalizedTargets() {
}) })
} }
} }
func (suite *FiltersSuite) TestPathEquals() {
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},
{"Exact - multiple folders", []string{"fA/fB"}, "/fA/fB", assert.True, assert.False},
{"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},
{"Partial match", []string{"f"}, "/fA/", assert.False, assert.True},
{"Suffix - same case", []string{"fB"}, "/fA/fB", assert.False, assert.True},
{"Suffix - different case", []string{"fb"}, "/fA/fB", assert.False, assert.True},
{"Prefix - same case", []string{"fA"}, "/fA/fB", assert.False, assert.True},
{"Prefix - different case", []string{"fa"}, "/fA/fB", assert.False, assert.True},
{"Contains - same case", []string{"fB"}, "/fA/fB/fC", assert.False, assert.True},
{"Contains - different case", []string{"fb"}, "/fA/fB/fC", assert.False, assert.True},
{"Slice - one matches", []string{"foo", "/fA/fb", "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.PathEquals(test.targets)
nf := filters.NotPathEquals(test.targets)
test.expectF(t, f.Compare(test.input), "filter")
test.expectNF(t, nf.Compare(test.input), "negated filter")
})
}
}
func (suite *FiltersSuite) TestPathEquals_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.PathEquals(test.targets)
assert.Equal(t, test.expect, f.NormalizedTargets)
})
}
}

View File

@ -343,6 +343,7 @@ type scopeConfig struct {
usePathFilter bool usePathFilter bool
usePrefixFilter bool usePrefixFilter bool
useSuffixFilter bool useSuffixFilter bool
useEqualsFilter bool
} }
type option func(*scopeConfig) type option func(*scopeConfig)
@ -371,6 +372,15 @@ func SuffixMatch() option {
} }
} }
// ExactMatch ensures the selector uses an Equals comparator, instead
// of contains. Will not override a default Any() or None()
// comparator.
func ExactMatch() option {
return func(sc *scopeConfig) {
sc.useEqualsFilter = true
}
}
// pathComparator is an internal-facing option. It is assumed that scope // pathComparator is an internal-facing option. It is assumed that scope
// constructors will provide the pathComparator option whenever a folder- // constructors will provide the pathComparator option whenever a folder-
// level scope (ie, a scope that compares path hierarchies) is created. // level scope (ie, a scope that compares path hierarchies) is created.
@ -433,6 +443,10 @@ func filterize(sc scopeConfig, s ...string) filters.Filter {
} }
if sc.usePathFilter { if sc.usePathFilter {
if sc.useEqualsFilter {
return filters.PathEquals(s)
}
if sc.usePrefixFilter { if sc.usePrefixFilter {
return filters.PathPrefix(s) return filters.PathPrefix(s)
} }