prevent cross-contamination on filter reduce (#898)

## Description

Over-restrictive scope correlation caused the
reduce processor to only include filters which
matched the data type of the scope.  Ironically,
this allowed a superset of information to match,
by evading the _all-match_ expectations of
filter scopes.

Also replaces the data category consts inside /details
with the path category types, since those are acting as
better canonical owners of data type identification
throughout the app.

## Type of change

- [x] 🐛 Bugfix

## Issue(s)

* #890

## Test Plan

- [x] 💪 Manual
- [x]  Unit test
This commit is contained in:
Keepers 2022-09-19 18:34:51 -06:00 committed by GitHub
parent b2d3330db7
commit 41bb3ee6f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 297 additions and 131 deletions

View File

@ -39,7 +39,7 @@ func (dm DetailsModel) PrintEntries(ctx context.Context) {
} }
func printTable(ctx context.Context, dm DetailsModel) { func printTable(ctx context.Context, dm DetailsModel) {
perType := map[itemType][]print.Printable{} perType := map[ItemType][]print.Printable{}
for _, de := range dm.Entries { for _, de := range dm.Entries {
it := de.infoType() it := de.infoType()
@ -226,21 +226,21 @@ func (de DetailsEntry) Values() []string {
return vs return vs
} }
type itemType int type ItemType int
const ( const (
UnknownType itemType = iota UnknownType ItemType = iota
// separate each service by a factor of 100 for padding // separate each service by a factor of 100 for padding
ExchangeContact ExchangeContact
ExchangeEvent ExchangeEvent
ExchangeMail ExchangeMail
SharepointItem itemType = iota + 100 SharepointItem ItemType = iota + 100
OneDriveItem itemType = iota + 200 OneDriveItem ItemType = iota + 200
FolderItem itemType = iota + 300 FolderItem ItemType = iota + 300
) )
// ItemInfo is a oneOf that contains service specific // ItemInfo is a oneOf that contains service specific
@ -258,7 +258,7 @@ type ItemInfo struct {
// infoType provides internal categorization for collecting like-typed ItemInfos. // infoType provides internal categorization for collecting like-typed ItemInfos.
// It should return the most granular value type (ex: "event" for an exchange // It should return the most granular value type (ex: "event" for an exchange
// calendar event). // calendar event).
func (i ItemInfo) infoType() itemType { func (i ItemInfo) infoType() ItemType {
switch { switch {
case i.Folder != nil: case i.Folder != nil:
return i.Folder.ItemType return i.Folder.ItemType
@ -277,8 +277,8 @@ func (i ItemInfo) infoType() itemType {
} }
type FolderInfo struct { type FolderInfo struct {
ItemType itemType ItemType ItemType `json:"itemType,omitempty"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
} }
func (i FolderInfo) Headers() []string { func (i FolderInfo) Headers() []string {
@ -291,7 +291,7 @@ func (i FolderInfo) Values() []string {
// ExchangeInfo describes an exchange item // ExchangeInfo describes an exchange item
type ExchangeInfo struct { type ExchangeInfo struct {
ItemType itemType ItemType ItemType `json:"itemType,omitempty"`
Sender string `json:"sender,omitempty"` Sender string `json:"sender,omitempty"`
Subject string `json:"subject,omitempty"` Subject string `json:"subject,omitempty"`
Received time.Time `json:"received,omitempty"` Received time.Time `json:"received,omitempty"`
@ -344,7 +344,7 @@ func (i ExchangeInfo) Values() []string {
// TODO: Implement this. This is currently here // TODO: Implement this. This is currently here
// just to illustrate usage // just to illustrate usage
type SharepointInfo struct { type SharepointInfo struct {
ItemType itemType ItemType ItemType `json:"itemType,omitempty"`
} }
// Headers returns the human-readable names of properties in a SharepointInfo // Headers returns the human-readable names of properties in a SharepointInfo
@ -361,9 +361,9 @@ func (i SharepointInfo) Values() []string {
// OneDriveInfo describes a oneDrive item // OneDriveInfo describes a oneDrive item
type OneDriveInfo struct { type OneDriveInfo struct {
ItemType itemType ItemType ItemType `json:"itemType,omitempty"`
ParentPath string `json:"parentPath"` ParentPath string `json:"parentPath"`
ItemName string `json:"itemName"` ItemName string `json:"itemName"`
} }
// Headers returns the human-readable names of properties in a OneDriveInfo // Headers returns the human-readable names of properties in a OneDriveInfo

View File

@ -333,8 +333,8 @@ func (sr *ExchangeRestore) EventRecurs(recurs string) []ExchangeScope {
func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope { func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope {
return []ExchangeScope{ return []ExchangeScope{
makeFilterScope[ExchangeScope]( makeFilterScope[ExchangeScope](
ExchangeMail, ExchangeEvent,
ExchangeFilterMailReceivedAfter, ExchangeFilterEventStartsAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), wrapFilter(filters.Less)),
} }
@ -347,8 +347,8 @@ func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope
func (sr *ExchangeRestore) EventStartsBefore(timeStrings string) []ExchangeScope { func (sr *ExchangeRestore) EventStartsBefore(timeStrings string) []ExchangeScope {
return []ExchangeScope{ return []ExchangeScope{
makeFilterScope[ExchangeScope]( makeFilterScope[ExchangeScope](
ExchangeMail, ExchangeEvent,
ExchangeFilterMailReceivedBefore, ExchangeFilterEventStartsBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), wrapFilter(filters.Greater)),
} }
@ -690,25 +690,21 @@ func (s exchange) Reduce(ctx context.Context, deets *details.Details) *details.D
) )
} }
// matchesEntry returns true if either the path or the info in the exchangeEntry matches the scope details.
func (s ExchangeScope) matchesEntry(
cat categorizer,
pathValues map[categorizer]string,
entry details.DetailsEntry,
) bool {
// matchesPathValues can be handled generically, thanks to SCIENCE.
return matchesPathValues(s, cat.(exchangeCategory), pathValues, entry.ShortRef) || s.matchesInfo(entry.Exchange)
}
// matchesInfo handles the standard behavior when comparing a scope and an ExchangeFilter // matchesInfo handles the standard behavior when comparing a scope and an ExchangeFilter
// returns true if the scope and info match for the provided category. // returns true if the scope and info match for the provided category.
func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool { func (s ExchangeScope) matchesInfo(dii details.ItemInfo) bool {
// we need values to match against info := dii.Exchange
if info == nil { if info == nil {
return false return false
} }
filterCat := s.FilterCategory() filterCat := s.FilterCategory()
cfpc := categoryFromItemType(info.ItemType)
if !typeAndCategoryMatches(filterCat, cfpc) {
return false
}
i := "" i := ""
switch filterCat { switch filterCat {
@ -732,3 +728,19 @@ func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool {
return s.Matches(filterCat, i) return s.Matches(filterCat, i)
} }
// categoryFromItemType interprets the category represented by the ExchangeInfo
// struct. Since every ExchangeInfo can hold all exchange data info, the exact
// type that the struct represents must be compared using its ItemType prop.
func categoryFromItemType(pct details.ItemType) exchangeCategory {
switch pct {
case details.ExchangeContact:
return ExchangeContact
case details.ExchangeMail:
return ExchangeMail
case details.ExchangeEvent:
return ExchangeEvent
}
return ExchangeCategoryUnknown
}

View File

@ -710,62 +710,99 @@ func (suite *ExchangeSelectorSuite) TestExchangeScope_MatchesInfo() {
epoch = time.Time{} epoch = time.Time{}
now = time.Now() now = time.Now()
future = now.Add(1 * time.Minute) future = now.Add(1 * time.Minute)
info = &details.ExchangeInfo{
ContactName: name,
EventRecurs: true,
EventStart: now,
Organizer: organizer,
Sender: sender,
Subject: subject,
Received: now,
}
) )
infoWith := func(itype details.ItemType) details.ItemInfo {
return details.ItemInfo{
Exchange: &details.ExchangeInfo{
ItemType: itype,
ContactName: name,
EventRecurs: true,
EventStart: now,
Organizer: organizer,
Sender: sender,
Subject: subject,
Received: now,
},
}
}
table := []struct { table := []struct {
name string name string
itype details.ItemType
scope []ExchangeScope scope []ExchangeScope
expect assert.BoolAssertionFunc expect assert.BoolAssertionFunc
}{ }{
{"any mail with a sender", es.MailSender(AnyTgt), assert.True}, {"any mail with a sender", details.ExchangeMail, es.MailSender(AnyTgt), assert.True},
{"no mail, regardless of sender", es.MailSender(NoneTgt), assert.False}, {"no mail, regardless of sender", details.ExchangeMail, es.MailSender(NoneTgt), assert.False},
{"mail from a different sender", es.MailSender("magoo@ma.goo"), assert.False}, {"mail from a different sender", details.ExchangeMail, es.MailSender("magoo@ma.goo"), assert.False},
{"mail from the matching sender", es.MailSender(sender), assert.True}, {"mail from the matching sender", details.ExchangeMail, es.MailSender(sender), assert.True},
{"mail with any subject", es.MailSubject(AnyTgt), assert.True}, {"mail with any subject", details.ExchangeMail, es.MailSubject(AnyTgt), assert.True},
{"mail with none subject", es.MailSubject(NoneTgt), assert.False}, {"mail with none subject", details.ExchangeMail, es.MailSubject(NoneTgt), assert.False},
{"mail with a different subject", es.MailSubject("fancy"), assert.False}, {"mail with a different subject", details.ExchangeMail, es.MailSubject("fancy"), assert.False},
{"mail with the matching subject", es.MailSubject(subject), assert.True}, {"mail with the matching subject", details.ExchangeMail, es.MailSubject(subject), assert.True},
{"mail with a substring subject match", es.MailSubject(subject[5:9]), assert.True}, {"mail with a substring subject match", details.ExchangeMail, es.MailSubject(subject[5:9]), assert.True},
{"mail received after the epoch", es.MailReceivedAfter(common.FormatTime(epoch)), assert.True}, {"mail received after the epoch", details.ExchangeMail, es.MailReceivedAfter(common.FormatTime(epoch)), assert.True},
{"mail received after now", es.MailReceivedAfter(common.FormatTime(now)), assert.False}, {"mail received after now", details.ExchangeMail, es.MailReceivedAfter(common.FormatTime(now)), assert.False},
{"mail received after sometime later", es.MailReceivedAfter(common.FormatTime(future)), assert.False}, {
{"mail received before the epoch", es.MailReceivedBefore(common.FormatTime(epoch)), assert.False}, "mail received after sometime later",
{"mail received before now", es.MailReceivedBefore(common.FormatTime(now)), assert.False}, details.ExchangeMail,
{"mail received before sometime later", es.MailReceivedBefore(common.FormatTime(future)), assert.True}, es.MailReceivedAfter(common.FormatTime(future)),
{"event with any organizer", es.EventOrganizer(AnyTgt), assert.True}, assert.False,
{"event with none organizer", es.EventOrganizer(NoneTgt), assert.False}, },
{"event with a different organizer", es.EventOrganizer("fancy"), assert.False}, {
{"event with the matching organizer", es.EventOrganizer(organizer), assert.True}, "mail received before the epoch",
{"event that recurs", es.EventRecurs("true"), assert.True}, details.ExchangeMail,
{"event that does not recur", es.EventRecurs("false"), assert.False}, es.MailReceivedBefore(common.FormatTime(epoch)),
{"event starting after the epoch", es.EventStartsAfter(common.FormatTime(epoch)), assert.True}, assert.False,
{"event starting after now", es.EventStartsAfter(common.FormatTime(now)), assert.False}, },
{"event starting after sometime later", es.EventStartsAfter(common.FormatTime(future)), assert.False}, {"mail received before now", details.ExchangeMail, es.MailReceivedBefore(common.FormatTime(now)), assert.False},
{"event starting before the epoch", es.EventStartsBefore(common.FormatTime(epoch)), assert.False}, {
{"event starting before now", es.EventStartsBefore(common.FormatTime(now)), assert.False}, "mail received before sometime later",
{"event starting before sometime later", es.EventStartsBefore(common.FormatTime(future)), assert.True}, details.ExchangeMail,
{"event with any subject", es.EventSubject(AnyTgt), assert.True}, es.MailReceivedBefore(common.FormatTime(future)),
{"event with none subject", es.EventSubject(NoneTgt), assert.False}, assert.True,
{"event with a different subject", es.EventSubject("fancy"), assert.False}, },
{"event with the matching subject", es.EventSubject(subject), assert.True}, {"event with any organizer", details.ExchangeEvent, es.EventOrganizer(AnyTgt), assert.True},
{"contact with a different name", es.ContactName("blarps"), assert.False}, {"event with none organizer", details.ExchangeEvent, es.EventOrganizer(NoneTgt), assert.False},
{"contact with the same name", es.ContactName(name), assert.True}, {"event with a different organizer", details.ExchangeEvent, es.EventOrganizer("fancy"), assert.False},
{"contact with a subname search", es.ContactName(name[2:5]), assert.True}, {"event with the matching organizer", details.ExchangeEvent, es.EventOrganizer(organizer), assert.True},
{"event that recurs", details.ExchangeEvent, es.EventRecurs("true"), assert.True},
{"event that does not recur", details.ExchangeEvent, es.EventRecurs("false"), assert.False},
{"event starting after the epoch", details.ExchangeEvent, es.EventStartsAfter(common.FormatTime(epoch)), assert.True},
{"event starting after now", details.ExchangeEvent, es.EventStartsAfter(common.FormatTime(now)), assert.False},
{
"event starting after sometime later",
details.ExchangeEvent,
es.EventStartsAfter(common.FormatTime(future)),
assert.False,
},
{
"event starting before the epoch",
details.ExchangeEvent,
es.EventStartsBefore(common.FormatTime(epoch)),
assert.False,
},
{"event starting before now", details.ExchangeEvent, es.EventStartsBefore(common.FormatTime(now)), assert.False},
{
"event starting before sometime later",
details.ExchangeEvent,
es.EventStartsBefore(common.FormatTime(future)),
assert.True,
},
{"event with any subject", details.ExchangeEvent, es.EventSubject(AnyTgt), assert.True},
{"event with none subject", details.ExchangeEvent, es.EventSubject(NoneTgt), assert.False},
{"event with a different subject", details.ExchangeEvent, es.EventSubject("fancy"), assert.False},
{"event with the matching subject", details.ExchangeEvent, es.EventSubject(subject), assert.True},
{"contact with a different name", details.ExchangeContact, es.ContactName("blarps"), assert.False},
{"contact with the same name", details.ExchangeContact, es.ContactName(name), assert.True},
{"contact with a subname search", details.ExchangeContact, es.ContactName(name[2:5]), assert.True},
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
scopes := setScopesToDefault(test.scope) scopes := setScopesToDefault(test.scope)
for _, scope := range scopes { for _, scope := range scopes {
test.expect(t, scope.matchesInfo(info)) test.expect(t, scope.matchesInfo(infoWith(test.itype)))
} }
}) })
} }
@ -864,6 +901,12 @@ func (suite *ExchangeSelectorSuite) TestIdPath() {
} }
func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() { 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")
)
makeDeets := func(refs ...string) *details.Details { makeDeets := func(refs ...string) *details.Details {
deets := &details.Details{ deets := &details.Details{
DetailsModel: details.DetailsModel{ DetailsModel: details.DetailsModel{
@ -872,20 +915,30 @@ func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
} }
for _, r := range refs { for _, r := range refs {
itype := details.UnknownType
switch r {
case contact:
itype = details.ExchangeContact
case event:
itype = details.ExchangeEvent
case mail:
itype = details.ExchangeMail
}
deets.Entries = append(deets.Entries, details.DetailsEntry{ deets.Entries = append(deets.Entries, details.DetailsEntry{
RepoRef: r, RepoRef: r,
ItemInfo: details.ItemInfo{
Exchange: &details.ExchangeInfo{
ItemType: itype,
},
},
}) })
} }
return deets return deets
} }
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")
)
arr := func(s ...string) []string { arr := func(s ...string) []string {
return s return s
} }
@ -1009,6 +1062,44 @@ func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
}, },
arr(contact, event), arr(contact, event),
}, },
{
"filter on mail subject",
func() *details.Details {
ds := makeDeets(mail)
for i := range ds.Entries {
ds.Entries[i].Exchange.Subject = "has a subject"
}
return ds
}(),
func() *ExchangeRestore {
er := NewExchangeRestore()
er.Include(er.Users(Any()))
er.Filter(er.MailSubject("subj"))
return er
},
arr(mail),
},
{
"filter on mail subject multiple input categories",
func() *details.Details {
mds := makeDeets(mail)
for i := range mds.Entries {
mds.Entries[i].Exchange.Subject = "has a subject"
}
ds := makeDeets(contact, event)
ds.Entries = append(ds.Entries, mds.Entries...)
return ds
}(),
func() *ExchangeRestore {
er := NewExchangeRestore()
er.Include(er.Users(Any()))
er.Filter(er.MailSubject("subj"))
return er
},
arr(mail),
},
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
@ -1067,7 +1158,7 @@ func (suite *ExchangeSelectorSuite) TestScopesByCategory() {
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
result := scopesByCategory[ExchangeScope](test.scopes, cats) result := scopesByCategory[ExchangeScope](test.scopes, cats, false)
assert.Len(t, result[ExchangeContact], test.expect.contact) assert.Len(t, result[ExchangeContact], test.expect.contact)
assert.Len(t, result[ExchangeEvent], test.expect.event) assert.Len(t, result[ExchangeEvent], test.expect.event)
assert.Len(t, result[ExchangeMail], test.expect.mail) assert.Len(t, result[ExchangeMail], test.expect.mail)
@ -1288,3 +1379,38 @@ func (suite *ExchangeSelectorSuite) TestExchangeCategory_PathKeys() {
}) })
} }
} }
func (suite *ExchangeSelectorSuite) TestCategoryFromItemType() {
table := []struct {
name string
input details.ItemType
expect exchangeCategory
}{
{
name: "contact",
input: details.ExchangeContact,
expect: ExchangeContact,
},
{
name: "event",
input: details.ExchangeEvent,
expect: ExchangeEvent,
},
{
name: "mail",
input: details.ExchangeMail,
expect: ExchangeMail,
},
{
name: "unknown",
input: details.UnknownType,
expect: ExchangeCategoryUnknown,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
result := categoryFromItemType(test.input)
assert.Equal(t, test.expect, result)
})
}
}

View File

@ -52,7 +52,10 @@ func (mc mockCategorizer) isLeaf() bool {
} }
func (mc mockCategorizer) pathValues(pth path.Path) map[categorizer]string { func (mc mockCategorizer) pathValues(pth path.Path) map[categorizer]string {
return map[categorizer]string{rootCatStub: "stub"} return map[categorizer]string{
rootCatStub: "root",
leafCatStub: "leaf",
}
} }
func (mc mockCategorizer) pathKeys() []categorizer { func (mc mockCategorizer) pathKeys() []categorizer {
@ -86,11 +89,7 @@ func (ms mockScope) categorizer() categorizer {
return unknownCatStub return unknownCatStub
} }
func (ms mockScope) matchesEntry( func (ms mockScope) matchesInfo(dii details.ItemInfo) bool {
cat categorizer,
pathValues map[categorizer]string,
entry details.DetailsEntry,
) bool {
return ms[shouldMatch].Target == "true" return ms[shouldMatch].Target == "true"
} }
@ -107,14 +106,27 @@ func stubScope(match string) mockScope {
sm = match sm = match
} }
filt := passAny
if match == "none" {
filt = failAny
}
return mockScope{ return mockScope{
rootCatStub.String(): passAny, rootCatStub.String(): filt,
leafCatStub.String(): filt,
scopeKeyCategory: filters.Identity(rootCatStub.String()), scopeKeyCategory: filters.Identity(rootCatStub.String()),
scopeKeyDataType: filters.Identity(rootCatStub.String()), scopeKeyDataType: filters.Identity(rootCatStub.String()),
shouldMatch: filters.Identity(sm), shouldMatch: filters.Identity(sm),
} }
} }
func stubInfoScope(match string) mockScope {
sc := stubScope(match)
sc[scopeKeyInfoFilter] = filters.Identity("true")
return sc
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// selectors // selectors
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -299,23 +299,14 @@ func (s OneDriveScope) setDefaults() {
// no-op while no child scope types below user are identified // no-op while no child scope types below user are identified
} }
// matchesEntry returns true if either the path or the info in the oneDriveEntry matches the scope details.
func (s OneDriveScope) matchesEntry(
cat categorizer,
pathValues map[categorizer]string,
entry details.DetailsEntry,
) bool {
// matchesPathValues can be handled generically, thanks to SCIENCE.
return matchesPathValues(s, cat.(oneDriveCategory), pathValues, entry.ShortRef) || s.matchesInfo(entry.OneDrive)
}
// matchesInfo handles the standard behavior when comparing a scope and an oneDriveInfo // matchesInfo handles the standard behavior when comparing a scope and an oneDriveInfo
// returns true if the scope and info match for the provided category. // returns true if the scope and info match for the provided category.
func (s OneDriveScope) matchesInfo(info *details.OneDriveInfo) bool { func (s OneDriveScope) matchesInfo(dii details.ItemInfo) bool {
// we need values to match against info := dii.OneDrive
if info == nil { if info == nil {
return false return false
} }
// the scope must define targets to match on // the scope must define targets to match on
filterCat := s.FilterCategory() filterCat := s.FilterCategory()
targets := s.Get(filterCat) targets := s.Get(filterCat)

View File

@ -93,19 +93,14 @@ type (
// internal to scopes.go can utilize the scope's category without the service context. // internal to scopes.go can utilize the scope's category without the service context.
categorizer() categorizer categorizer() categorizer
// matchesEntry is used to determine if the scope values match with either the pathValues, // matchesInfo is used to determine if the scope values match a specific DetailsEntry
// or the DetailsEntry for the given category. // ItemInfo filter. Unlike path filtering, the entry comparison requires service-specific
// The path comparison (using cat and pathValues) can be handled generically within // context in order for the scope to extract the correct serviceInfo in the entry.
// scopes.go. However, the entry comparison requires service-specific context in order
// for the scope to extract the correct serviceInfo in the entry.
// //
// Params: // Params:
// cat - the category type expressed in the Path. Not the category of the Scope. If the // info - the details entry itemInfo containing extended service info that a filter may
// scope does not align with this parameter, the result is automatically false. // compare. Identification of the correct entry Info service is left up to the fulfiller.
// pathValues - the result of categorizer.pathValues() for the Path being checked. matchesInfo(info details.ItemInfo) bool
// entry - the details entry containing extended service info for the item that a filter may
// compare. Identification of the correct entry Info service is left up to the scope.
matchesEntry(cat categorizer, pathValues map[categorizer]string, entry details.DetailsEntry) bool
// setDefaults populates default values for certain scope categories. // setDefaults populates default values for certain scope categories.
// Primarily to ensure that root- or mid-tier scopes (such as folders) // Primarily to ensure that root- or mid-tier scopes (such as folders)
@ -220,9 +215,9 @@ func reduce[T scopeT, C categoryT](
} }
// aggregate each scope type by category for easier isolation in future processing. // aggregate each scope type by category for easier isolation in future processing.
excls := scopesByCategory[T](s.Excludes, dataCategories) excls := scopesByCategory[T](s.Excludes, dataCategories, false)
filts := scopesByCategory[T](s.Filters, dataCategories) filts := scopesByCategory[T](s.Filters, dataCategories, true)
incls := scopesByCategory[T](s.Includes, dataCategories) incls := scopesByCategory[T](s.Includes, dataCategories, false)
ents := []details.DetailsEntry{} ents := []details.DetailsEntry{}
@ -262,9 +257,12 @@ func reduce[T scopeT, C categoryT](
// ex: a slice containing the scopes [mail1, mail2, event1] // ex: a slice containing the scopes [mail1, mail2, event1]
// would produce a map like { mail: [1, 2], event: [1] } // would produce a map like { mail: [1, 2], event: [1] }
// so long as "mail" and "event" are contained in cats. // so long as "mail" and "event" are contained in cats.
// For ALL-mach requirements, scopes used as filters should force inclusion using
// includeAll=true, independent of the category.
func scopesByCategory[T scopeT, C categoryT]( func scopesByCategory[T scopeT, C categoryT](
scopes []scope, scopes []scope,
cats map[path.CategoryType]C, cats map[path.CategoryType]C,
includeAll bool,
) map[C][]T { ) map[C][]T {
m := map[C][]T{} m := map[C][]T{}
for _, cat := range cats { for _, cat := range cats {
@ -274,7 +272,8 @@ func scopesByCategory[T scopeT, C categoryT](
for _, sc := range scopes { for _, sc := range scopes {
for _, cat := range cats { for _, cat := range cats {
t := T(sc) t := T(sc)
if typeAndCategoryMatches(cat, t.categorizer()) { // include a scope if the data category matches, or the caller forces inclusion.
if includeAll || typeAndCategoryMatches(cat, t.categorizer()) {
m[cat] = append(m[cat], t) m[cat] = append(m[cat], t)
} }
} }
@ -285,8 +284,8 @@ func scopesByCategory[T scopeT, C categoryT](
// passes compares each path to the included and excluded exchange scopes. Returns true // passes compares each path to the included and excluded exchange scopes. Returns true
// if the path is included, passes filters, and not excluded. // if the path is included, passes filters, and not excluded.
func passes[T scopeT]( func passes[T scopeT, C categoryT](
cat categorizer, cat C,
pathValues map[categorizer]string, pathValues map[categorizer]string,
entry details.DetailsEntry, entry details.DetailsEntry,
excs, filts, incs []T, excs, filts, incs []T,
@ -303,7 +302,7 @@ func passes[T scopeT](
var included bool var included bool
for _, inc := range incs { for _, inc := range incs {
if inc.matchesEntry(cat, pathValues, entry) { if matchesEntry(inc, cat, pathValues, entry) {
included = true included = true
break break
} }
@ -316,14 +315,14 @@ func passes[T scopeT](
// all filters must pass // all filters must pass
for _, filt := range filts { for _, filt := range filts {
if !filt.matchesEntry(cat, pathValues, entry) { if !matchesEntry(filt, cat, pathValues, entry) {
return false return false
} }
} }
// any matching exclusion means failure // any matching exclusion means failure
for _, exc := range excs { for _, exc := range excs {
if exc.matchesEntry(cat, pathValues, entry) { if matchesEntry(exc, cat, pathValues, entry) {
return false return false
} }
} }
@ -331,6 +330,22 @@ func passes[T scopeT](
return true return true
} }
// matchesEntry determines whether the category and scope require a path
// comparison or an entry info comparison.
func matchesEntry[T scopeT, C categoryT](
sc T,
cat C,
pathValues map[categorizer]string,
entry details.DetailsEntry,
) bool {
// filterCategory requires matching against service-specific info values
if len(getFilterCategory(sc)) > 0 {
return sc.matchesInfo(entry.ItemInfo)
}
return matchesPathValues(sc, cat, pathValues, entry.ShortRef)
}
// matchesPathValues will check whether the pathValues have matching entries // matchesPathValues will check whether the pathValues have matching entries
// in the scope. The keys of the values to match against are identified by // in the scope. The keys of the values to match against are identified by
// the categorizer. // the categorizer.
@ -342,27 +357,25 @@ func matchesPathValues[T scopeT, C categoryT](
pathValues map[categorizer]string, pathValues map[categorizer]string,
shortRef string, shortRef string,
) bool { ) bool {
// if scope specifies a filter category,
// path checking is automatically skipped.
if len(getFilterCategory(sc)) > 0 {
return false
}
for _, c := range cat.pathKeys() { for _, c := range cat.pathKeys() {
scopeVals := getCatValue(sc, c) scopeVals := getCatValue(sc, c)
// the scope must define the targets to match on // the scope must define the targets to match on
if len(scopeVals) == 0 { if len(scopeVals) == 0 {
return false return false
} }
// None() fails all matches // None() fails all matches
if scopeVals[0] == NoneTgt { if scopeVals[0] == NoneTgt {
return false return false
} }
// the path must contain a value to match against
// the pathValues must have an entry for the given categorizer
pathVal, ok := pathValues[c] pathVal, ok := pathValues[c]
if !ok { if !ok {
return false return false
} }
// all parts of the scope must match // all parts of the scope must match
cc := c.(C) cc := c.(C)
if !isAnyTarget(sc, cc) { if !isAnyTarget(sc, cc) {

View File

@ -103,19 +103,29 @@ func (suite *SelectorScopesSuite) TestContains() {
func (suite *SelectorScopesSuite) TestGetCatValue() { func (suite *SelectorScopesSuite) TestGetCatValue() {
t := suite.T() t := suite.T()
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = filterize(rootCatStub.String()) stub[rootCatStub.String()] = filterize(rootCatStub.String())
assert.Equal(t, assert.Equal(t,
[]string{rootCatStub.String()}, []string{rootCatStub.String()},
getCatValue(stub, rootCatStub)) getCatValue(stub, rootCatStub))
assert.Equal(t, None(), getCatValue(stub, leafCatStub)) assert.Equal(t,
None(),
getCatValue(stub, mockCategorizer("foo")))
} }
func (suite *SelectorScopesSuite) TestIsAnyTarget() { func (suite *SelectorScopesSuite) TestIsAnyTarget() {
t := suite.T() t := suite.T()
stub := stubScope("") stub := stubScope("")
assert.True(t, isAnyTarget(stub, rootCatStub)) assert.True(t, isAnyTarget(stub, rootCatStub))
assert.True(t, isAnyTarget(stub, leafCatStub))
assert.False(t, isAnyTarget(stub, mockCategorizer("smarf")))
stub = stubScope("none")
assert.False(t, isAnyTarget(stub, rootCatStub))
assert.False(t, isAnyTarget(stub, leafCatStub)) assert.False(t, isAnyTarget(stub, leafCatStub))
assert.False(t, isAnyTarget(stub, mockCategorizer("smarf")))
} }
var reduceTestTable = []struct { var reduceTestTable = []struct {
@ -161,7 +171,7 @@ var reduceTestTable = []struct {
name: "include all filter none", name: "include all filter none",
sel: func() mockSel { sel: func() mockSel {
sel := stubSelector() sel := stubSelector()
sel.Filters[0] = scope(stubScope("none")) sel.Filters[0] = scope(stubInfoScope("none"))
sel.Excludes = nil sel.Excludes = nil
return sel return sel
}, },
@ -257,7 +267,8 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() {
[]scope{scope(s1), scope(s2)}, []scope{scope(s1), scope(s2)},
map[path.CategoryType]mockCategorizer{ map[path.CategoryType]mockCategorizer{
path.UnknownCategory: rootCatStub, path.UnknownCategory: rootCatStub,
}) },
false)
assert.Len(t, result, 1) assert.Len(t, result, 1)
assert.Len(t, result[rootCatStub], 1) assert.Len(t, result[rootCatStub], 1)
assert.Empty(t, result[leafCatStub]) assert.Empty(t, result[leafCatStub])
@ -265,7 +276,8 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() {
func (suite *SelectorScopesSuite) TestPasses() { func (suite *SelectorScopesSuite) TestPasses() {
cat := rootCatStub cat := rootCatStub
pathVals := map[categorizer]string{} pth := stubPath(suite.T(), "uid", []string{"fld"}, path.EventsCategory)
pathVals := cat.pathValues(pth)
entry := details.DetailsEntry{} entry := details.DetailsEntry{}
for _, test := range reduceTestTable { for _, test := range reduceTestTable {