Geodata: Reduce memory usage again (#5975)

https://github.com/XTLS/Xray-core/pull/5975#issuecomment-4274779560
This commit is contained in:
Meow
2026-04-26 00:15:37 +08:00
committed by GitHub
parent 7cf25970de
commit bc590bcb56
16 changed files with 700 additions and 50 deletions

View File

@@ -158,9 +158,12 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, client)
}
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
var domainMatcher geodata.DomainMatcher
if len(effectiveRules) > 0 {
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
}
// If there is no DNS client in config, add a `localhost` DNS client
@@ -271,25 +274,27 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching
hasMatch := false
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
if s.domainMatcher != nil {
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
}

View File

@@ -13,8 +13,8 @@ import (
// StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct {
reps [][]net.Address
matcher geodata.DomainMatcher
responses [][]net.Address
matcher geodata.DomainMatcher
}
// NewStaticHosts creates a new StaticHosts instance.
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
rep = append(rep, addr)
}
}
// if len(rep) == 0 {
// errors.LogError(context.Background(), "empty value in static hosts, ignore this rule: ", mapping.Domain)
// continue
// }
reps = append(reps, rep)
rules = append(rules, mapping.Domain)
}
if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
reps: reps,
matcher: matcher,
responses: reps,
matcher: matcher,
}, nil
}
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0)
found := false
for _, ruleIdx := range h.matcher.Match(domain) {
for _, rep := range h.reps[ruleIdx] {
for _, idx := range h.matcher.Match(domain) {
for _, rep := range h.responses[idx] {
if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
return nil, err
}
}
ips = append(ips, h.reps[ruleIdx]...)
ips = append(ips, h.responses[idx]...)
found = true
}
if !found {
@@ -122,5 +122,8 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5)
}

View File

@@ -23,10 +23,54 @@ type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}
type MphDomainMatcherFactory struct{}
type MphDomainMatcherFactory struct {
sync.Mutex
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
}
func buildDomainRulesKey(rules []*DomainRule) string {
var sb strings.Builder
cache := false
for _, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
sb.WriteString(v.Custom.Type.String())
sb.WriteString(":")
sb.WriteString(v.Custom.Value)
sb.WriteString(",")
case *DomainRule_Geosite:
cache = true
sb.WriteString(v.Geosite.File)
sb.WriteString(":")
sb.WriteString(v.Geosite.Code)
sb.WriteString("@")
sb.WriteString(v.Geosite.Attrs)
sb.WriteString(",")
default:
panic("unknown domain rule type")
}
}
if !cache {
return ""
}
return sb.String()
}
// BuildMatcher implements DomainMatcherFactory.
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
key := buildDomainRulesKey(rules)
if key != "" {
f.Lock()
defer f.Unlock()
if g := f.shared[key]; g != nil {
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
return g, nil
}
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
}
g := strmatcher.NewMphValueMatcher()
for i, r := range rules {
switch v := r.Value.(type) {
@@ -57,25 +101,30 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
if err := g.Build(); err != nil {
return nil, err
}
if key != "" {
f.shared[key] = g
}
return g, nil
}
type CompactDomainMatcherFactory struct {
sync.Mutex
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
shared map[string]strmatcher.MatcherSet // TODO: cleanup
}
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, error) {
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
f.Lock()
defer f.Unlock()
if m := f.shared[key]; m != nil {
return m, nil
if s := f.shared[key]; s != nil {
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil
}
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
g := strmatcher.NewLinearValueMatcher()
s := strmatcher.NewLinearAnyMatcher()
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
if err != nil {
return nil, err
@@ -87,16 +136,19 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
g.Add(m, 0)
s.Add(m)
}
f.shared[key] = g
return g, err
f.shared[key] = s
return s, err
}
// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
@@ -126,7 +178,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherGroup
matchers []strmatcher.MatcherSet
values []uint32
}
@@ -178,8 +230,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
default:
return &MphDomainMatcherFactory{}
return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
}
}

View File

@@ -10,7 +10,7 @@ import (
)
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
@@ -31,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
@@ -50,7 +50,7 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})

View File

