use slices for all filter targets (#3028)

Simple concept for a large footprint of change:
filters should always accept a slice of targets
to compare against, instead of a string.  Currently,
we're faking a slice by way of using a const
separator anyway, so we might as well support
slices outright.

Lots of changes (simplification, mostly) in
selectors cascade from this shift.  Any updates
you see here are purely to comply with the updated
filters interface.

This step will make concealing PII in selector and filter
logging significantly easier than before.

---

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

- [x]  No

#### Type of change

- [x] 🤖 Supportability/Tests

#### Issue(s)

* #2024

#### Test Plan

- [x]  Unit test
This commit is contained in:
Keepers 2023-04-17 16:07:17 -06:00 committed by GitHub
parent 86f9d60bf7
commit 0458d04b9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 831 additions and 515 deletions

View File

@ -12,11 +12,13 @@ func _() {
_ = x[BackupOpSchema-1] _ = x[BackupOpSchema-1]
_ = x[RestoreOpSchema-2] _ = x[RestoreOpSchema-2]
_ = x[BackupSchema-3] _ = x[BackupSchema-3]
_ = x[BackupDetailsSchema-4]
_ = x[RepositorySchema-5]
} }
const _Schema_name = "UnknownSchemaBackupOpSchemaRestoreOpSchemaBackupSchema" const _Schema_name = "UnknownSchemaBackupOpSchemaRestoreOpSchemaBackupSchemaBackupDetailsSchemaRepositorySchema"
var _Schema_index = [...]uint8{0, 13, 27, 42, 54} var _Schema_index = [...]uint8{0, 13, 27, 42, 54, 73, 89}
func (i Schema) String() string { func (i Schema) String() string {
if i < 0 || i >= Schema(len(_Schema_index)-1) { if i < 0 || i >= Schema(len(_Schema_index)-1) {

View File

@ -0,0 +1,37 @@
// Code generated by "stringer -type=comparator -linecomment"; DO NOT EDIT.
package filters
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[UnknownComparator-0]
_ = x[EqualTo-1]
_ = x[GreaterThan-2]
_ = x[LessThan-3]
_ = x[TargetContains-4]
_ = x[TargetIn-5]
_ = x[Passes-6]
_ = x[Fails-7]
_ = x[IdentityValue-8]
_ = x[TargetPrefixes-9]
_ = x[TargetSuffixes-10]
_ = x[TargetPathPrefix-11]
_ = x[TargetPathContains-12]
_ = x[TargetPathSuffix-13]
_ = x[TargetPathEquals-14]
}
const _comparator_name = "UnknownComparisonEQGTLTContINPassFailIdentityPfxSfxPathPfxPathContPathSfxPathEQ"
var _comparator_index = [...]uint8{0, 17, 19, 21, 23, 27, 29, 33, 37, 45, 48, 51, 58, 66, 73, 79}
func (i comparator) String() string {
if i < 0 || i >= comparator(len(_comparator_index)-1) {
return "comparator(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _comparator_name[_comparator_index[i]:_comparator_index[i+1]]
}

View File

@ -1,45 +1,60 @@
package filters package filters
import ( import (
"fmt"
"strings" "strings"
"github.com/alcionai/clues"
"golang.org/x/exp/slices"
"github.com/alcionai/corso/src/internal/common/pii"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
type comparator int type comparator int
//go:generate stringer -type=comparator -linecomment
const ( const (
UnknownComparator comparator = iota UnknownComparator comparator = iota // UnknownComparison
// a == b // a == b
EqualTo EqualTo // EQ
// a > b // a > b
GreaterThan GreaterThan // GT
// a < b // a < b
LessThan LessThan // LT
// "foo,bar,baz" contains "foo" // "foo,bar,baz" contains "foo"
TargetContains TargetContains // Cont
// "foo" is found in "foo,bar,baz" // "foo" is found in "foo,bar,baz"
TargetIn TargetIn // IN
// always passes // always passes
Passes Passes // Pass
// always fails // always fails
Fails Fails // Fail
// passthrough for the target // passthrough for the target
IdentityValue IdentityValue // Identity
// "foo" is a prefix of "foobarbaz" // "foo" is a prefix of "foobarbaz"
TargetPrefixes TargetPrefixes // Pfx
// "baz" is a suffix of "foobarbaz" // "baz" is a suffix of "foobarbaz"
TargetSuffixes TargetSuffixes // Sfx
// "foo" equals any complete element prefix of "foo/bar/baz" // "foo" equals any complete element prefix of "foo/bar/baz"
TargetPathPrefix TargetPathPrefix // PathPfx
// "foo" equals any complete element in "foo/bar/baz" // "foo" equals any complete element in "foo/bar/baz"
TargetPathContains TargetPathContains // PathCont
// "baz" equals any complete element suffix of "foo/bar/baz" // "baz" equals any complete element suffix of "foo/bar/baz"
TargetPathSuffix TargetPathSuffix // PathSfx
// "foo/bar/baz" equals the complete path "foo/bar/baz" // "foo/bar/baz" equals the complete path "foo/bar/baz"
TargetPathEquals TargetPathEquals // PathEQ
) )
func normAll(ss []string) []string {
r := slices.Clone(ss)
for i := range r {
r[i] = norm(r[i])
}
return r
}
func norm(s string) string { func norm(s string) string {
return strings.ToLower(s) return strings.ToLower(s)
} }
@ -69,213 +84,198 @@ func normPathElem(s string) string {
// true if Filter.Comparer(filter.target, v) is true. // true if Filter.Comparer(filter.target, v) is true.
type Filter struct { type Filter struct {
Comparator comparator `json:"comparator"` Comparator comparator `json:"comparator"`
Target string `json:"target"` // the value to compare against
Targets []string `json:"targets"` // the set of values to compare Targets []string `json:"targets"` // the set of values to compare
NormalizedTargets []string `json:"normalizedTargets"` // the set of comparable values post normalization NormalizedTargets []string `json:"normalizedTargets"` // the set of comparable values post normalization
Negate bool `json:"negate"` // when true, negate the comparator result Negate bool `json:"negate"` // when true, negate the comparator result
// only used when the filter's purpose is to hold a value without intent for comparison
Identity string `json:"identity"`
// deprecated, kept around for deserialization
Target string `json:"target"` // the value to compare against
} }
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// Constructors // Constructors
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// Equal creates a filter where Compare(v) is true if // Identity creates a filter intended to hold values, rather than
// compare them. Comparatively, it'll behave the same as Equals.
func Identity(id string) Filter {
return Filter{
Comparator: IdentityValue,
Identity: id,
Targets: []string{id},
NormalizedTargets: normAll([]string{id}),
}
}
// Equal creates a filter where Compare(v) is true if, for any target string,
// target == v // target == v
func Equal(target string) Filter { func Equal(target []string) Filter {
return newFilter(EqualTo, target, false) return newFilter(EqualTo, target, normAll(target), false)
} }
// NotEqual creates a filter where Compare(v) is true if // NotEqual creates a filter where Compare(v) is true if, for any target string,
// target != v // target != v
func NotEqual(target string) Filter { func NotEqual(target []string) Filter {
return newFilter(EqualTo, target, true) return newFilter(EqualTo, target, normAll(target), true)
} }
// Greater creates a filter where Compare(v) is true if // Greater creates a filter where Compare(v) is true if, for any target string,
// target > v // target > v
func Greater(target string) Filter { func Greater(target []string) Filter {
return newFilter(GreaterThan, target, false) return newFilter(GreaterThan, target, normAll(target), false)
} }
// NotGreater creates a filter where Compare(v) is true if // NotGreater creates a filter where Compare(v) is true if, for any target string,
// target <= v // target <= v
func NotGreater(target string) Filter { func NotGreater(target []string) Filter {
return newFilter(GreaterThan, target, true) return newFilter(GreaterThan, target, normAll(target), true)
} }
// Less creates a filter where Compare(v) is true if // Less creates a filter where Compare(v) is true if, for any target string,
// target < v // target < v
func Less(target string) Filter { func Less(target []string) Filter {
return newFilter(LessThan, target, false) return newFilter(LessThan, target, normAll(target), false)
} }
// NotLess creates a filter where Compare(v) is true if // NotLess creates a filter where Compare(v) is true if, for any target string,
// target >= v // target >= v
func NotLess(target string) Filter { func NotLess(target []string) Filter {
return newFilter(LessThan, target, true) return newFilter(LessThan, target, normAll(target), true)
} }
// Contains creates a filter where Compare(v) is true if // Contains creates a filter where Compare(v) is true if, for any target string,
// target.Contains(v) // target.Contains(v)
func Contains(target string) Filter { func Contains(target []string) Filter {
return newFilter(TargetContains, target, false) return newFilter(TargetContains, target, normAll(target), false)
} }
// NotContains creates a filter where Compare(v) is true if // NotContains creates a filter where Compare(v) is true if, for any target string,
// !target.Contains(v) // !target.Contains(v)
func NotContains(target string) Filter { func NotContains(target []string) Filter {
return newFilter(TargetContains, target, true) return newFilter(TargetContains, target, normAll(target), true)
} }
// In creates a filter where Compare(v) is true if // In creates a filter where Compare(v) is true if, for any target string,
// v.Contains(target) // v.Contains(target)
func In(targets []string) Filter { func In(target []string) Filter {
return newSliceFilter(TargetIn, targets, targets, false) return newFilter(TargetIn, target, normAll(target), false)
} }
// NotIn creates a filter where Compare(v) is true if // NotIn creates a filter where Compare(v) is true if, for any target string,
// !v.Contains(target) // !v.Contains(target)
func NotIn(targets []string) Filter { func NotIn(target []string) Filter {
return newSliceFilter(TargetIn, targets, targets, true) return newFilter(TargetIn, target, normAll(target), true)
} }
// Pass creates a filter where Compare(v) always returns true // Pass creates a filter where Compare(v) always returns true
func Pass() Filter { func Pass() Filter {
return newFilter(Passes, "*", false) return newFilter(Passes, []string{"*"}, nil, false)
} }
// Fail creates a filter where Compare(v) always returns false // Fail creates a filter where Compare(v) always returns false
func Fail() Filter { func Fail() Filter {
return newFilter(Fails, "", false) return newFilter(Fails, []string{""}, nil, false)
} }
// Identity creates a filter intended to hold values, rather than // Prefix creates a filter where Compare(v) is true if, for any target string,
// compare them. Comparatively, it'll behave the same as Equals.
func Identity(id string) Filter {
return newFilter(IdentityValue, id, false)
}
// Prefix creates a filter where Compare(v) is true if
// target.Prefix(v) // target.Prefix(v)
func Prefix(target string) Filter { func Prefix(target []string) Filter {
return newFilter(TargetPrefixes, target, false) return newFilter(TargetPrefixes, target, normAll(target), false)
} }
// NotPrefix creates a filter where Compare(v) is true if // NotPrefix creates a filter where Compare(v) is true if, for any target string,
// !target.Prefix(v) // !target.Prefix(v)
func NotPrefix(target string) Filter { func NotPrefix(target []string) Filter {
return newFilter(TargetPrefixes, target, true) return newFilter(TargetPrefixes, target, normAll(target), true)
} }
// Suffix creates a filter where Compare(v) is true if // Suffix creates a filter where Compare(v) is true if, for any target string,
// target.Suffix(v) // target.Suffix(v)
func Suffix(target string) Filter { func Suffix(target []string) Filter {
return newFilter(TargetSuffixes, target, false) return newFilter(TargetSuffixes, target, normAll(target), false)
} }
// NotSuffix creates a filter where Compare(v) is true if // NotSuffix creates a filter where Compare(v) is true if, for any target string,
// !target.Suffix(v) // !target.Suffix(v)
func NotSuffix(target string) Filter { func NotSuffix(target []string) Filter {
return newFilter(TargetSuffixes, target, true) return newFilter(TargetSuffixes, target, normAll(target), true)
} }
// PathPrefix creates a filter where Compare(v) is true if // PathPrefix creates a filter where Compare(v) is true if, for any target string,
// target.Prefix(v) && // target.Prefix(v) &&
// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 // split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1
// ex: target "/foo/bar" returns true for input "/foo/bar/baz", // ex: target "/foo/bar" returns true for input "/foo/bar/baz",
// but false for "/foo/barbaz" // but false for "/foo/barbaz"
//
// 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 PathPrefix(targets []string) Filter { func PathPrefix(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathPrefix, targets, tgts, false) return newFilter(TargetPathPrefix, targets, tgts, false)
} }
// NotPathPrefix creates a filter where Compare(v) is true if // NotPathPrefix creates a filter where Compare(v) is true if, for any target string,
// !target.Prefix(v) || // !target.Prefix(v) ||
// !split(target)[i].Equals(split(v)[i]) for _any_ i in 0..len(target)-1 // !split(target)[i].Equals(split(v)[i]) for _any_ i in 0..len(target)-1
// ex: target "/foo/bar" returns false for input "/foo/bar/baz", // ex: target "/foo/bar" returns false for input "/foo/bar/baz",
// but true for "/foo/barbaz" // but true for "/foo/barbaz"
//
// 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 NotPathPrefix(targets []string) Filter { func NotPathPrefix(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathPrefix, targets, tgts, true) return newFilter(TargetPathPrefix, targets, tgts, true)
} }
// PathContains creates a filter where Compare(v) is true if // PathContains creates a filter where Compare(v) is true if, for any target string,
// for _any_ elem e in split(v), target.Equals(e) || // for _any_ elem e in split(v), target.Equals(e) ||
// for _any_ sequence of elems in split(v), target.Equals(path.Join(e[n:m])) // for _any_ sequence of elems in split(v), target.Equals(path.Join(e[n:m]))
// ex: target "foo" returns true for input "/baz/foo/bar", // ex: target "foo" returns true for input "/baz/foo/bar",
// but false for "/baz/foobar" // but false for "/baz/foobar"
// ex: target "baz/foo" returns true for input "/baz/foo/bar", // ex: target "baz/foo" returns true for input "/baz/foo/bar",
// but false for "/baz/foobar" // but false for "/baz/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 PathContains(targets []string) Filter { func PathContains(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathContains, targets, tgts, false) return newFilter(TargetPathContains, targets, tgts, false)
} }
// NotPathContains creates a filter where Compare(v) is true if // NotPathContains creates a filter where Compare(v) is true if, for any target string,
// for _every_ elem e in split(v), !target.Equals(e) || // for _every_ elem e in split(v), !target.Equals(e) ||
// for _every_ sequence of elems in split(v), !target.Equals(path.Join(e[n:m])) // for _every_ sequence of elems in split(v), !target.Equals(path.Join(e[n:m]))
// ex: target "foo" returns false for input "/baz/foo/bar", // ex: target "foo" returns false for input "/baz/foo/bar",
// but true for "/baz/foobar" // but true for "/baz/foobar"
// ex: target "baz/foo" returns false for input "/baz/foo/bar", // ex: target "baz/foo" returns false for input "/baz/foo/bar",
// but true for "/baz/foobar" // but true for "/baz/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 NotPathContains(targets []string) Filter { func NotPathContains(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathContains, targets, tgts, true) return newFilter(TargetPathContains, targets, tgts, true)
} }
// PathSuffix creates a filter where Compare(v) is true if // PathSuffix creates a filter where Compare(v) is true if, for any target string,
// target.Suffix(v) && // target.Suffix(v) &&
// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 // 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", // ex: target "/bar/baz" returns true for input "/foo/bar/baz",
// but false for "/foobar/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 { func PathSuffix(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathSuffix, targets, tgts, false) return newFilter(TargetPathSuffix, targets, tgts, false)
} }
// NotPathSuffix creates a filter where Compare(v) is true if // NotPathSuffix creates a filter where Compare(v) is true if
@ -283,69 +283,45 @@ func PathSuffix(targets []string) Filter {
// !split(target)[i].Equals(split(v)[i]) for _any_ i in 0..len(target)-1 // !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", // ex: target "/bar/baz" returns false for input "/foo/bar/baz",
// but true for "/foobar/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 { func NotPathSuffix(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathSuffix, targets, tgts, true) return newFilter(TargetPathSuffix, targets, tgts, true)
} }
// PathEquals creates a filter where Compare(v) is true if // PathEquals creates a filter where Compare(v) is true if, for any target string,
// target.Equals(v) && // target.Equals(v) &&
// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 // 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/" // ex: target "foo" returns true for inputs "/foo/", "/foo", and "foo/"
// but false for "/foo/bar", "bar/foo/", and "/foobar/" // 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 { func PathEquals(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathEquals, targets, tgts, false) return newFilter(TargetPathEquals, targets, tgts, false)
} }
// NotPathEquals creates a filter where Compare(v) is true if // NotPathEquals creates a filter where Compare(v) is true if, for any target string,
// !target.Equals(v) || // !target.Equals(v) ||
// !split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 // !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/" // ex: target "foo" returns true "/foo/bar", "bar/foo/", and "/foobar/"
// but false for for inputs "/foo/", "/foo", and "foo/" // 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 { func NotPathEquals(targets []string) Filter {
tgts := make([]string, len(targets)) tgts := make([]string, len(targets))
for i := range targets { for i := range targets {
tgts[i] = normPathElem(targets[i]) tgts[i] = normPathElem(targets[i])
} }
return newSliceFilter(TargetPathEquals, targets, tgts, true) return newFilter(TargetPathEquals, targets, tgts, true)
} }
// newFilter is the standard filter constructor. // newFilter constructs filters that contain multiple targets
func newFilter(c comparator, target string, negate bool) Filter { func newFilter(c comparator, targets, normTargets []string, negate bool) Filter {
return Filter{
Comparator: c,
Target: target,
Negate: negate,
}
}
// newSliceFilter constructs filters that contain multiple targets
func newSliceFilter(c comparator, targets, normTargets []string, negate bool) Filter {
return Filter{ return Filter{
Comparator: c, Comparator: c,
Targets: targets, Targets: targets,
@ -378,10 +354,7 @@ func (f Filter) CompareAny(inputs ...string) bool {
// Compare checks whether the input passes the filter. // Compare checks whether the input passes the filter.
func (f Filter) Compare(input string) bool { func (f Filter) Compare(input string) bool {
var ( var cmp func(string, string) bool
cmp func(string, string) bool
hasSlice bool
)
switch f.Comparator { switch f.Comparator {
case EqualTo, IdentityValue: case EqualTo, IdentityValue:
@ -394,23 +367,18 @@ func (f Filter) Compare(input string) bool {
cmp = contains cmp = contains
case TargetIn: case TargetIn:
cmp = in cmp = in
hasSlice = true
case TargetPrefixes: case TargetPrefixes:
cmp = prefixed cmp = prefixed
case TargetSuffixes: case TargetSuffixes:
cmp = suffixed cmp = suffixed
case TargetPathPrefix: case TargetPathPrefix:
cmp = pathPrefix cmp = pathPrefix
hasSlice = true
case TargetPathContains: case TargetPathContains:
cmp = pathContains cmp = pathContains
hasSlice = true
case TargetPathSuffix: case TargetPathSuffix:
cmp = pathSuffix cmp = pathSuffix
hasSlice = true
case TargetPathEquals: case TargetPathEquals:
cmp = pathEquals cmp = pathEquals
hasSlice = true
case Passes: case Passes:
return true return true
case Fails: case Fails:
@ -419,11 +387,11 @@ func (f Filter) Compare(input string) bool {
var ( var (
res bool res bool
targets = []string{f.Target} targets = f.NormalizedTargets
) )
if hasSlice { if len(targets) == 0 {
targets = f.NormalizedTargets targets = f.Targets
} }
for _, tgt := range targets { for _, tgt := range targets {
@ -535,35 +503,41 @@ func pathEquals(target, input string) bool {
} }
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// Helpers // Printers and PII control
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// prefixString maps the comparators to string prefixes for printing. var _ clues.PlainConcealer = &Filter{}
var prefixString = map[comparator]string{
EqualTo: "eq:", var safeFilterValues = map[string]struct{}{"*": {}}
GreaterThan: "gt:",
LessThan: "lt:", func (f Filter) Conceal() string {
TargetContains: "cont:", fcs := f.Comparator.String()
TargetIn: "in:",
TargetPrefixes: "pfx:", switch f.Comparator {
TargetSuffixes: "sfx:", case Passes, Fails:
TargetPathPrefix: "pathPfx:", return fcs
TargetPathContains: "pathCont:", }
TargetPathSuffix: "pathSfx:",
TargetPathEquals: "pathEq:", concealed := pii.ConcealElements(f.Targets, safeFilterValues)
return fcs + ":" + strings.Join(concealed, ",")
}
func (f Filter) Format(fs fmt.State, _ rune) {
fmt.Fprint(fs, f.Conceal())
} }
func (f Filter) String() string { func (f Filter) String() string {
switch f.Comparator { return f.Conceal()
case Passes: }
return "pass"
case Fails: func (f Filter) PlainString() string {
return "fail" fcs := f.Comparator.String()
}
switch f.Comparator {
if len(f.Targets) > 0 { case Passes, Fails:
return prefixString[f.Comparator] + strings.Join(f.Targets, ",") return fcs
} }
return prefixString[f.Comparator] + f.Target return fcs + ":" + strings.Join(f.Targets, ",")
} }

View File

@ -1,8 +1,11 @@
package filters_test package filters_test
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -18,9 +21,29 @@ func TestFiltersSuite(t *testing.T) {
suite.Run(t, &FiltersSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &FiltersSuite{Suite: tester.NewUnitSuite(t)})
} }
// set the clues hashing to mask for the span of this suite
func (suite *FiltersSuite) SetupSuite() {
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
}
// revert clues hashing to plaintext for all other tests
func (suite *FiltersSuite) TeardownSuite() {
clues.SetHasher(clues.NoHash())
}
func sl(s ...string) []string {
return append([]string{}, s...)
}
var (
foo = sl("foo")
five = sl("5")
smurfs = sl("smurfs")
)
func (suite *FiltersSuite) TestEquals() { func (suite *FiltersSuite) TestEquals() {
f := filters.Equal("foo") f := filters.Equal(foo)
nf := filters.NotEqual("foo") nf := filters.NotEqual(foo)
table := []struct { table := []struct {
input string input string
@ -41,8 +64,8 @@ func (suite *FiltersSuite) TestEquals() {
} }
func (suite *FiltersSuite) TestEquals_any() { func (suite *FiltersSuite) TestEquals_any() {
f := filters.Equal("foo") f := filters.Equal(foo)
nf := filters.NotEqual("foo") nf := filters.NotEqual(foo)
table := []struct { table := []struct {
name string name string
@ -64,8 +87,8 @@ func (suite *FiltersSuite) TestEquals_any() {
} }
func (suite *FiltersSuite) TestGreater() { func (suite *FiltersSuite) TestGreater() {
f := filters.Greater("5") f := filters.Greater(five)
nf := filters.NotGreater("5") nf := filters.NotGreater(five)
table := []struct { table := []struct {
input string input string
@ -87,8 +110,8 @@ func (suite *FiltersSuite) TestGreater() {
} }
func (suite *FiltersSuite) TestLess() { func (suite *FiltersSuite) TestLess() {
f := filters.Less("5") f := filters.Less(five)
nf := filters.NotLess("5") nf := filters.NotLess(five)
table := []struct { table := []struct {
input string input string
@ -110,8 +133,8 @@ func (suite *FiltersSuite) TestLess() {
} }
func (suite *FiltersSuite) TestContains() { func (suite *FiltersSuite) TestContains() {
f := filters.Contains("smurfs") f := filters.Contains(smurfs)
nf := filters.NotContains("smurfs") nf := filters.NotContains(smurfs)
table := []struct { table := []struct {
input string input string
@ -131,32 +154,9 @@ func (suite *FiltersSuite) TestContains() {
} }
} }
func (suite *FiltersSuite) TestContains_Joined() {
f := filters.Contains("smarf,userid")
nf := filters.NotContains("smarf,userid")
table := []struct {
input string
expectF assert.BoolAssertionFunc
expectNF assert.BoolAssertionFunc
}{
{"userid", assert.True, assert.False},
{"f,userid", assert.True, assert.False},
{"fnords", assert.False, assert.True},
}
for _, test := range table {
suite.Run(test.input, func() {
t := suite.T()
test.expectF(t, f.Compare(test.input), "filter")
test.expectNF(t, nf.Compare(test.input), "negated filter")
})
}
}
func (suite *FiltersSuite) TestIn() { func (suite *FiltersSuite) TestIn() {
f := filters.In([]string{"murf"}) f := filters.In(sl("murf"))
nf := filters.NotIn([]string{"murf"}) nf := filters.NotIn(sl("murf"))
table := []struct { table := []struct {
input string input string
@ -177,8 +177,8 @@ func (suite *FiltersSuite) TestIn() {
} }
func (suite *FiltersSuite) TestIn_MultipleTargets() { func (suite *FiltersSuite) TestIn_MultipleTargets() {
f := filters.In([]string{"murf", "foo"}) f := filters.In(sl("murf", "foo"))
nf := filters.NotIn([]string{"murf", "foo"}) nf := filters.NotIn(sl("murf", "foo"))
table := []struct { table := []struct {
input string input string
@ -201,8 +201,8 @@ func (suite *FiltersSuite) TestIn_MultipleTargets() {
} }
func (suite *FiltersSuite) TestIn_MultipleTargets_Joined() { func (suite *FiltersSuite) TestIn_MultipleTargets_Joined() {
f := filters.In([]string{"userid", "foo"}) f := filters.In(sl("userid", "foo"))
nf := filters.NotIn([]string{"userid", "foo"}) nf := filters.NotIn(sl("userid", "foo"))
table := []struct { table := []struct {
input string input string
@ -225,8 +225,8 @@ func (suite *FiltersSuite) TestIn_MultipleTargets_Joined() {
} }
func (suite *FiltersSuite) TestIn_Joined() { func (suite *FiltersSuite) TestIn_Joined() {
f := filters.In([]string{"userid"}) f := filters.In(sl("userid"))
nf := filters.NotIn([]string{"userid"}) nf := filters.NotIn(sl("userid"))
table := []struct { table := []struct {
input string input string
@ -247,7 +247,7 @@ func (suite *FiltersSuite) TestIn_Joined() {
} }
func (suite *FiltersSuite) TestPrefixes() { func (suite *FiltersSuite) TestPrefixes() {
target := "folderA" target := sl("folderA")
f := filters.Prefix(target) f := filters.Prefix(target)
nf := filters.NotPrefix(target) nf := filters.NotPrefix(target)
@ -274,7 +274,7 @@ func (suite *FiltersSuite) TestPrefixes() {
} }
func (suite *FiltersSuite) TestSuffixes() { func (suite *FiltersSuite) TestSuffixes() {
target := "folderB" target := sl("folderB")
f := filters.Suffix(target) f := filters.Suffix(target)
nf := filters.NotSuffix(target) nf := filters.NotSuffix(target)
@ -613,3 +613,95 @@ func (suite *FiltersSuite) TestPathEquals_NormalizedTargets() {
}) })
} }
} }
func (suite *FiltersSuite) TestFilter_pii() {
targets := []string{"fnords", "smarf", "*"}
table := []struct {
name string
f filters.Filter
}{
{"equal", filters.Equal(targets)},
{"contains", filters.Contains(targets)},
{"greater", filters.Greater(targets)},
{"less", filters.Less(targets)},
{"prefix", filters.Prefix(targets)},
{"suffix", filters.Suffix(targets)},
{"pathprefix", filters.PathPrefix(targets)},
{"pathsuffix", filters.PathSuffix(targets)},
{"pathcontains", filters.PathContains(targets)},
{"pathequals", filters.PathEquals(targets)},
}
for _, test := range table {
suite.Run(test.name, func() {
var (
t = suite.T()
expect = test.f.Comparator.String() + ":***,***,*"
expectPlain = test.f.Comparator.String() + ":" + strings.Join(targets, ",")
)
result := test.f.Conceal()
assert.Equal(t, expect, result, "conceal")
result = test.f.String()
assert.Equal(t, expect, result, "string")
result = test.f.PlainString()
assert.Equal(t, expectPlain, result, "plainString")
result = fmt.Sprintf("%s", test.f)
assert.Equal(t, expect, result, "fmt %%s")
result = fmt.Sprintf("%v", test.f)
assert.Equal(t, expect, result, "fmt %%v")
result = fmt.Sprintf("%+v", test.f)
assert.Equal(t, expect, result, "fmt %%+v")
})
}
table2 := []struct {
name string
f filters.Filter
expect string
expectPlain string
}{
{"pass", filters.Pass(), "Pass", "Pass"},
{"fail", filters.Fail(), "Fail", "Fail"},
{
"identity",
filters.Identity("id"),
filters.IdentityValue.String() + ":***",
filters.IdentityValue.String() + ":id",
},
{
"identity",
filters.Identity("*"),
filters.IdentityValue.String() + ":*",
filters.IdentityValue.String() + ":*",
},
}
for _, test := range table2 {
suite.Run(test.name, func() {
t := suite.T()
result := test.f.Conceal()
assert.Equal(t, test.expect, result, "conceal")
result = test.f.String()
assert.Equal(t, test.expect, result, "string")
result = test.f.PlainString()
assert.Equal(t, test.expectPlain, result, "plainString")
result = fmt.Sprintf("%s", test.f)
assert.Equal(t, test.expect, result, "fmt %%s")
result = fmt.Sprintf("%v", test.f)
assert.Equal(t, test.expect, result, "fmt %%v")
result = fmt.Sprintf("%+v", test.f)
assert.Equal(t, test.expect, result, "fmt %%+v")
})
}
}

View File

@ -2,6 +2,7 @@ package selectors
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -121,6 +122,15 @@ func (s exchange) PathCategories() selectorPathCategories {
} }
} }
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
func (s ExchangeScope) Conceal() string { return conceal(s) }
func (s ExchangeScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
func (s ExchangeScope) String() string { return conceal(s) }
func (s ExchangeScope) PlainString() string { return plainString(s) }
// ------------------- // -------------------
// Exclude/Includes // Exclude/Includes
@ -336,7 +346,7 @@ func (sr *ExchangeRestore) ContactName(senderID string) []ExchangeScope {
ExchangeContact, ExchangeContact,
ExchangeInfoContactName, ExchangeInfoContactName,
[]string{senderID}, []string{senderID},
wrapSliceFilter(filters.In)), filters.In),
} }
} }
@ -351,7 +361,7 @@ func (sr *ExchangeRestore) EventOrganizer(organizer string) []ExchangeScope {
ExchangeEvent, ExchangeEvent,
ExchangeInfoEventOrganizer, ExchangeInfoEventOrganizer,
[]string{organizer}, []string{organizer},
wrapSliceFilter(filters.In)), filters.In),
} }
} }
@ -366,7 +376,7 @@ func (sr *ExchangeRestore) EventRecurs(recurs string) []ExchangeScope {
ExchangeEvent, ExchangeEvent,
ExchangeInfoEventRecurs, ExchangeInfoEventRecurs,
[]string{recurs}, []string{recurs},
wrapFilter(filters.Equal)), filters.Equal),
} }
} }
@ -380,7 +390,7 @@ func (sr *ExchangeRestore) EventStartsAfter(timeStrings string) []ExchangeScope
ExchangeEvent, ExchangeEvent,
ExchangeInfoEventStartsAfter, ExchangeInfoEventStartsAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), filters.Less),
} }
} }
@ -394,7 +404,7 @@ func (sr *ExchangeRestore) EventStartsBefore(timeStrings string) []ExchangeScope
ExchangeEvent, ExchangeEvent,
ExchangeInfoEventStartsBefore, ExchangeInfoEventStartsBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), filters.Greater),
} }
} }
@ -409,7 +419,7 @@ func (sr *ExchangeRestore) EventSubject(subject string) []ExchangeScope {
ExchangeEvent, ExchangeEvent,
ExchangeInfoEventSubject, ExchangeInfoEventSubject,
[]string{subject}, []string{subject},
wrapSliceFilter(filters.In)), filters.In),
} }
} }
@ -423,7 +433,7 @@ func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope
ExchangeMail, ExchangeMail,
ExchangeInfoMailReceivedAfter, ExchangeInfoMailReceivedAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), filters.Less),
} }
} }
@ -437,7 +447,7 @@ func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScop
ExchangeMail, ExchangeMail,
ExchangeInfoMailReceivedBefore, ExchangeInfoMailReceivedBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), filters.Greater),
} }
} }
@ -452,7 +462,7 @@ func (sr *ExchangeRestore) MailSender(sender string) []ExchangeScope {
ExchangeMail, ExchangeMail,
ExchangeInfoMailSender, ExchangeInfoMailSender,
[]string{sender}, []string{sender},
wrapSliceFilter(filters.In)), filters.In),
} }
} }
@ -467,7 +477,7 @@ func (sr *ExchangeRestore) MailSubject(subject string) []ExchangeScope {
ExchangeMail, ExchangeMail,
ExchangeInfoMailSubject, ExchangeInfoMailSubject,
[]string{subject}, []string{subject},
wrapSliceFilter(filters.In)), filters.In),
} }
} }

