diff --git a/src/pkg/filters/filters.go b/src/pkg/filters/filters.go index c95582ad1..e716d6cda 100644 --- a/src/pkg/filters/filters.go +++ b/src/pkg/filters/filters.go @@ -24,6 +24,9 @@ const ( Fails // passthrough for the target IdentityValue + // target is a prefix of the value it is compared + // against + TargetPrefixes ) func norm(s string) string { @@ -119,6 +122,18 @@ func Identity(id string) Filter { return newFilter(IdentityValue, id, false) } +// Prefix creates a filter where Compare(v) is true if +// target.Prefix(v) +func Prefix(target string) Filter { + return newFilter(TargetPrefixes, target, false) +} + +// NotPrefix creates a filter where Compare(v) is true if +// !target.Prefix(v) +func NotPrefix(target string) Filter { + return newFilter(TargetPrefixes, target, true) +} + // newFilter is the standard filter constructor. func newFilter(c comparator, target string, negate bool) Filter { return Filter{c, target, negate} @@ -143,6 +158,8 @@ func (f Filter) Compare(input string) bool { cmp = contains case TargetIn: cmp = in + case TargetPrefixes: + cmp = prefixed case Passes: return true case Fails: @@ -182,6 +199,11 @@ func in(target, input string) bool { return strings.Contains(input, target) } +// true if target has input as a prefix. +func prefixed(target, input string) bool { + return strings.HasPrefix(target, input) +} + // ---------------------------------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------------------------------- @@ -193,6 +215,7 @@ var prefixString = map[comparator]string{ LessThan: "lt:", TargetContains: "cont:", TargetIn: "in:", + TargetPrefixes: "pr:", } func (f Filter) String() string { diff --git a/src/pkg/filters/filters_test.go b/src/pkg/filters/filters_test.go index f11abc8cb..bfa7237fa 100644 --- a/src/pkg/filters/filters_test.go +++ b/src/pkg/filters/filters_test.go @@ -159,3 +159,28 @@ func (suite *FiltersSuite) TestIn_Joined() { }) } } + +func (suite *FiltersSuite) TestPrefixes() { + input := "folderA" + + table := []struct { + name string + target string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"Exact match - same case", "folderA", assert.True, assert.False}, + {"Exact match - different case", "Foldera", assert.True, assert.False}, + {"Prefix match - same case", "folderA/folderB", assert.True, assert.False}, + {"Prefix match - different case", "Foldera/folderB", assert.True, assert.False}, + {"Should not match substring", "folder1/folderA", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + f := filters.Prefix(test.target) + nf := filters.NotPrefix(test.target) + test.expectF(t, f.Compare(input), "filter") + test.expectNF(t, nf.Compare(input), "negated filter") + }) + } +} diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index 207b8a1ac..6fccbd5a9 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -847,6 +847,10 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_MatchesPath() { {"no folders", es.MailFolders(Any(), None()), "", assert.False}, {"matching folder", es.MailFolders(Any(), []string{fld}), "", assert.True}, {"non-matching folder", es.MailFolders(Any(), []string{"smarf"}), "", assert.False}, + // This test validates that folders that match a substring of the scope are not included (bugfix 1) + {"non-matching folder substring", es.MailFolders(Any(), []string{fld + "_suffix"}), "", assert.False}, + {"matching folder prefix", es.MailFolders(Any(), []string{"mailF"}), "", assert.True}, + {"matching folder substring", es.MailFolders(Any(), []string{"Folder"}), "", assert.False}, {"one of multiple folders", es.MailFolders(Any(), []string{"smarf", fld}), "", assert.True}, {"all mail", es.Mails(Any(), Any(), Any()), "", assert.True}, {"no mail", es.Mails(Any(), Any(), None()), "", assert.False}, @@ -913,9 +917,10 @@ func (suite *ExchangeSelectorSuite) TestIdPath() { func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() { var ( - contact = stubRepoRef(path.ExchangeService, path.ContactsCategory, "uid", "cfld", "cid") - event = stubRepoRef(path.ExchangeService, path.EventsCategory, "uid", "ecld", "eid") - mail = stubRepoRef(path.ExchangeService, path.EmailCategory, "uid", "mfld", "mid") + contact = stubRepoRef(path.ExchangeService, path.ContactsCategory, "uid", "cfld", "cid") + event = stubRepoRef(path.ExchangeService, path.EventsCategory, "uid", "ecld", "eid") + mail = stubRepoRef(path.ExchangeService, path.EmailCategory, "uid", "mfld", "mid") + contactInSubFolder = stubRepoRef(path.ExchangeService, path.ContactsCategory, "uid", "cfld1/cfld2", "cid") ) makeDeets := func(refs ...string) *details.Details { @@ -1020,6 +1025,16 @@ func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() { }, arr(contact), }, + { + "only match contactInSubFolder", + makeDeets(contactInSubFolder, contact, event, mail), + func() *ExchangeRestore { + er := NewExchangeRestore() + er.Include(er.ContactFolders([]string{"uid"}, []string{"cfld1/cfld2"})) + return er + }, + arr(contactInSubFolder), + }, { "only match event", makeDeets(contact, event, mail), diff --git a/src/pkg/selectors/scopes.go b/src/pkg/selectors/scopes.go index 8f38111b0..ba8035941 100644 --- a/src/pkg/selectors/scopes.go +++ b/src/pkg/selectors/scopes.go @@ -379,12 +379,44 @@ func matchesPathValues[T scopeT, C categoryT]( // all parts of the scope must match cc := c.(C) if !isAnyTarget(sc, cc) { - notMatch := filters.NotContains(join(scopeVals...)) - if c.isLeaf() && len(shortRef) > 0 { - if notMatch.Compare(pathVal) && notMatch.Compare(shortRef) { - return false + var ( + match = false + // Used to check if the path contains the value specified in scopeVals + pathHas = filters.Contains(pathVal) + // Used to check if the path has the value specified in scopeVal as a prefix + pathPrefix = filters.Prefix(pathVal) + // Used to check if the shortRef equals the value specified in scopeVals + shortRefEq = filters.Equal(shortRef) + ) + + for _, scopeVal := range scopeVals { + switch { + case c.isLeaf() && len(shortRef) > 0: + // Leaf category - we do a "contains" match for path or equality match on + // the shortRef + if pathHas.Compare(scopeVal) || shortRefEq.Compare(scopeVal) { + match = true + } + case !c.isLeaf() && c != c.rootCat(): + // Folder category - we check if the scope is a prefix + // TODO: If the scopeVal is not a "path" - then we'll want to check + // if any of the path elements match the scopeVal exactly + if pathPrefix.Compare(scopeVal) { + match = true + } + default: + if pathHas.Compare(scopeVal) { + match = true + } } - } else if notMatch.Compare(pathVal) { + // short circuit if we found a match + if match { + break + } + } + + if !match { + // Didn't match any scope return false } } diff --git a/src/pkg/selectors/selectors_reduce_test.go b/src/pkg/selectors/selectors_reduce_test.go index 53e7fd854..1c4c93580 100644 --- a/src/pkg/selectors/selectors_reduce_test.go +++ b/src/pkg/selectors/selectors_reduce_test.go @@ -164,6 +164,85 @@ func (suite *SelectorReduceSuite) TestReduce() { testdata.ExchangeEventsItems..., ), }, + { + name: "ExchangeMailByFolder", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.MailFolders( + selectors.Any(), + []string{testdata.ExchangeEmailBasePath.Folder()}, + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEmailItems[0]}, + }, + { + name: "ExchangeMailByFolderRoot", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.MailFolders( + selectors.Any(), + []string{testdata.ExchangeEmailInboxPath.Folder()}, + )) + + return sel + }, + expected: testdata.ExchangeEmailItems, + }, + { + name: "ExchangeContactByFolder", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.ContactFolders( + selectors.Any(), + []string{testdata.ExchangeContactsBasePath.Folder()}, + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeContactsItems[0]}, + }, + { + name: "ExchangeContactByFolderRoot", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.ContactFolders( + selectors.Any(), + []string{testdata.ExchangeContactsRootPath.Folder()}, + )) + + return sel + }, + expected: testdata.ExchangeContactsItems, + }, + + { + name: "ExchangeEventsByFolder", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.EventCalendars( + selectors.Any(), + []string{testdata.ExchangeEventsBasePath.Folder()}, + )) + + return sel + }, + expected: []details.DetailsEntry{testdata.ExchangeEventsItems[0]}, + }, + { + name: "ExchangeEventsByFolderRoot", + selFunc: func() selectors.Reducer { + sel := selectors.NewExchangeRestore() + sel.Include(sel.EventCalendars( + selectors.Any(), + []string{testdata.ExchangeEventsRootPath.Folder()}, + )) + + return sel + }, + expected: testdata.ExchangeEventsItems, + }, } for _, test := range table { diff --git a/src/pkg/selectors/testdata/details.go b/src/pkg/selectors/testdata/details.go index 5772a94a6..167f89928 100644 --- a/src/pkg/selectors/testdata/details.go +++ b/src/pkg/selectors/testdata/details.go @@ -42,9 +42,11 @@ var ( Time1 = time.Date(2022, 9, 21, 10, 0, 0, 0, time.UTC) Time2 = time.Date(2022, 10, 21, 10, 0, 0, 0, time.UTC) - ExchangeEmailBasePath = mustParsePath("tenant-id/exchange/user-id/email/Inbox/subfolder", false) + ExchangeEmailInboxPath = mustParsePath("tenant-id/exchange/user-id/email/Inbox", false) + ExchangeEmailBasePath = mustAppendPath(ExchangeEmailInboxPath, "subfolder", false) + ExchangeEmailBasePath2 = mustAppendPath(ExchangeEmailInboxPath, "othersubfolder", false) ExchangeEmailItemPath1 = mustAppendPath(ExchangeEmailBasePath, ItemName1, true) - ExchangeEmailItemPath2 = mustAppendPath(ExchangeEmailBasePath, ItemName2, true) + ExchangeEmailItemPath2 = mustAppendPath(ExchangeEmailBasePath2, ItemName2, true) ExchangeEmailItems = []details.DetailsEntry{ { @@ -75,9 +77,11 @@ var ( }, } - ExchangeContactsBasePath = mustParsePath("tenant-id/exchange/user-id/contacts/contacts", false) + ExchangeContactsRootPath = mustParsePath("tenant-id/exchange/user-id/contacts/contacts", false) + ExchangeContactsBasePath = mustAppendPath(ExchangeContactsRootPath, "contacts", false) + ExchangeContactsBasePath2 = mustAppendPath(ExchangeContactsRootPath, "morecontacts", false) ExchangeContactsItemPath1 = mustAppendPath(ExchangeContactsBasePath, ItemName1, true) - ExchangeContactsItemPath2 = mustAppendPath(ExchangeContactsBasePath, ItemName2, true) + ExchangeContactsItemPath2 = mustAppendPath(ExchangeContactsBasePath2, ItemName2, true) ExchangeContactsItems = []details.DetailsEntry{ { @@ -104,9 +108,11 @@ var ( }, } - ExchangeEventsBasePath = mustParsePath("tenant-id/exchange/user-id/events/holidays", false) + ExchangeEventsRootPath = mustParsePath("tenant-id/exchange/user-id/events/holidays", false) + ExchangeEventsBasePath = mustAppendPath(ExchangeEventsRootPath, "holidays", false) + ExchangeEventsBasePath2 = mustAppendPath(ExchangeEventsRootPath, "moreholidays", false) ExchangeEventsItemPath1 = mustAppendPath(ExchangeEventsBasePath, ItemName1, true) - ExchangeEventsItemPath2 = mustAppendPath(ExchangeEventsBasePath, ItemName2, true) + ExchangeEventsItemPath2 = mustAppendPath(ExchangeEventsBasePath2, ItemName2, true) ExchangeEventsItems = []details.DetailsEntry{ {