@@ -816,8 +816,10 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
defer f.Unlock()
if ipset := f.shared[key]; ipset != nil {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil
}
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules {

View File

@@ -0,0 +1,53 @@
package strmatcher
// LinearAnyMatcher is an implementation of AnyMatcher.
type LinearAnyMatcher struct {
full *FullMatcherSet
domain *DomainMatcherSet
substr *SubstrMatcherSet
regex *SimpleMatcherSet
}
func NewLinearAnyMatcher() *LinearAnyMatcher {
return new(LinearAnyMatcher)
}
// Add implements AnyMatcher.Add.
func (s *LinearAnyMatcher) Add(matcher Matcher) {
switch matcher := matcher.(type) {
case FullMatcher:
if s.full == nil {
s.full = NewFullMatcherSet()
}
s.full.AddFullMatcher(matcher)
case DomainMatcher:
if s.domain == nil {
s.domain = NewDomainMatcherSet()
}
s.domain.AddDomainMatcher(matcher)
case SubstrMatcher:
if s.substr == nil {
s.substr = new(SubstrMatcherSet)
}
s.substr.AddSubstrMatcher(matcher)
default:
if s.regex == nil {
s.regex = new(SimpleMatcherSet)
}
s.regex.AddMatcher(matcher)
}
}
// MatchAny implements AnyMatcher.MatchAny.
func (s *LinearAnyMatcher) MatchAny(input string) bool {
if s.full != nil && s.full.MatchAny(input) {
return true
}
if s.domain != nil && s.domain.MatchAny(input) {
return true
}
if s.substr != nil && s.substr.MatchAny(input) {
return true
}
return s.regex != nil && s.regex.MatchAny(input)
}

View File

@@ -288,3 +288,65 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
return result
}
}
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
type MatcherSetForAll interface {
AddMatcher(matcher Matcher)
}
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
type MatcherSetForFull interface {
AddFullMatcher(matcher FullMatcher)
}
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
type MatcherSetForDomain interface {
AddDomainMatcher(matcher DomainMatcher)
}
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
type MatcherSetForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher)
}
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
type MatcherSetForRegex interface {
AddRegexMatcher(matcher *RegexMatcher)
}
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
// It returns error if the MatcherSet does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherSet.
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
if s, ok := s.(IndexMatcher); ok {
s.Add(matcher)
return nil
}
if s, ok := s.(MatcherSetForAll); ok {
s.AddMatcher(matcher)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if s, ok := s.(MatcherSetForFull); ok {
s.AddFullMatcher(matcher)
return nil
}
case DomainMatcher:
if s, ok := s.(MatcherSetForDomain); ok {
s.AddDomainMatcher(matcher)
return nil
}
case SubstrMatcher:
if s, ok := s.(MatcherSetForSubstr); ok {
s.AddSubstrMatcher(matcher)
return nil
}
case *RegexMatcher:
if s, ok := s.(MatcherSetForRegex); ok {
s.AddRegexMatcher(matcher)
return nil
}
}
return errors.New("cannot add matcher to matcher set")
}

View File

@@ -0,0 +1,79 @@
package strmatcher
type trieNode2 struct {
matched bool
children map[string]*trieNode2
}
// DomainMatcherSet is an implementation of MatcherSet.
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
type DomainMatcherSet struct {
root *trieNode2
}
func NewDomainMatcherSet() *DomainMatcherSet {
return &DomainMatcherSet{
root: new(trieNode2),
}
}
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
node := s.root
pattern := matcher.Pattern()
for i := len(pattern); i > 0; {
var part string
for j := i - 1; ; j-- {
if pattern[j] == '.' {
part = pattern[j+1 : i]
i = j
break
}
if j == 0 {
part = pattern[j:i]
i = j
break
}
}
if node.children == nil {
node.children = make(map[string]*trieNode2)
}
next := node.children[part]
if next == nil {
next = new(trieNode2)
node.children[part] = next
}
node = next
}
node.matched = true
}
// MatchAny implements MatcherSet.MatchAny.
func (s *DomainMatcherSet) MatchAny(input string) bool {
node := s.root
for i := len(input); i > 0; {
for j := i - 1; ; j-- {
if input[j] == '.' {
node = node.children[input[j+1:i]]
i = j
break
}
if j == 0 {
node = node.children[input[j:i]]
i = j
break
}
}
if node == nil {
return false
}
if node.matched {
return true
}
if node.children == nil {
return false
}
}
return false
}