View File

@ -78,9 +78,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Contacts() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeContactFolder: folder, ExchangeContactFolder: {folder},
ExchangeContact: join(c1, c2), ExchangeContact: {c1, c2},
}, },
) )
} }
@ -103,9 +103,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Contacts() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeContactFolder: folder, ExchangeContactFolder: {folder},
ExchangeContact: join(c1, c2), ExchangeContact: {c1, c2},
}, },
) )
@ -129,9 +129,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_ContactFolders(
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeContactFolder: join(f1, f2), ExchangeContactFolder: {f1, f2},
ExchangeContact: AnyTgt, ExchangeContact: Any(),
}, },
) )
} }
@ -153,9 +153,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_ContactFolders(
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeContactFolder: join(f1, f2), ExchangeContactFolder: {f1, f2},
ExchangeContact: AnyTgt, ExchangeContact: Any(),
}, },
) )
@ -180,9 +180,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Events() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeEventCalendar: c1, ExchangeEventCalendar: {c1},
ExchangeEvent: join(e1, e2), ExchangeEvent: {e1, e2},
}, },
) )
} }
@ -204,9 +204,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_EventCalendars(
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeEventCalendar: join(c1, c2), ExchangeEventCalendar: {c1, c2},
ExchangeEvent: AnyTgt, ExchangeEvent: Any(),
}, },
) )
} }
@ -229,9 +229,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Events() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeEventCalendar: c1, ExchangeEventCalendar: {c1},
ExchangeEvent: join(e1, e2), ExchangeEvent: {e1, e2},
}, },
) )
} }
@ -253,9 +253,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_EventCalendars(
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeEventCalendar: join(c1, c2), ExchangeEventCalendar: {c1, c2},
ExchangeEvent: AnyTgt, ExchangeEvent: Any(),
}, },
) )
} }
@ -278,9 +278,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_Mails() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeMailFolder: folder, ExchangeMailFolder: {folder},
ExchangeMail: join(m1, m2), ExchangeMail: {m1, m2},
}, },
) )
} }
@ -303,9 +303,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_Mails() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeMailFolder: folder, ExchangeMailFolder: {folder},
ExchangeMail: join(m1, m2), ExchangeMail: {m1, m2},
}, },
) )
@ -329,9 +329,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_MailFolders() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeMailFolder: join(f1, f2), ExchangeMailFolder: {f1, f2},
ExchangeMail: AnyTgt, ExchangeMail: Any(),
}, },
) )
} }
@ -353,9 +353,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_MailFolders() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(scopes[0]), ExchangeScope(scopes[0]),
map[categorizer]string{ map[categorizer][]string{
ExchangeMailFolder: join(f1, f2), ExchangeMailFolder: {f1, f2},
ExchangeMail: AnyTgt, ExchangeMail: Any(),
}, },
) )
@ -380,9 +380,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_AllData() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(sc), ExchangeScope(sc),
map[categorizer]string{ map[categorizer][]string{
ExchangeContact: AnyTgt, ExchangeContact: Any(),
ExchangeContactFolder: AnyTgt, ExchangeContactFolder: Any(),
}, },
) )
} }
@ -391,8 +391,8 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_AllData() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(sc), ExchangeScope(sc),
map[categorizer]string{ map[categorizer][]string{
ExchangeEvent: AnyTgt, ExchangeEvent: Any(),
}, },
) )
} }
@ -401,9 +401,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Exclude_AllData() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(sc), ExchangeScope(sc),
map[categorizer]string{ map[categorizer][]string{
ExchangeMail: AnyTgt, ExchangeMail: Any(),
ExchangeMailFolder: AnyTgt, ExchangeMailFolder: Any(),
}, },
) )
} }
@ -428,9 +428,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_AllData() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(sc), ExchangeScope(sc),
map[categorizer]string{ map[categorizer][]string{
ExchangeContact: AnyTgt, ExchangeContact: Any(),
ExchangeContactFolder: AnyTgt, ExchangeContactFolder: Any(),
}, },
) )
} }
@ -439,8 +439,8 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_AllData() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(sc), ExchangeScope(sc),
map[categorizer]string{ map[categorizer][]string{
ExchangeEvent: AnyTgt, ExchangeEvent: Any(),
}, },
) )
} }
@ -449,9 +449,9 @@ func (suite *ExchangeSelectorSuite) TestExchangeSelector_Include_AllData() {
scopeMustHave( scopeMustHave(
t, t,
ExchangeScope(sc), ExchangeScope(sc),
map[categorizer]string{ map[categorizer][]string{
ExchangeMail: AnyTgt, ExchangeMail: Any(),
ExchangeMailFolder: AnyTgt, ExchangeMailFolder: Any(),
}, },
) )
} }