View File

@@ -0,0 +1,95 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestDomainMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "a.b.com",
},
{
Pattern: "c.a.b.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "x.example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "a.b.com",
Result: true,
},
{
Domain: "c.a.b.com",
Result: true,
},
{
Domain: "c.a..b.com",
Result: false,
},
{
Domain: ".com",
Result: false,
},
{
Domain: "com",
Result: false,
},
{
Domain: "",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewDomainMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyDomainMatcherSet(t *testing.T) {
s := NewDomainMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}

View File

@@ -0,0 +1,24 @@
package strmatcher
// FullMatcherSet is an implementation of MatcherSet.
// It uses a hash table to facilitate exact match lookup.
type FullMatcherSet struct {
matchers map[string]struct{}
}
func NewFullMatcherSet() *FullMatcherSet {
return &FullMatcherSet{
matchers: make(map[string]struct{}),
}
}
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
s.matchers[matcher.Pattern()] = struct{}{}
}
// MatchAny implements MatcherSet.Any.
func (s *FullMatcherSet) MatchAny(input string) bool {
_, found := s.matchers[input]
return found
}

View File

@@ -0,0 +1,65 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestFullMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewFullMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyFullMatcherSet(t *testing.T) {
s := NewFullMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}

View File

@@ -0,0 +1,22 @@
package strmatcher
// SimpleMatcherSet is an implementation of MatcherSet.
// It simply stores all matchers in an array and sequentially matches them.
type SimpleMatcherSet struct {
matchers []Matcher
}
// AddMatcher implements MatcherSetForAll.AddMatcher.
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
s.matchers = append(s.matchers, matcher)
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SimpleMatcherSet) MatchAny(input string) bool {
for _, m := range s.matchers {
if m.Match(input) {
return true
}
}
return false
}

View File

@@ -0,0 +1,69 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSimpleMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "example.com",
mType: Domain,
},
{
pattern: "example.com",
mType: Full,
},
{
pattern: "example.com",
mType: Regex,
},
}
cases := []struct {
input string
output bool
}{
{
input: "www.example.com",
output: true,
},
{
input: "example.com",
output: true,
},
{
input: "www.e3ample.com",
output: false,
},
{
input: "xample.com",
output: false,
},
{
input: "xexample.com",
output: true,
},
{
input: "examplexcom",
output: true,
},
}
matcherSet := &SimpleMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}

View File

@@ -0,0 +1,24 @@
package strmatcher
import "strings"
// SubstrMatcherSet is implementation of MatcherSet,
// It is simply implmeneted to comply with the priority specification of Substr matchers.
type SubstrMatcherSet struct {
patterns []string
}
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
s.patterns = append(s.patterns, matcher.Pattern())
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SubstrMatcherSet) MatchAny(input string) bool {
for _, pattern := range s.patterns {
if strings.Contains(input, pattern) {
return true
}
}
return false
}

View File

@@ -0,0 +1,77 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSubstrMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "apis",
mType: Substr,
},
{
pattern: "google",
mType: Substr,
},
{
pattern: "apis",
mType: Substr,
},
}
cases := []struct {
input string
output bool
}{
{
input: "google.com",
output: true,
},
{
input: "apis.com",
output: true,
},
{
input: "googleapis.com",
output: true,
},
{
input: "fonts.googleapis.com",
output: true,
},
{
input: "apis.googleapis.com",
output: true,
},
{
input: "baidu.com",
output: false,
},
{
input: "goog",
output: false,
},
{
input: "api",
output: false,
},
}
matcherSet := &SubstrMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}

View File

@@ -15,7 +15,7 @@ const (
)
// Matcher is the interface to determine a string matches a pattern.
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
type Matcher interface {
// Type returns the matcher's type.
Type() Type
@@ -101,3 +101,21 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherSet interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
type AnyMatcher interface {
// Add adds a new Matcher to AnyMatcher.
Add(matcher Matcher)
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}