View File

@ -1,6 +1,7 @@
package selectors package selectors
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
@ -95,8 +96,8 @@ type mockScope scope
var _ scoper = &mockScope{} var _ scoper = &mockScope{}
func (ms mockScope) categorizer() categorizer { func (s mockScope) categorizer() categorizer {
switch ms[scopeKeyCategory].Target { switch s[scopeKeyCategory].Identity {
case rootCatStub.String(): case rootCatStub.String():
return rootCatStub return rootCatStub
case leafCatStub.String(): case leafCatStub.String():
@ -106,11 +107,11 @@ func (ms mockScope) categorizer() categorizer {
return unknownCatStub return unknownCatStub
} }
func (ms mockScope) matchesInfo(dii details.ItemInfo) bool { func (s mockScope) matchesInfo(dii details.ItemInfo) bool {
return ms[shouldMatch].Target == "true" return s[shouldMatch].Target == "true"
} }
func (ms mockScope) setDefaults() {} func (s mockScope) setDefaults() {}
const ( const (
shouldMatch = "should-match-entry" shouldMatch = "should-match-entry"
@ -144,6 +145,15 @@ func stubInfoScope(match string) mockScope {
return sc return sc
} }
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
func (s mockScope) Conceal() string { return conceal(s) }
func (s mockScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
func (s mockScope) String() string { return conceal(s) }
func (s mockScope) PlainString() string { return plainString(s) }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// selectors // selectors
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -155,7 +165,7 @@ type mockSel struct {
func stubSelector(resourceOwners []string) mockSel { func stubSelector(resourceOwners []string) mockSel {
return mockSel{ return mockSel{
Selector: Selector{ Selector: Selector{
ResourceOwners: filterize(scopeConfig{}, resourceOwners...), ResourceOwners: filterFor(scopeConfig{}, resourceOwners...),
Service: ServiceExchange, Service: ServiceExchange,
Excludes: []scope{scope(stubScope(""))}, Excludes: []scope{scope(stubScope(""))},
Filters: []scope{scope(stubScope(""))}, Filters: []scope{scope(stubScope(""))},
@ -177,10 +187,10 @@ func setScopesToDefault[T scopeT](ts []T) []T {
} }
// calls assert.Equal(t, v, getCatValue(sc, k)[0]) on each k:v pair in the map // calls assert.Equal(t, v, getCatValue(sc, k)[0]) on each k:v pair in the map
func scopeMustHave[T scopeT](t *testing.T, sc T, m map[categorizer]string) { func scopeMustHave[T scopeT](t *testing.T, sc T, m map[categorizer][]string) {
for k, v := range m { for k, vs := range m {
t.Run(k.String(), func(t *testing.T) { t.Run(k.String(), func(t *testing.T) {
assert.Equal(t, split(v), getCatValue(sc, k), "Key: %s", k) assert.Equal(t, vs, getCatValue(sc, k), "Key: %s", k)
}) })
} }
} }

View File

@ -2,6 +2,7 @@ package selectors
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -120,6 +121,15 @@ func (s oneDrive) PathCategories() selectorPathCategories {
} }
} }
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
func (s OneDriveScope) Conceal() string { return conceal(s) }
func (s OneDriveScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
func (s OneDriveScope) String() string { return conceal(s) }
func (s OneDriveScope) PlainString() string { return plainString(s) }
// ------------------- // -------------------
// Scope Factories // Scope Factories
@ -249,7 +259,7 @@ func (s *oneDrive) CreatedAfter(timeStrings string) []OneDriveScope {
OneDriveItem, OneDriveItem,
FileInfoCreatedAfter, FileInfoCreatedAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), filters.Less),
} }
} }
@ -263,7 +273,7 @@ func (s *oneDrive) CreatedBefore(timeStrings string) []OneDriveScope {
OneDriveItem, OneDriveItem,
FileInfoCreatedBefore, FileInfoCreatedBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), filters.Greater),
} }
} }
@ -277,7 +287,7 @@ func (s *oneDrive) ModifiedAfter(timeStrings string) []OneDriveScope {
OneDriveItem, OneDriveItem,
FileInfoModifiedAfter, FileInfoModifiedAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), filters.Less),
} }
} }
@ -291,7 +301,7 @@ func (s *oneDrive) ModifiedBefore(timeStrings string) []OneDriveScope {
OneDriveItem, OneDriveItem,
FileInfoModifiedBefore, FileInfoModifiedBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), filters.Greater),
} }
} }
@ -488,12 +498,6 @@ func (s OneDriveScope) setDefaults() {
} }
} }
// DiscreteCopy makes a clone of the scope, then replaces the clone's user comparison
// with only the provided user.
func (s OneDriveScope) DiscreteCopy(user string) OneDriveScope {
return discreteCopy(s, user)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Backup Details Filtering // Backup Details Filtering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -74,9 +74,9 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_AllData() {
scopeMustHave( scopeMustHave(
t, t,
OneDriveScope(scope), OneDriveScope(scope),
map[categorizer]string{ map[categorizer][]string{
OneDriveItem: AnyTgt, OneDriveItem: Any(),
OneDriveFolder: AnyTgt, OneDriveFolder: Any(),
}, },
) )
} }
@ -106,9 +106,9 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Include_AllData() {
scopeMustHave( scopeMustHave(
t, t,
OneDriveScope(sc), OneDriveScope(sc),
map[categorizer]string{ map[categorizer][]string{
OneDriveItem: AnyTgt, OneDriveItem: Any(),
OneDriveFolder: AnyTgt, OneDriveFolder: Any(),
}, },
) )
} }
@ -136,9 +136,9 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Exclude_AllData() {
scopeMustHave( scopeMustHave(
t, t,
OneDriveScope(sc), OneDriveScope(sc),
map[categorizer]string{ map[categorizer][]string{
OneDriveItem: AnyTgt, OneDriveItem: Any(),
OneDriveFolder: AnyTgt, OneDriveFolder: Any(),
}, },
) )
} }

View File

@ -2,9 +2,10 @@ package selectors
import ( import (
"context" "context"
"encoding/json"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
@ -152,6 +153,9 @@ type (
// Primarily to ensure that root- or mid-tier scopes (such as folders) // Primarily to ensure that root- or mid-tier scopes (such as folders)
// cascade 'Any' matching to more granular categories. // cascade 'Any' matching to more granular categories.
setDefaults() setDefaults()
// Scopes need to comply with PII printing controls.
clues.PlainConcealer
} }
// scopeT is the generic type interface of a scoper. // scopeT is the generic type interface of a scoper.
scopeT interface { scopeT interface {
@ -163,7 +167,7 @@ type (
// makeScope produces a well formatted, typed scope that ensures all base values are populated. // makeScope produces a well formatted, typed scope that ensures all base values are populated.
func makeScope[T scopeT]( func makeScope[T scopeT](
cat categorizer, cat categorizer,
vs []string, tgts []string,
opts ...option, opts ...option,
) T { ) T {
sc := &scopeConfig{} sc := &scopeConfig{}
@ -172,7 +176,7 @@ func makeScope[T scopeT](
s := T{ s := T{
scopeKeyCategory: filters.Identity(cat.String()), scopeKeyCategory: filters.Identity(cat.String()),
scopeKeyDataType: filters.Identity(cat.leafCat().String()), scopeKeyDataType: filters.Identity(cat.leafCat().String()),
cat.String(): filterize(*sc, vs...), cat.String(): filterFor(*sc, tgts...),
} }
return s return s
@ -182,17 +186,61 @@ func makeScope[T scopeT](
// towards identifying filter-type scopes, that ensures all base values are populated. // towards identifying filter-type scopes, that ensures all base values are populated.
func makeInfoScope[T scopeT]( func makeInfoScope[T scopeT](
cat, infoCat categorizer, cat, infoCat categorizer,
vs []string, tgts []string,
f func([]string) filters.Filter, ff filterFunc,
opts ...option,
) T { ) T {
sc := &scopeConfig{}
sc.populate(opts...)
return T{ return T{
scopeKeyCategory: filters.Identity(cat.String()), scopeKeyCategory: filters.Identity(cat.String()),
scopeKeyDataType: filters.Identity(cat.leafCat().String()), scopeKeyDataType: filters.Identity(cat.leafCat().String()),
scopeKeyInfoCategory: filters.Identity(infoCat.String()), scopeKeyInfoCategory: filters.Identity(infoCat.String()),
infoCat.String(): f(clean(vs)), infoCat.String(): filterize(*sc, ff, tgts...),
} }
} }
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
// loggableMSS transforms the scope into a map by stringifying each filter.
func loggableMSS[T scopeT](s T, plain bool) map[string]string {
m := map[string]string{}
for k, filt := range s {
if plain {
m[k] = filt.PlainString()
} else {
m[k] = filt.Conceal()
}
}
return m
}
func conceal[T scopeT](s T) string {
return marshalScope(loggableMSS(s, false))
}
func format[T scopeT](s T, fs fmt.State, _ rune) {
fmt.Fprint(fs, conceal(s))
}
func plainString[T scopeT](s T) string {
return marshalScope(loggableMSS(s, true))
}
func marshalScope(mss map[string]string) string {
bs, err := json.Marshal(mss)
if err != nil {
return "error-marshalling-selector"
}
return string(bs)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// scope funcs // scope funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -229,17 +277,16 @@ func matchesAny[T scopeT, C categoryT](s T, cat C, inpts []string) bool {
// getCategory returns the scope's category value. // getCategory returns the scope's category value.
// if s is an info-type scope, returns the info category. // if s is an info-type scope, returns the info category.
func getCategory[T scopeT](s T) string { func getCategory[T scopeT](s T) string {
return s[scopeKeyCategory].Target return s[scopeKeyCategory].Identity
} }
// getInfoCategory returns the scope's infoFilter category value. // getInfoCategory returns the scope's infoFilter category value.
func getInfoCategory[T scopeT](s T) string { func getInfoCategory[T scopeT](s T) string {
return s[scopeKeyInfoCategory].Target return s[scopeKeyInfoCategory].Identity
} }
// getCatValue takes the value of s[cat], split it by the standard // getCatValue takes the value of s[cat] and returns the slice.
// delimiter, and returns the slice. If s[cat] is nil, returns // If s[cat] is nil, returns None().
// None().
func getCatValue[T scopeT](s T, cat categorizer) []string { func getCatValue[T scopeT](s T, cat categorizer) []string {
filt, ok := s[cat.String()] filt, ok := s[cat.String()]
if !ok { if !ok {
@ -250,7 +297,7 @@ func getCatValue[T scopeT](s T, cat categorizer) []string {
return filt.Targets return filt.Targets
} }
return split(filt.Target) return filt.Targets
} }
// set sets a value by category to the scope. Only intended for internal // set sets a value by category to the scope. Only intended for internal
@ -259,22 +306,11 @@ func set[T scopeT](s T, cat categorizer, v []string, opts ...option) T {
sc := &scopeConfig{} sc := &scopeConfig{}
sc.populate(opts...) sc.populate(opts...)
s[cat.String()] = filterize(*sc, v...) s[cat.String()] = filterFor(*sc, v...)
return s return s
} }
// discreteCopy makes a shallow clone of the scocpe, and sets the resource
// owner filter target in the clone to the provided string.
func discreteCopy[T scopeT](s T, resourceOwner string) T {
clone := maps.Clone(s)
return set(
clone,
clone.categorizer().rootCat(),
[]string{resourceOwner})
}
// returns true if the category is included in the scope's category type, // returns true if the category is included in the scope's category type,
// and the value is set to None(). // and the value is set to None().
func isNoneTarget[T scopeT, C categoryT](s T, cat C) bool { func isNoneTarget[T scopeT, C categoryT](s T, cat C) bool {
@ -316,7 +352,7 @@ func reduce[T scopeT, C categoryT](
// if a DiscreteOwner is specified, only match details for that owner. // if a DiscreteOwner is specified, only match details for that owner.
matchesResourceOwner := s.ResourceOwners matchesResourceOwner := s.ResourceOwners
if len(s.DiscreteOwner) > 0 { if len(s.DiscreteOwner) > 0 {
matchesResourceOwner = filterize(scopeConfig{}, s.DiscreteOwner) matchesResourceOwner = filterFor(scopeConfig{}, s.DiscreteOwner)
} }
// aggregate each scope type by category for easier isolation in future processing. // aggregate each scope type by category for easier isolation in future processing.

View File

@ -1,6 +1,9 @@
package selectors package selectors
import ( import (
"fmt"
"io"
"strings"
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -57,7 +60,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "blank value", name: "blank value",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = filters.Equal("") stub[rootCatStub.String()] = filters.Equal([]string{""})
return stub return stub
}, },
check: rootCatStub.String(), check: rootCatStub.String(),
@ -67,7 +70,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "blank target", name: "blank target",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = filterize(scopeConfig{}, "fnords") stub[rootCatStub.String()] = filterFor(scopeConfig{}, "fnords")
return stub return stub
}, },
check: "", check: "",
@ -77,7 +80,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "matching target", name: "matching target",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = filterize(scopeConfig{}, rootCatStub.String()) stub[rootCatStub.String()] = filterFor(scopeConfig{}, rootCatStub.String())
return stub return stub
}, },
check: rootCatStub.String(), check: rootCatStub.String(),
@ -87,7 +90,7 @@ func (suite *SelectorScopesSuite) TestContains() {
name: "non-matching target", name: "non-matching target",
scope: func() mockScope { scope: func() mockScope {
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = filterize(scopeConfig{}, rootCatStub.String()) stub[rootCatStub.String()] = filterFor(scopeConfig{}, rootCatStub.String())
return stub return stub
}, },
check: "smarf", check: "smarf",
@ -109,7 +112,7 @@ func (suite *SelectorScopesSuite) TestGetCatValue() {
t := suite.T() t := suite.T()
stub := stubScope("") stub := stubScope("")
stub[rootCatStub.String()] = filterize(scopeConfig{}, rootCatStub.String()) stub[rootCatStub.String()] = filterFor(scopeConfig{}, rootCatStub.String())
assert.Equal(t, assert.Equal(t,
[]string{rootCatStub.String()}, []string{rootCatStub.String()},
@ -342,7 +345,7 @@ func (suite *SelectorScopesSuite) TestScopesByCategory() {
t := suite.T() t := suite.T()
s1 := stubScope("") s1 := stubScope("")
s2 := stubScope("") s2 := stubScope("")
s2[scopeKeyCategory] = filterize(scopeConfig{}, unknownCatStub.String()) s2[scopeKeyCategory] = filterFor(scopeConfig{}, unknownCatStub.String())
result := scopesByCategory[mockScope]( result := scopesByCategory[mockScope](
[]scope{scope(s1), scope(s2)}, []scope{scope(s1), scope(s2)},
map[path.CategoryType]mockCategorizer{ map[path.CategoryType]mockCategorizer{
@ -449,8 +452,8 @@ func (suite *SelectorScopesSuite) TestMatchesPathValues() {
pvs[leafCatStub] = append(pvs[leafCatStub], test.shortRef) pvs[leafCatStub] = append(pvs[leafCatStub], test.shortRef)
sc := stubScope("") sc := stubScope("")
sc[rootCatStub.String()] = filterize(scopeConfig{}, test.rootVal) sc[rootCatStub.String()] = filterFor(scopeConfig{}, test.rootVal)
sc[leafCatStub.String()] = filterize(scopeConfig{}, test.leafVal) sc[leafCatStub.String()] = filterFor(scopeConfig{}, test.leafVal)
test.expect(t, matchesPathValues(sc, cat, pvs)) test.expect(t, matchesPathValues(sc, cat, pvs))
}) })
@ -504,47 +507,6 @@ func (suite *SelectorScopesSuite) TestClean() {
} }
} }
func (suite *SelectorScopesSuite) TestWrapFilter() {
table := []struct {
name string
filter filterFunc
input []string
comparator int
target string
}{
{
name: "any",
filter: filters.Contains,
input: Any(),
comparator: int(filters.Passes),
target: AnyTgt,
},
{
name: "none",
filter: filters.Greater,
input: None(),
comparator: int(filters.Fails),
target: NoneTgt,
},
{
name: "something",
filter: filters.Equal,
input: []string{"userid"},
comparator: int(filters.EqualTo),
target: "userid",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ff := wrapFilter(test.filter)(test.input)
assert.Equal(t, int(ff.Comparator), test.comparator)
assert.Equal(t, ff.Target, test.target)
})
}
}
func (suite *SelectorScopesSuite) TestScopeConfig() { func (suite *SelectorScopesSuite) TestScopeConfig() {
input := "input" input := "input"
@ -568,25 +530,79 @@ func (suite *SelectorScopesSuite) TestScopeConfig() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
result := filterize(test.config, input) result := filterFor(test.config, input)
assert.Equal(t, test.expect, int(result.Comparator)) assert.Equal(t, test.expect, int(result.Comparator))
}) })
} }
} }
func (suite *SelectorScopesSuite) TestDiscreteCopy() { var _ fmt.State = &mockFMTState{}
var (
t = suite.T()
orig = stubScope(AnyTgt)
clone = discreteCopy(orig, "fnords")
)
for k, v := range orig { type mockFMTState struct {
if k != rootCatStub.String() { w io.Writer
assert.Equal(t, v.Target, clone[k].Target) }
} else {
assert.Equal(t, AnyTgt, v.Target) func (ms mockFMTState) Write(bs []byte) (int, error) { return ms.w.Write(bs) }
assert.Equal(t, "fnords", clone[k].Target) func (ms mockFMTState) Width() (int, bool) { return 0, false }
} func (ms mockFMTState) Precision() (int, bool) { return 0, false }
func (ms mockFMTState) Flag(int) bool { return false }
func (suite *SelectorScopesSuite) TestScopesPII() {
table := []struct {
name string
s mockScope
contains []string
containsPlain []string
}{
{
name: "empty",
s: mockScope{},
contains: []string{`{}`},
containsPlain: []string{`{}`},
},
{
name: "multiple filters",
s: mockScope{
"pass": filterFor(scopeConfig{}, "*"),
"fail": filterFor(scopeConfig{}, ""),
"foo": filterFor(scopeConfig{}, "bar"),
"qux": filterFor(scopeConfig{}, "fnords", "smarf"),
},
contains: []string{
`"pass":"Pass"`,
`"fail":"Fail"`,
`"foo":"EQ:bar"`,
`"qux":"Cont:fnords,smarf"`,
},
containsPlain: []string{
`"pass":"Pass"`,
`"fail":"Fail"`,
`"foo":"EQ:bar"`,
`"qux":"Cont:fnords,smarf"`,
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := conceal(test.s)
for _, c := range test.contains {
assert.Contains(t, result, c, "conceal")
}
result = plainString(test.s)
for _, c := range test.containsPlain {
assert.Contains(t, result, c, "plainString")
}
sb := &strings.Builder{}
fs := mockFMTState{sb}
format(test.s, &fs, 0)
for _, c := range test.contains {
assert.Contains(t, sb.String(), c, "conceal")
}
})
} }
} }

View File

@ -4,9 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
@ -60,9 +60,8 @@ const (
) )
var ( var (
delimiter = string('\x1F') passAny = filters.Pass()
passAny = filters.Pass() failAny = filters.Fail()
failAny = filters.Fail()
) )
// All is the resource name that gets output when the resource is AnyTgt. // All is the resource name that gets output when the resource is AnyTgt.
@ -134,7 +133,7 @@ func newSelector(s service, resourceOwners []string) Selector {
return Selector{ return Selector{
Service: s, Service: s,
ResourceOwners: filterize(scopeConfig{}, resourceOwners...), ResourceOwners: filterFor(scopeConfig{}, resourceOwners...),
DiscreteOwner: owner, DiscreteOwner: owner,
Excludes: []scope{}, Excludes: []scope{},
Includes: []scope{}, Includes: []scope{},
@ -145,7 +144,7 @@ func newSelector(s service, resourceOwners []string) Selector {
// in the selector. // in the selector.
// TODO(rkeepers): remove in favor of split and s.DiscreteOwner // TODO(rkeepers): remove in favor of split and s.DiscreteOwner
func (s Selector) DiscreteResourceOwners() []string { func (s Selector) DiscreteResourceOwners() []string {
return split(s.ResourceOwners.Target) return s.ResourceOwners.Targets
} }
// SetDiscreteOwnerIDName ensures the selector has the correct discrete owner // SetDiscreteOwnerIDName ensures the selector has the correct discrete owner
@ -220,7 +219,7 @@ func splitByResourceOwner[T scopeT, C categoryT](s Selector, allOwners []string,
targets := allOwners targets := allOwners
if !isAnyResourceOwner(s) { if !isAnyResourceOwner(s) {
targets = split(s.ResourceOwners.Target) targets = s.ResourceOwners.Targets
} }
ss := make([]Selector, 0, len(targets)) ss := make([]Selector, 0, len(targets))
@ -234,15 +233,6 @@ func splitByResourceOwner[T scopeT, C categoryT](s Selector, allOwners []string,
return ss return ss
} }
func (s Selector) String() string {
bs, err := json.Marshal(s)
if err != nil {
return "error"
}
return string(bs)
}
// appendScopes iterates through each scope in the list of scope slices, // appendScopes iterates through each scope in the list of scope slices,
// calling setDefaults() to ensure it is completely populated, and appends // calling setDefaults() to ensure it is completely populated, and appends
// those scopes to the `to` slice. // those scopes to the `to` slice.
@ -329,6 +319,84 @@ func selectorAsIface[T any](s Selector) (T, error) {
return t, err return t, err
} }
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
var _ clues.PlainConcealer = &Selector{}
type loggableSelector struct {
Service service `json:"service,omitempty"`
ResourceOwners string `json:"resourceOwners,omitempty"`
DiscreteOwner string `json:"discreteOwner,omitempty"`
Excludes []map[string]string `json:"exclusions,omitempty"`
Filters []map[string]string `json:"filters,omitempty"`
Includes []map[string]string `json:"includes,omitempty"`
}
func (s Selector) Conceal() string {
ls := loggableSelector{
Service: s.Service,
ResourceOwners: s.ResourceOwners.Conceal(),
DiscreteOwner: clues.Conceal(s.DiscreteOwner),
Excludes: toMSS(s.Excludes, false),
Filters: toMSS(s.Filters, false),
Includes: toMSS(s.Includes, false),
}
return ls.marshal()
}
func (s Selector) Format(fs fmt.State, _ rune) {
fmt.Fprint(fs, s.Conceal())
}
func (s Selector) String() string {
return s.Conceal()
}
func (s Selector) PlainString() string {
ls := loggableSelector{
Service: s.Service,
ResourceOwners: s.ResourceOwners.PlainString(),
DiscreteOwner: s.DiscreteOwner,
Excludes: toMSS(s.Excludes, true),
Filters: toMSS(s.Filters, true),
Includes: toMSS(s.Includes, true),
}
return ls.marshal()
}
func toMSS(scs []scope, plain bool) []map[string]string {
mss := make([]map[string]string, 0, len(scs))
for _, s := range scs {
m := map[string]string{}
for k, filt := range s {
if plain {
m[k] = filt.PlainString()
} else {
m[k] = filt.Conceal()
}
}
mss = append(mss, m)
}
return mss
}
func (ls loggableSelector) marshal() string {
bs, err := json.Marshal(ls)
if err != nil {
return "error-marshalling-selector"
}
return string(bs)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// helpers // helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -339,7 +407,7 @@ func resourceOwnersIn(s []scope, rootCat string) []string {
rm := map[string]struct{}{} rm := map[string]struct{}{}
for _, sc := range s { for _, sc := range s {
for _, v := range split(sc[rootCat].Target) { for _, v := range sc[rootCat].Targets {
rm[v] = struct{}{} rm[v] = struct{}{}
} }
} }
@ -357,7 +425,7 @@ func resourceOwnersIn(s []scope, rootCat string) []string {
// produces the discrete set of path categories in the slice of scopes. // produces the discrete set of path categories in the slice of scopes.
func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType { func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType {
rm := map[path.CategoryType]struct{}{} m := map[path.CategoryType]struct{}{}
for _, s := range ss { for _, s := range ss {
t := T(s) t := T(s)
@ -367,16 +435,10 @@ func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType {
continue continue
} }
rm[lc.PathType()] = struct{}{} m[lc.PathType()] = struct{}{}
} }
rs := []path.CategoryType{} return maps.Keys(m)
for k := range rm {
rs = append(rs, k)
}
return rs
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -438,14 +500,6 @@ func badCastErr(cast, is service) error {
return clues.Stack(ErrorBadSelectorCast, clues.New(fmt.Sprintf("%s is not %s", cast, is))) return clues.Stack(ErrorBadSelectorCast, clues.New(fmt.Sprintf("%s is not %s", cast, is)))
} }
func join(s ...string) string {
return strings.Join(s, delimiter)
}
func split(s string) []string {
return strings.Split(s, delimiter)
}
// if the provided slice contains Any, returns [Any] // if the provided slice contains Any, returns [Any]
// if the slice contains None, returns [None] // if the slice contains None, returns [None]
// if the slice contains Any and None, returns the first // if the slice contains Any and None, returns the first
@ -469,68 +523,84 @@ func clean(s []string) []string {
return s return s
} }
type filterFunc func([]string) filters.Filter
// filterize turns the slice into a filter. // filterize turns the slice into a filter.
// if the input is Any(), returns a passAny filter. // if the input is Any(), returns a passAny filter.
// if the input is None(), returns a failAny filter. // if the input is None(), returns a failAny filter.
// if the scopeConfig specifies a filter, use that filter. // if the scopeConfig specifies a filter, use that filter.
// if the input is len(1), returns an Equals filter. // if the input is len(1), returns an Equals filter.
// otherwise returns a Contains filter. // otherwise returns a Contains filter.
func filterize(sc scopeConfig, s ...string) filters.Filter { func filterFor(sc scopeConfig, targets ...string) filters.Filter {
s = clean(s) return filterize(sc, nil, targets...)
}
if len(s) == 0 || s[0] == NoneTgt { // filterize turns the slice into a filter.
// if the input is Any(), returns a passAny filter.
// if the input is None(), returns a failAny filter.
// if the scopeConfig specifies a filter, use that filter.
// if defaultFilter is non-nil, returns that filter.
// if the input is len(1), returns an Equals filter.
// otherwise returns a Contains filter.
func filterize(
sc scopeConfig,
defaultFilter filterFunc,
targets ...string,
) filters.Filter {
targets = clean(targets)
if len(targets) == 0 || targets[0] == NoneTgt {
return failAny return failAny
} }
if s[0] == AnyTgt { if targets[0] == AnyTgt {
return passAny return passAny
} }
if sc.usePathFilter { if sc.usePathFilter {
if sc.useEqualsFilter { if sc.useEqualsFilter {
return filters.PathEquals(s) return filters.PathEquals(targets)
} }
if sc.usePrefixFilter { if sc.usePrefixFilter {
return filters.PathPrefix(s) return filters.PathPrefix(targets)
} }
if sc.useSuffixFilter { if sc.useSuffixFilter {
return filters.PathSuffix(s) return filters.PathSuffix(targets)
} }
return filters.PathContains(s) return filters.PathContains(targets)
} }
if sc.usePrefixFilter { if sc.usePrefixFilter {
return filters.Prefix(join(s...)) return filters.Prefix(targets)
} }
if sc.useSuffixFilter { if sc.useSuffixFilter {
return filters.Suffix(join(s...)) return filters.Suffix(targets)
} }
if len(s) == 1 { if defaultFilter != nil {
return filters.Equal(s[0]) return defaultFilter(targets)
} }
return filters.Contains(join(s...)) if len(targets) == 1 {
return filters.Equal(targets)
}
return filters.Contains(targets)
} }
type (
filterFunc func(string) filters.Filter
sliceFilterFunc func([]string) filters.Filter
)
// pathFilterFactory returns the appropriate path filter // pathFilterFactory returns the appropriate path filter
// (contains, prefix, or suffix) for the provided options. // (contains, prefix, or suffix) for the provided options.
// If multiple options are flagged, Prefix takes priority. // If multiple options are flagged, Prefix takes priority.
// If no options are provided, returns PathContains. // If no options are provided, returns PathContains.
func pathFilterFactory(opts ...option) sliceFilterFunc { func pathFilterFactory(opts ...option) filterFunc {
sc := &scopeConfig{} sc := &scopeConfig{}
sc.populate(opts...) sc.populate(opts...)
var ff sliceFilterFunc var ff filterFunc
switch true { switch true {
case sc.usePrefixFilter: case sc.usePrefixFilter:
@ -546,7 +616,7 @@ func pathFilterFactory(opts ...option) sliceFilterFunc {
return wrapSliceFilter(ff) return wrapSliceFilter(ff)
} }
func wrapSliceFilter(ff sliceFilterFunc) sliceFilterFunc { func wrapSliceFilter(ff filterFunc) filterFunc {
return func(s []string) filters.Filter { return func(s []string) filters.Filter {
s = clean(s) s = clean(s)
@ -558,23 +628,6 @@ func wrapSliceFilter(ff sliceFilterFunc) sliceFilterFunc {
} }
} }
// wrapFilter produces a func that filterizes the input by:
// - cleans the input string
// - normalizes the cleaned input (returns anyFail if empty, allFail if *)
// - joins the string
// - and generates a filter with the joined input.
func wrapFilter(ff filterFunc) sliceFilterFunc {
return func(s []string) filters.Filter {
s = clean(s)
if f, ok := isAnyOrNone(s); ok {
return f
}
return ff(join(s...))
}
}
// returns (<filter>, true) if s is len==1 and s[0] is // returns (<filter>, true) if s is len==1 and s[0] is
// anyTgt or noneTgt, implying that the caller should use // anyTgt or noneTgt, implying that the caller should use
// the returned filter. On (<filter>, false), the caller // the returned filter. On (<filter>, false), the caller

View File

@ -1,6 +1,7 @@
package selectors package selectors
import ( import (
"fmt"
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -28,6 +29,16 @@ func (suite *SelectorSuite) TestNewSelector() {
assert.NotNil(t, s.Includes) assert.NotNil(t, s.Includes)
} }
// set the clues hashing to mask for the span of this suite
func (suite *SelectorSuite) SetupSuite() {
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
}
// revert clues hashing to plaintext for all other tests
func (suite *SelectorSuite) TeardownSuite() {
clues.SetHasher(clues.NoHash())
}
func (suite *SelectorSuite) TestBadCastErr() { func (suite *SelectorSuite) TestBadCastErr() {
err := badCastErr(ServiceUnknown, ServiceExchange) err := badCastErr(ServiceUnknown, ServiceExchange)
assert.Error(suite.T(), err, clues.ToCore(err)) assert.Error(suite.T(), err, clues.ToCore(err))
@ -56,36 +67,21 @@ func (suite *SelectorSuite) TestResourceOwnersIn() {
input: []scope{{rootCat: filters.Identity("foo")}}, input: []scope{{rootCat: filters.Identity("foo")}},
expect: []string{"foo"}, expect: []string{"foo"},
}, },
{
name: "multiple values",
input: []scope{{rootCat: filters.Identity(join("foo", "bar"))}},
expect: []string{"foo", "bar"},
},
{
name: "with any",
input: []scope{{rootCat: filters.Identity(join("foo", "bar", AnyTgt))}},
expect: []string{"foo", "bar"},
},
{
name: "with none",
input: []scope{{rootCat: filters.Identity(join("foo", "bar", NoneTgt))}},
expect: []string{"foo", "bar"},
},
{ {
name: "multiple scopes", name: "multiple scopes",
input: []scope{ input: []scope{
{rootCat: filters.Identity(join("foo", "bar"))}, {rootCat: filters.Identity("foo,bar")},
{rootCat: filters.Identity(join("baz"))}, {rootCat: filters.Identity("baz")},
}, },
expect: []string{"foo", "bar", "baz"}, expect: []string{"foo,bar", "baz"},
}, },
{ {
name: "multiple scopes with duplicates", name: "multiple scopes with duplicates",
input: []scope{ input: []scope{
{rootCat: filters.Identity(join("foo", "bar"))}, {rootCat: filters.Identity("foo")},
{rootCat: filters.Identity(join("baz", "foo"))}, {rootCat: filters.Identity("foo")},
}, },
expect: []string{"foo", "bar", "baz"}, expect: []string{"foo"},
}, },
} }
for _, test := range table { for _, test := range table {
@ -138,9 +134,9 @@ func (suite *SelectorSuite) TestContains() {
key := rootCatStub key := rootCatStub
target := "fnords" target := "fnords"
does := stubScope("") does := stubScope("")
does[key.String()] = filterize(scopeConfig{}, target) does[key.String()] = filterFor(scopeConfig{}, target)
doesNot := stubScope("") doesNot := stubScope("")
doesNot[key.String()] = filterize(scopeConfig{}, "smarf") doesNot[key.String()] = filterFor(scopeConfig{}, "smarf")
assert.True(t, matches(does, key, target), "does contain") assert.True(t, matches(does, key, target), "does contain")
assert.False(t, matches(doesNot, key, target), "does not contain") assert.False(t, matches(doesNot, key, target), "does not contain")
@ -420,3 +416,81 @@ func (suite *SelectorSuite) TestPathCategories_includes() {
}) })
} }
} }
func (suite *SelectorSuite) TestSelector_pii() {
table := []struct {
name string
sel func() Selector
expect string
expectPlain string
}{
{
name: "empty selector",
sel: func() Selector { return Selector{} },
expect: `{"resourceOwners":"UnknownComparison:"}`,
expectPlain: `{"resourceOwners":"UnknownComparison:"}`,
},
{
name: "no scopes",
sel: func() Selector {
return Selector{
Service: ServiceUnknown,
DiscreteOwner: "owner",
ResourceOwners: filterFor(scopeConfig{}, "owner_1", "owner_2"),
}
},
expect: `{"resourceOwners":"Cont:***,***","discreteOwner":"***"}`,
expectPlain: `{"resourceOwners":"Cont:owner_1,owner_2","discreteOwner":"owner"}`,
},
{
name: "one scope each type",
sel: func() Selector {
s := NewExchangeBackup([]string{"owner_1", "owner_2"})
s.DiscreteOwner = "owner"
s.Exclude(s.MailFolders([]string{"e"}))
s.Filter(s.MailFolders([]string{"f"}, SuffixMatch()))
s.Include(s.MailFolders([]string{"i"}, PrefixMatch()))
return s.Selector
},
//nolint:lll
expect: `{"service":1,` +
`"resourceOwners":"Cont:***,***",` +
`"discreteOwner":"***",` +
`"exclusions":[{"ExchangeMail":"Pass","ExchangeMailFolder":"PathCont:***","category":"Identity:***","type":"Identity:***"}],` +
`"filters":[{"ExchangeMail":"Pass","ExchangeMailFolder":"PathSfx:***","category":"Identity:***","type":"Identity:***"}],` +
`"includes":[{"ExchangeMail":"Pass","ExchangeMailFolder":"PathPfx:***","category":"Identity:***","type":"Identity:***"}]}`,
//nolint:lll
expectPlain: `{"service":1,` +
`"resourceOwners":"Cont:owner_1,owner_2",` +
`"discreteOwner":"owner",` +
`"exclusions":[{"ExchangeMail":"Pass","ExchangeMailFolder":"PathCont:e","category":"Identity:ExchangeMailFolder","type":"Identity:ExchangeMail"}],` +
`"filters":[{"ExchangeMail":"Pass","ExchangeMailFolder":"PathSfx:f","category":"Identity:ExchangeMailFolder","type":"Identity:ExchangeMail"}],` +
`"includes":[{"ExchangeMail":"Pass","ExchangeMailFolder":"PathPfx:i","category":"Identity:ExchangeMailFolder","type":"Identity:ExchangeMail"}]}`,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := test.sel().Conceal()
assert.Equal(t, test.expect, result, "conceal")
result = test.sel().String()
assert.Equal(t, test.expect, result, "string")
result = test.sel().PlainString()
assert.Equal(t, test.expectPlain, result, "plainString")
result = fmt.Sprintf("%s", test.sel())
assert.Equal(t, test.expect, result, "fmt %%s")
result = fmt.Sprintf("%v", test.sel())
assert.Equal(t, test.expect, result, "fmt %%v")
result = fmt.Sprintf("%+v", test.sel())
assert.Equal(t, test.expect, result, "fmt %%+v")
})
}
}

View File

@ -2,6 +2,7 @@ package selectors
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -120,6 +121,15 @@ func (s sharePoint) PathCategories() selectorPathCategories {
} }
} }
// ---------------------------------------------------------------------------
// Stringers and Concealers
// ---------------------------------------------------------------------------
func (s SharePointScope) Conceal() string { return conceal(s) }
func (s SharePointScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
func (s SharePointScope) String() string { return conceal(s) }
func (s SharePointScope) PlainString() string { return plainString(s) }
// ------------------- // -------------------
// Scope Factories // Scope Factories
@ -286,7 +296,7 @@ func (s *sharePoint) Library(library string) []SharePointScope {
SharePointLibraryItem, SharePointLibraryItem,
SharePointInfoLibraryDrive, SharePointInfoLibraryDrive,
[]string{library}, []string{library},
wrapFilter(filters.Equal)), filters.Equal),
} }
} }
@ -366,7 +376,7 @@ func (s *sharePoint) CreatedAfter(timeStrings string) []SharePointScope {
SharePointLibraryItem, SharePointLibraryItem,
SharePointInfoCreatedAfter, SharePointInfoCreatedAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), filters.Less),
} }
} }
@ -376,7 +386,7 @@ func (s *sharePoint) CreatedBefore(timeStrings string) []SharePointScope {
SharePointLibraryItem, SharePointLibraryItem,
SharePointInfoCreatedBefore, SharePointInfoCreatedBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), filters.Greater),
} }
} }
@ -386,7 +396,7 @@ func (s *sharePoint) ModifiedAfter(timeStrings string) []SharePointScope {
SharePointLibraryItem, SharePointLibraryItem,
SharePointInfoModifiedAfter, SharePointInfoModifiedAfter,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Less)), filters.Less),
} }
} }
@ -396,7 +406,7 @@ func (s *sharePoint) ModifiedBefore(timeStrings string) []SharePointScope {
SharePointLibraryItem, SharePointLibraryItem,
SharePointInfoModifiedBefore, SharePointInfoModifiedBefore,
[]string{timeStrings}, []string{timeStrings},
wrapFilter(filters.Greater)), filters.Greater),
} }
} }
@ -648,12 +658,6 @@ func (s SharePointScope) setDefaults() {
} }
} }
// DiscreteCopy makes a shallow clone of the scope, then replaces the clone's
// site comparison with only the provided site.
func (s SharePointScope) DiscreteCopy(site string) SharePointScope {
return discreteCopy(s, site)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Backup Details Filtering // Backup Details Filtering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -81,18 +81,18 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_AllData() {
scopeMustHave( scopeMustHave(
t, t,
spsc, spsc,
map[categorizer]string{ map[categorizer][]string{
SharePointLibraryItem: AnyTgt, SharePointLibraryItem: Any(),
SharePointLibraryFolder: AnyTgt, SharePointLibraryFolder: Any(),
}, },
) )
case SharePointListItem: case SharePointListItem:
scopeMustHave( scopeMustHave(
t, t,
spsc, spsc,
map[categorizer]string{ map[categorizer][]string{
SharePointListItem: AnyTgt, SharePointListItem: Any(),
SharePointList: AnyTgt, SharePointList: Any(),
}, },
) )
} }
@ -109,8 +109,10 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs() {
s2 = "s2" s2 = "s2"
) )
sel := NewSharePointRestore([]string{s1, s2}) s12 := []string{s1, s2}
sel.Include(sel.WebURL([]string{s1, s2}))
sel := NewSharePointRestore(s12)
sel.Include(sel.WebURL(s12))
scopes := sel.Includes scopes := sel.Includes
require.Len(t, scopes, 3) require.Len(t, scopes, 3)
@ -118,7 +120,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs() {
scopeMustHave( scopeMustHave(
t, t,
SharePointScope(sc), SharePointScope(sc),
map[categorizer]string{SharePointWebURL: join(s1, s2)}, map[categorizer][]string{SharePointWebURL: s12},
) )
} }
} }
@ -127,17 +129,17 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs_any
table := []struct { table := []struct {
name string name string
in []string in []string
expect string expect []string
}{ }{
{ {
name: "any", name: "any",
in: []string{AnyTgt}, in: []string{AnyTgt},
expect: AnyTgt, expect: Any(),
}, },
{ {
name: "none", name: "none",
in: []string{NoneTgt}, in: []string{NoneTgt},
expect: NoneTgt, expect: None(),
}, },
} }
for _, test := range table { for _, test := range table {
@ -153,7 +155,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs_any
scopeMustHave( scopeMustHave(
t, t,
SharePointScope(sc), SharePointScope(sc),
map[categorizer]string{SharePointWebURL: test.expect}, map[categorizer][]string{SharePointWebURL: test.expect},
) )
} }
}) })
@ -168,8 +170,10 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Exclude_WebURLs() {
s2 = "s2" s2 = "s2"
) )
sel := NewSharePointRestore([]string{s1, s2}) s12 := []string{s1, s2}
sel.Exclude(sel.WebURL([]string{s1, s2}))
sel := NewSharePointRestore(s12)
sel.Exclude(sel.WebURL(s12))
scopes := sel.Excludes scopes := sel.Excludes
require.Len(t, scopes, 3) require.Len(t, scopes, 3)
@ -177,7 +181,7 @@ func (suite *SharePointSelectorSuite) TestSharePointSelector_Exclude_WebURLs() {
scopeMustHave( scopeMustHave(
t, t,
SharePointScope(sc), SharePointScope(sc),
map[categorizer]string{SharePointWebURL: join(s1, s2)}, map[categorizer][]string{SharePointWebURL: s12},
) )
} }
} }