Xray-core: Refactor geodata (#5814)
https://github.com/XTLS/Xray-core/issues/4422#issuecomment-3533007890 Breaking changes https://github.com/XTLS/Xray-core/pull/5569 Reverts https://github.com/XTLS/Xray-core/pull/5505 Closes https://github.com/XTLS/Xray-core/pull/643
This commit is contained in:
73 files changed
+5432
-4455
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func BenchmarkLinearIndexMatcher(b *testing.B) {
|
||||
benchmarkIndexMatcher(b, func() IndexMatcher {
|
||||
return NewLinearIndexMatcher()
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkMphIndexMatcher(b *testing.B) {
|
||||
benchmarkIndexMatcher(b, func() IndexMatcher {
|
||||
return NewMphIndexMatcher()
|
||||
})
|
||||
}
|
||||
|
||||
func benchmarkIndexMatcher(b *testing.B, ctor func() IndexMatcher) {
|
||||
b.Run("Match", func(b *testing.B) {
|
||||
b.Run("Domain------------", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true})
|
||||
})
|
||||
b.Run("Domain+Full-------", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true})
|
||||
})
|
||||
b.Run("Domain+Full+Substr", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true})
|
||||
})
|
||||
b.Run("All-Fail----------", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false})
|
||||
})
|
||||
})
|
||||
b.Run("Match/Dotless", func(b *testing.B) { // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used.
|
||||
b.Run("All-Succ", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true, Regex: true})
|
||||
})
|
||||
b.Run("All-Fail", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false, Regex: false})
|
||||
})
|
||||
})
|
||||
b.Run("MatchAny", func(b *testing.B) {
|
||||
b.Run("First-Full--", func(b *testing.B) {
|
||||
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: true, Domain: true, Substr: true})
|
||||
})
|
||||
b.Run("First-Domain", func(b *testing.B) {
|
||||
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: true, Substr: true})
|
||||
})
|
||||
b.Run("First-Substr", func(b *testing.B) {
|
||||
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: true})
|
||||
})
|
||||
b.Run("All-Fail----", func(b *testing.B) {
|
||||
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: false})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func BenchmarkFullMatcher(b *testing.B) {
|
||||
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Full, func() MatcherGroup {
|
||||
return new(SimpleMatcherGroup)
|
||||
})
|
||||
})
|
||||
b.Run("FullMatcherGroup--------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Full, func() MatcherGroup {
|
||||
return NewFullMatcherGroup()
|
||||
})
|
||||
})
|
||||
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Full, func() MatcherGroup {
|
||||
return NewACAutomatonMatcherGroup()
|
||||
})
|
||||
})
|
||||
b.Run("MphMatcherGroup---------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Full, func() MatcherGroup {
|
||||
return NewMphMatcherGroup()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkDomainMatcher(b *testing.B) {
|
||||
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Domain, func() MatcherGroup {
|
||||
return new(SimpleMatcherGroup)
|
||||
})
|
||||
})
|
||||
b.Run("DomainMatcherGroup------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Domain, func() MatcherGroup {
|
||||
return NewDomainMatcherGroup()
|
||||
})
|
||||
})
|
||||
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Domain, func() MatcherGroup {
|
||||
return NewACAutomatonMatcherGroup()
|
||||
})
|
||||
})
|
||||
b.Run("MphMatcherGroup---------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Domain, func() MatcherGroup {
|
||||
return NewMphMatcherGroup()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSubstrMatcher(b *testing.B) {
|
||||
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Substr, func() MatcherGroup {
|
||||
return new(SimpleMatcherGroup)
|
||||
})
|
||||
})
|
||||
b.Run("SubstrMatcherGroup------", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Substr, func() MatcherGroup {
|
||||
return new(SubstrMatcherGroup)
|
||||
})
|
||||
})
|
||||
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
|
||||
benchmarkMatcherType(b, Substr, func() MatcherGroup {
|
||||
return NewACAutomatonMatcherGroup()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Utility functions for benchmark
|
||||
|
||||
func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) {
|
||||
b.Run("Match", func(b *testing.B) {
|
||||
b.Run("Succ", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{t: true})
|
||||
})
|
||||
b.Run("Fail", func(b *testing.B) {
|
||||
benchmarkMatch(b, ctor(), map[Type]bool{t: false})
|
||||
})
|
||||
})
|
||||
b.Run("MatchAny", func(b *testing.B) {
|
||||
b.Run("Succ", func(b *testing.B) {
|
||||
benchmarkMatchAny(b, ctor(), map[Type]bool{t: true})
|
||||
})
|
||||
b.Run("Fail", func(b *testing.B) {
|
||||
benchmarkMatchAny(b, ctor(), map[Type]bool{t: false})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func benchmarkMatch(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) {
|
||||
prepareMatchers(g, enabledTypes)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = g.Match("0.example.com")
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkMatchAny(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) {
|
||||
prepareMatchers(g, enabledTypes)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = g.MatchAny("0.example.com")
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMatchers(g MatcherGroup, enabledTypes map[Type]bool) {
|
||||
for matcherType, hasMatch := range enabledTypes {
|
||||
switch matcherType {
|
||||
case Domain:
|
||||
if hasMatch {
|
||||
AddMatcherToGroup(g, DomainMatcher("example.com"), 0)
|
||||
}
|
||||
for i := 1; i < 1024; i++ {
|
||||
AddMatcherToGroup(g, DomainMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
|
||||
}
|
||||
case Full:
|
||||
if hasMatch {
|
||||
AddMatcherToGroup(g, FullMatcher("0.example.com"), 0)
|
||||
}
|
||||
for i := 1; i < 64; i++ {
|
||||
AddMatcherToGroup(g, FullMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
|
||||
}
|
||||
case Substr:
|
||||
if hasMatch {
|
||||
AddMatcherToGroup(g, SubstrMatcher("example.com"), 0)
|
||||
}
|
||||
for i := 1; i < 4; i++ {
|
||||
AddMatcherToGroup(g, SubstrMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
|
||||
}
|
||||
case Regex:
|
||||
matcher, err := Regex.New("^[^.]*$") // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used.
|
||||
common.Must(err)
|
||||
AddMatcherToGroup(g, matcher, 0)
|
||||
}
|
||||
}
|
||||
if g, ok := g.(buildable); ok {
|
||||
common.Must(g.Build())
|
||||
}
|
||||
}
|
||||
|
||||
type buildable interface {
|
||||
Build() error
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package strmatcher
|
||||
|
||||
// LinearIndexMatcher is an implementation of IndexMatcher.
|
||||
type LinearIndexMatcher struct {
|
||||
count uint32
|
||||
full *FullMatcherGroup
|
||||
domain *DomainMatcherGroup
|
||||
substr *SubstrMatcherGroup
|
||||
regex *SimpleMatcherGroup
|
||||
}
|
||||
|
||||
func NewLinearIndexMatcher() *LinearIndexMatcher {
|
||||
return new(LinearIndexMatcher)
|
||||
}
|
||||
|
||||
// Add implements IndexMatcher.Add.
|
||||
func (g *LinearIndexMatcher) Add(matcher Matcher) uint32 {
|
||||
g.count++
|
||||
index := g.count
|
||||
|
||||
switch matcher := matcher.(type) {
|
||||
case FullMatcher:
|
||||
if g.full == nil {
|
||||
g.full = NewFullMatcherGroup()
|
||||
}
|
||||
g.full.AddFullMatcher(matcher, index)
|
||||
case DomainMatcher:
|
||||
if g.domain == nil {
|
||||
g.domain = NewDomainMatcherGroup()
|
||||
}
|
||||
g.domain.AddDomainMatcher(matcher, index)
|
||||
case SubstrMatcher:
|
||||
if g.substr == nil {
|
||||
g.substr = new(SubstrMatcherGroup)
|
||||
}
|
||||
g.substr.AddSubstrMatcher(matcher, index)
|
||||
default:
|
||||
if g.regex == nil {
|
||||
g.regex = new(SimpleMatcherGroup)
|
||||
}
|
||||
g.regex.AddMatcher(matcher, index)
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
// Build implements IndexMatcher.Build.
|
||||
func (*LinearIndexMatcher) Build() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match implements IndexMatcher.Match.
|
||||
func (g *LinearIndexMatcher) Match(input string) []uint32 {
|
||||
// Allocate capacity to prevent matches escaping to heap
|
||||
result := make([][]uint32, 0, 5)
|
||||
if g.full != nil {
|
||||
if matches := g.full.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.domain != nil {
|
||||
if matches := g.domain.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.substr != nil {
|
||||
if matches := g.substr.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.regex != nil {
|
||||
if matches := g.regex.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
return CompositeMatches(result)
|
||||
}
|
||||
|
||||
// MatchAny implements IndexMatcher.MatchAny.
|
||||
func (g *LinearIndexMatcher) MatchAny(input string) bool {
|
||||
if g.full != nil && g.full.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
if g.domain != nil && g.domain.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
if g.substr != nil && g.substr.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
return g.regex != nil && g.regex.MatchAny(input)
|
||||
}
|
||||
|
||||
// Size implements IndexMatcher.Size.
|
||||
func (g *LinearIndexMatcher) Size() uint32 {
|
||||
return g.count
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
|
||||
func TestLinearIndexMatcher(t *testing.T) {
|
||||
rules := []struct {
|
||||
Type Type
|
||||
Domain string
|
||||
}{
|
||||
{
|
||||
Type: Regex,
|
||||
Domain: "apis\\.us$",
|
||||
},
|
||||
{
|
||||
Type: Substr,
|
||||
Domain: "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{
|
||||
Type: Substr,
|
||||
Domain: "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "fonts.googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "example.com",
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output []uint32
|
||||
}{
|
||||
{
|
||||
Input: "www.baidu.com",
|
||||
Output: []uint32{5, 9, 4},
|
||||
},
|
||||
{
|
||||
Input: "fonts.googleapis.com",
|
||||
Output: []uint32{8, 3, 7, 4, 2, 6},
|
||||
},
|
||||
{
|
||||
Input: "example.googleapis.com",
|
||||
Output: []uint32{3, 7, 4, 2, 6},
|
||||
},
|
||||
{
|
||||
Input: "testapis.us",
|
||||
Output: []uint32{2, 6, 1},
|
||||
},
|
||||
{
|
||||
Input: "example.com",
|
||||
Output: []uint32{10, 4},
|
||||
},
|
||||
}
|
||||
matcherGroup := NewLinearIndexMatcher()
|
||||
for _, rule := range rules {
|
||||
matcher, err := rule.Type.New(rule.Domain)
|
||||
common.Must(err)
|
||||
matcherGroup.Add(matcher)
|
||||
}
|
||||
matcherGroup.Build()
|
||||
for _, test := range cases {
|
||||
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package strmatcher
|
||||
|
||||
import "runtime"
|
||||
|
||||
// A MphIndexMatcher is divided into three parts:
|
||||
// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table;
|
||||
// 2. `substr` patterns are matched by ac automaton;
|
||||
// 3. `regex` patterns are matched with the regex library.
|
||||
type MphIndexMatcher struct {
|
||||
count uint32
|
||||
mph *MphMatcherGroup
|
||||
ac *ACAutomatonMatcherGroup
|
||||
regex *SimpleMatcherGroup
|
||||
}
|
||||
|
||||
func NewMphIndexMatcher() *MphIndexMatcher {
|
||||
return new(MphIndexMatcher)
|
||||
}
|
||||
|
||||
// Add implements IndexMatcher.Add.
|
||||
func (g *MphIndexMatcher) Add(matcher Matcher) uint32 {
|
||||
g.count++
|
||||
index := g.count
|
||||
|
||||
switch matcher := matcher.(type) {
|
||||
case FullMatcher:
|
||||
if g.mph == nil {
|
||||
g.mph = NewMphMatcherGroup()
|
||||
}
|
||||
g.mph.AddFullMatcher(matcher, index)
|
||||
case DomainMatcher:
|
||||
if g.mph == nil {
|
||||
g.mph = NewMphMatcherGroup()
|
||||
}
|
||||
g.mph.AddDomainMatcher(matcher, index)
|
||||
case SubstrMatcher:
|
||||
if g.ac == nil {
|
||||
g.ac = NewACAutomatonMatcherGroup()
|
||||
}
|
||||
g.ac.AddSubstrMatcher(matcher, index)
|
||||
case *RegexMatcher:
|
||||
if g.regex == nil {
|
||||
g.regex = &SimpleMatcherGroup{}
|
||||
}
|
||||
g.regex.AddMatcher(matcher, index)
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
// Build implements IndexMatcher.Build.
|
||||
func (g *MphIndexMatcher) Build() error {
|
||||
if g.mph != nil {
|
||||
runtime.GC() // peak mem
|
||||
g.mph.Build()
|
||||
}
|
||||
runtime.GC() // peak mem
|
||||
if g.ac != nil {
|
||||
g.ac.Build()
|
||||
runtime.GC() // peak mem
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match implements IndexMatcher.Match.
|
||||
func (g *MphIndexMatcher) Match(input string) []uint32 {
|
||||
result := make([][]uint32, 0, 5)
|
||||
if g.mph != nil {
|
||||
if matches := g.mph.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.ac != nil {
|
||||
if matches := g.ac.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.regex != nil {
|
||||
if matches := g.regex.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
return CompositeMatches(result)
|
||||
}
|
||||
|
||||
// MatchAny implements IndexMatcher.MatchAny.
|
||||
func (g *MphIndexMatcher) MatchAny(input string) bool {
|
||||
if g.mph != nil && g.mph.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
if g.ac != nil && g.ac.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
return g.regex != nil && g.regex.MatchAny(input)
|
||||
}
|
||||
|
||||
// Size implements IndexMatcher.Size.
|
||||
func (g *MphIndexMatcher) Size() uint32 {
|
||||
return g.count
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestMphIndexMatcher(t *testing.T) {
|
||||
rules := []struct {
|
||||
Type Type
|
||||
Domain string
|
||||
}{
|
||||
{
|
||||
Type: Regex,
|
||||
Domain: "apis\\.us$",
|
||||
},
|
||||
{
|
||||
Type: Substr,
|
||||
Domain: "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{
|
||||
Type: Substr,
|
||||
Domain: "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "fonts.googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "example.com",
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output []uint32
|
||||
}{
|
||||
{
|
||||
Input: "www.baidu.com",
|
||||
Output: []uint32{5, 9, 4},
|
||||
},
|
||||
{
|
||||
Input: "fonts.googleapis.com",
|
||||
Output: []uint32{8, 3, 7, 4, 2, 6},
|
||||
},
|
||||
{
|
||||
Input: "example.googleapis.com",
|
||||
Output: []uint32{3, 7, 4, 2, 6},
|
||||
},
|
||||
{
|
||||
Input: "testapis.us",
|
||||
Output: []uint32{2, 6, 1},
|
||||
},
|
||||
{
|
||||
Input: "example.com",
|
||||
Output: []uint32{10, 4},
|
||||
},
|
||||
}
|
||||
matcherGroup := NewMphIndexMatcher()
|
||||
for _, rule := range rules {
|
||||
matcher, err := rule.Type.New(rule.Domain)
|
||||
common.Must(err)
|
||||
matcherGroup.Add(matcher)
|
||||
}
|
||||
matcherGroup.Build()
|
||||
for _, test := range cases {
|
||||
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package strmatcher
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
)
|
||||
|
||||
const (
|
||||
acValidCharCount = 39 // aA-zZ (26), 0-9 (10), - (1), . (1), invalid(1)
|
||||
acMatchTypeCount = 3 // Full, Domain and Substr
|
||||
)
|
||||
|
||||
type acEdge byte
|
||||
|
||||
const (
|
||||
acTrieEdge acEdge = 1
|
||||
acFailEdge acEdge = 0
|
||||
)
|
||||
|
||||
type acNode struct {
|
||||
next [acValidCharCount]uint32 // EdgeIdx -> Next NodeIdx (Next trie node or fail node)
|
||||
edge [acValidCharCount]acEdge // EdgeIdx -> Trie Edge / Fail Edge
|
||||
fail uint32 // NodeIdx of *next matched* Substr Pattern on its fail path
|
||||
match uint32 // MatchIdx of matchers registered on this node, 0 indicates no match
|
||||
} // Sizeof acNode: (4+1)*acValidCharCount + <padding> + 4 + 4
|
||||
|
||||
type acValue [acMatchTypeCount][]uint32 // MatcherType -> Registered Matcher Values
|
||||
|
||||
// ACAutoMationMatcherGroup is an implementation of MatcherGroup.
|
||||
// It uses an AC Automata to provide support for Full, Domain and Substr matcher. Trie node is char based.
|
||||
//
|
||||
// NOTICE: ACAutomatonMatcherGroup currently uses a restricted charset (LDH Subset),
|
||||
// upstream should manually in a way to ensure all patterns and inputs passed to it to be in this charset.
|
||||
type ACAutomatonMatcherGroup struct {
|
||||
nodes []acNode // NodeIdx -> acNode
|
||||
values []acValue // MatchIdx -> acValue
|
||||
}
|
||||
|
||||
func NewACAutomatonMatcherGroup() *ACAutomatonMatcherGroup {
|
||||
ac := new(ACAutomatonMatcherGroup)
|
||||
ac.addNode() // Create root node (NodeIdx 0)
|
||||
ac.addMatchEntry() // Create sentinel match entry (MatchIdx 0)
|
||||
return ac
|
||||
}
|
||||
|
||||
// AddFullMatcher implements MatcherGroupForFull.AddFullMatcher.
|
||||
func (ac *ACAutomatonMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
|
||||
ac.addPattern(0, matcher.Pattern(), matcher.Type(), value)
|
||||
}
|
||||
|
||||
// AddDomainMatcher implements MatcherGroupForDomain.AddDomainMatcher.
|
||||
func (ac *ACAutomatonMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
|
||||
node := ac.addPattern(0, matcher.Pattern(), matcher.Type(), value) // For full domain match
|
||||
ac.addPattern(node, ".", matcher.Type(), value) // For partial domain match
|
||||
}
|
||||
|
||||
// AddSubstrMatcher implements MatcherGroupForSubstr.AddSubstrMatcher.
|
||||
func (ac *ACAutomatonMatcherGroup) AddSubstrMatcher(matcher SubstrMatcher, value uint32) {
|
||||
ac.addPattern(0, matcher.Pattern(), matcher.Type(), value)
|
||||
}
|
||||
|
||||
func (ac *ACAutomatonMatcherGroup) addPattern(nodeIdx uint32, pattern string, matcherType Type, value uint32) uint32 {
|
||||
node := &ac.nodes[nodeIdx]
|
||||
for i := len(pattern) - 1; i >= 0; i-- {
|
||||
edgeIdx := acCharset[pattern[i]]
|
||||
nextIdx := node.next[edgeIdx]
|
||||
if nextIdx == 0 { // Add new Trie Edge
|
||||
nextIdx = ac.addNode()
|
||||
ac.nodes[nodeIdx].next[edgeIdx] = nextIdx
|
||||
ac.nodes[nodeIdx].edge[edgeIdx] = acTrieEdge
|
||||
}
|
||||
nodeIdx = nextIdx
|
||||
node = &ac.nodes[nodeIdx]
|
||||
}
|
||||
if node.match == 0 { // Add new match entry
|
||||
node.match = ac.addMatchEntry()
|
||||
}
|
||||
ac.values[node.match][matcherType] = append(ac.values[node.match][matcherType], value)
|
||||
return nodeIdx
|
||||
}
|
||||
|
||||
func (ac *ACAutomatonMatcherGroup) addNode() uint32 {
|
||||
ac.nodes = append(ac.nodes, acNode{})
|
||||
return uint32(len(ac.nodes) - 1)
|
||||
}
|
||||
|
||||
func (ac *ACAutomatonMatcherGroup) addMatchEntry() uint32 {
|
||||
ac.values = append(ac.values, acValue{})
|
||||
return uint32(len(ac.values) - 1)
|
||||
}
|
||||
|
||||
func (ac *ACAutomatonMatcherGroup) Build() error {
|
||||
fail := make([]uint32, len(ac.nodes))
|
||||
queue := list.New()
|
||||
for edgeIdx := 0; edgeIdx < acValidCharCount; edgeIdx++ {
|
||||
if nextIdx := ac.nodes[0].next[edgeIdx]; nextIdx != 0 {
|
||||
queue.PushBack(nextIdx)
|
||||
}
|
||||
}
|
||||
for {
|
||||
front := queue.Front()
|
||||
if front == nil {
|
||||
break
|
||||
}
|
||||
queue.Remove(front)
|
||||
nodeIdx := front.Value.(uint32)
|
||||
node := &ac.nodes[nodeIdx] // Current node
|
||||
failNode := &ac.nodes[fail[nodeIdx]] // Fail node of currrent node
|
||||
for edgeIdx := 0; edgeIdx < acValidCharCount; edgeIdx++ {
|
||||
nodeIdx := node.next[edgeIdx] // Next node through trie edge
|
||||
failIdx := failNode.next[edgeIdx] // Next node through fail edge
|
||||
if nodeIdx != 0 {
|
||||
queue.PushBack(nodeIdx)
|
||||
fail[nodeIdx] = failIdx
|
||||
if match := ac.nodes[failIdx].match; match != 0 && len(ac.values[match][Substr]) > 0 { // Fail node is a Substr match node
|
||||
ac.nodes[nodeIdx].fail = failIdx
|
||||
} else { // Use path compression to reduce fail path to only contain match nodes
|
||||
ac.nodes[nodeIdx].fail = ac.nodes[failIdx].fail
|
||||
}
|
||||
} else { // Add new fail edge
|
||||
node.next[edgeIdx] = failIdx
|
||||
node.edge[edgeIdx] = acFailEdge
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match implements MatcherGroup.Match.
|
||||
func (ac *ACAutomatonMatcherGroup) Match(input string) []uint32 {
|
||||
suffixMatches := make([][]uint32, 0, 5)
|
||||
substrMatches := make([][]uint32, 0, 5)
|
||||
fullMatch := true // fullMatch indicates no fail edge traversed so far.
|
||||
node := &ac.nodes[0] // start from root node.
|
||||
// 1. the match string is all through trie edge. FULL MATCH or DOMAIN
|
||||
// 2. the match string is through a fail edge. NOT FULL MATCH
|
||||
// 2.1 Through a fail edge, but there exists a valid node. SUBSTR
|
||||
for i := len(input) - 1; i >= 0; i-- {
|
||||
edge := acCharset[input[i]]
|
||||
fullMatch = fullMatch && (node.edge[edge] == acTrieEdge)
|
||||
node = &ac.nodes[node.next[edge]] // Advance to next node
|
||||
// When entering a new node, traverse the fail path to find all possible Substr patterns:
|
||||
// 1. The fail path is compressed to only contains match nodes and root node (for terminate condition).
|
||||
// 2. node.fail != 0 is added here for better performance (as shown by benchmark), possibly it helps branch prediction.
|
||||
if node.fail != 0 {
|
||||
for failIdx, failNode := node.fail, &ac.nodes[node.fail]; failIdx != 0; failIdx, failNode = failNode.fail, &ac.nodes[failIdx] {
|
||||
substrMatches = append(substrMatches, ac.values[failNode.match][Substr])
|
||||
}
|
||||
}
|
||||
// When entering a new node, check whether this node is a match.
|
||||
// For Substr matchers:
|
||||
// 1. Matched in any situation, whether a failNode edge is traversed or not.
|
||||
// For Domain matchers:
|
||||
// 1. Should not traverse any fail edge (fullMatch).
|
||||
// 2. Only check on dot separator (input[i] == '.').
|
||||
if node.match != 0 {
|
||||
values := ac.values[node.match]
|
||||
if len(values[Substr]) > 0 {
|
||||
substrMatches = append(substrMatches, values[Substr])
|
||||
}
|
||||
if fullMatch && input[i] == '.' && len(values[Domain]) > 0 {
|
||||
suffixMatches = append(suffixMatches, values[Domain])
|
||||
}
|
||||
}
|
||||
}
|
||||
// At the end of input, check if the whole string matches a pattern.
|
||||
// For Domain matchers:
|
||||
// 1. Exact match on Domain Matcher works like Full Match. e.g. foo.com is a full match for domain:foo.com.
|
||||
// For Full matchers:
|
||||
// 1. Only when no fail edge is traversed (fullMatch).
|
||||
// 2. Takes the highest priority (added at last).
|
||||
if fullMatch && node.match != 0 {
|
||||
values := ac.values[node.match]
|
||||
if len(values[Domain]) > 0 {
|
||||
suffixMatches = append(suffixMatches, values[Domain])
|
||||
}
|
||||
if len(values[Full]) > 0 {
|
||||
suffixMatches = append(suffixMatches, values[Full])
|
||||
}
|
||||
}
|
||||
if len(substrMatches) == 0 {
|
||||
return CompositeMatchesReverse(suffixMatches)
|
||||
}
|
||||
return CompositeMatchesReverse(append(substrMatches, suffixMatches...))
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherGroup.MatchAny.
|
||||
func (ac *ACAutomatonMatcherGroup) MatchAny(input string) bool {
|
||||
fullMatch := true
|
||||
node := &ac.nodes[0]
|
||||
for i := len(input) - 1; i >= 0; i-- {
|
||||
edge := acCharset[input[i]]
|
||||
fullMatch = fullMatch && (node.edge[edge] == acTrieEdge)
|
||||
node = &ac.nodes[node.next[edge]]
|
||||
if node.fail != 0 { // There is a match on this node's fail path
|
||||
return true
|
||||
}
|
||||
if node.match != 0 { // There is a match on this node
|
||||
values := ac.values[node.match]
|
||||
if len(values[Substr]) > 0 { // Substr match succeeds unconditionally
|
||||
return true
|
||||
}
|
||||
if fullMatch && input[i] == '.' && len(values[Domain]) > 0 { // Domain match only succeeds with dot separator on trie path
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return fullMatch && node.match != 0 // At the end of input, Domain and Full match will succeed if no fail edge is traversed
|
||||
}
|
||||
|
||||
// Letter-Digit-Hyphen (LDH) subset (https://tools.ietf.org/html/rfc952):
|
||||
// - Letters A to Z (no distinction is made between uppercase and lowercase)
|
||||
// - Digits 0 to 9
|
||||
// - Hyphens(-) and Periods(.)
|
||||
//
|
||||
// If for future the strmatcher are used for other scenarios than domain,
|
||||
// we could add a new Charset interface to represent variable charsets.
|
||||
var acCharset = [256]int{
|
||||
'A': 1,
|
||||
'a': 1,
|
||||
'B': 2,
|
||||
'b': 2,
|
||||
'C': 3,
|
||||
'c': 3,
|
||||
'D': 4,
|
||||
'd': 4,
|
||||
'E': 5,
|
||||
'e': 5,
|
||||
'F': 6,
|
||||
'f': 6,
|
||||
'G': 7,
|
||||
'g': 7,
|
||||
'H': 8,
|
||||
'h': 8,
|
||||
'I': 9,
|
||||
'i': 9,
|
||||
'J': 10,
|
||||
'j': 10,
|
||||
'K': 11,
|
||||
'k': 11,
|
||||
'L': 12,
|
||||
'l': 12,
|
||||
'M': 13,
|
||||
'm': 13,
|
||||
'N': 14,
|
||||
'n': 14,
|
||||
'O': 15,
|
||||
'o': 15,
|
||||
'P': 16,
|
||||
'p': 16,
|
||||
'Q': 17,
|
||||
'q': 17,
|
||||
'R': 18,
|
||||
'r': 18,
|
||||
'S': 19,
|
||||
's': 19,
|
||||
'T': 20,
|
||||
't': 20,
|
||||
'U': 21,
|
||||
'u': 21,
|
||||
'V': 22,
|
||||
'v': 22,
|
||||
'W': 23,
|
||||
'w': 23,
|
||||
'X': 24,
|
||||
'x': 24,
|
||||
'Y': 25,
|
||||
'y': 25,
|
||||
'Z': 26,
|
||||
'z': 26,
|
||||
'-': 27,
|
||||
'.': 28,
|
||||
'0': 29,
|
||||
'1': 30,
|
||||
'2': 31,
|
||||
'3': 32,
|
||||
'4': 33,
|
||||
'5': 34,
|
||||
'6': 35,
|
||||
'7': 36,
|
||||
'8': 37,
|
||||
'9': 38,
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestACAutomatonMatcherGroup(t *testing.T) {
|
||||
cases1 := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
input string
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "www.example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "www.e3ample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "xample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "xexample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
input: "xexample.com",
|
||||
output: false,
|
||||
},
|
||||
}
|
||||
for _, test := range cases1 {
|
||||
ac := NewACAutomatonMatcherGroup()
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(ac, matcher, 0))
|
||||
ac.Build()
|
||||
if m := ac.MatchAny(test.input); m != test.output {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
{
|
||||
cases2Input := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "163.com",
|
||||
mType: Domain,
|
||||
},
|
||||
{
|
||||
pattern: "m.126.com",
|
||||
mType: Full,
|
||||
},
|
||||
{
|
||||
pattern: "3.com",
|
||||
mType: Full,
|
||||
},
|
||||
{
|
||||
pattern: "google.com",
|
||||
mType: Substr,
|
||||
},
|
||||
{
|
||||
pattern: "vgoogle.com",
|
||||
mType: Substr,
|
||||
},
|
||||
}
|
||||
ac := NewACAutomatonMatcherGroup()
|
||||
for _, test := range cases2Input {
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(ac, matcher, 0))
|
||||
}
|
||||
ac.Build()
|
||||
cases2Output := []struct {
|
||||
pattern string
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
pattern: "126.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "m.163.com",
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
pattern: "mm163.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "m.126.com",
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
pattern: "163.com",
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
pattern: "63.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "oogle.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "vvgoogle.com",
|
||||
res: true,
|
||||
},
|
||||
}
|
||||
for _, test := range cases2Output {
|
||||
if m := ac.MatchAny(test.pattern); m != test.res {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cases3Input := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "video.google.com",
|
||||
mType: Domain,
|
||||
},
|
||||
{
|
||||
pattern: "gle.com",
|
||||
mType: Domain,
|
||||
},
|
||||
}
|
||||
ac := NewACAutomatonMatcherGroup()
|
||||
for _, test := range cases3Input {
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(ac, matcher, 0))
|
||||
}
|
||||
ac.Build()
|
||||
cases3Output := []struct {
|
||||
pattern string
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
pattern: "google.com",
|
||||
res: false,
|
||||
},
|
||||
}
|
||||
for _, test := range cases3Output {
|
||||
if m := ac.MatchAny(test.pattern); m != test.res {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
cases4Input := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "apis",
|
||||
mType: Substr,
|
||||
},
|
||||
{
|
||||
pattern: "googleapis.com",
|
||||
mType: Domain,
|
||||
},
|
||||
}
|
||||
ac := NewACAutomatonMatcherGroup()
|
||||
for _, test := range cases4Input {
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(ac, matcher, 0))
|
||||
}
|
||||
ac.Build()
|
||||
cases4Output := []struct {
|
||||
pattern string
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
pattern: "gapis.com",
|
||||
res: true,
|
||||
},
|
||||
}
|
||||
for _, test := range cases4Output {
|
||||
if m := ac.MatchAny(test.pattern); m != test.res {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestACAutomatonMatcherGroupSubstr(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 []uint32
|
||||
}{
|
||||
{
|
||||
input: "google.com",
|
||||
output: []uint32{1},
|
||||
},
|
||||
{
|
||||
input: "apis.com",
|
||||
output: []uint32{0, 2},
|
||||
},
|
||||
{
|
||||
input: "googleapis.com",
|
||||
output: []uint32{1, 0, 2},
|
||||
},
|
||||
{
|
||||
input: "fonts.googleapis.com",
|
||||
output: []uint32{1, 0, 2},
|
||||
},
|
||||
{
|
||||
input: "apis.googleapis.com",
|
||||
output: []uint32{0, 2, 1, 0, 2},
|
||||
},
|
||||
}
|
||||
matcherGroup := NewACAutomatonMatcherGroup()
|
||||
for id, entry := range patterns {
|
||||
matcher, err := entry.mType.New(entry.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(id)))
|
||||
}
|
||||
matcherGroup.Build()
|
||||
for _, test := range cases {
|
||||
if r := matcherGroup.Match(test.input); !reflect.DeepEqual(r, test.output) {
|
||||
t.Error("unexpected output: ", r, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
|
||||
func TestACAutomatonMatcherGroupAsIndexMatcher(t *testing.T) {
|
||||
rules := []struct {
|
||||
Type Type
|
||||
Domain string
|
||||
}{
|
||||
// Regex not supported by ACAutomationMatcherGroup
|
||||
// {
|
||||
// Type: Regex,
|
||||
// Domain: "apis\\.us$",
|
||||
// },
|
||||
{
|
||||
Type: Substr,
|
||||
Domain: "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{
|
||||
Type: Substr,
|
||||
Domain: "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "fonts.googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "example.com",
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output []uint32
|
||||
}{
|
||||
{
|
||||
Input: "www.baidu.com",
|
||||
Output: []uint32{5, 9, 4},
|
||||
},
|
||||
{
|
||||
Input: "fonts.googleapis.com",
|
||||
Output: []uint32{8, 3, 7, 4, 2, 6},
|
||||
},
|
||||
{
|
||||
Input: "example.googleapis.com",
|
||||
Output: []uint32{3, 7, 4, 2, 6},
|
||||
},
|
||||
{
|
||||
Input: "testapis.us",
|
||||
Output: []uint32{2, 6 /*, 1*/},
|
||||
},
|
||||
{
|
||||
Input: "example.com",
|
||||
Output: []uint32{10, 4},
|
||||
},
|
||||
}
|
||||
matcherGroup := NewACAutomatonMatcherGroup()
|
||||
for i, rule := range rules {
|
||||
matcher, err := rule.Type.New(rule.Domain)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(i+2)))
|
||||
}
|
||||
matcherGroup.Build()
|
||||
for _, test := range cases {
|
||||
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package strmatcher
|
||||
|
||||
type trieNode struct {
|
||||
values []uint32
|
||||
children map[string]*trieNode
|
||||
}
|
||||
|
||||
// DomainMatcherGroup is an implementation of MatcherGroup.
|
||||
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
|
||||
type DomainMatcherGroup struct {
|
||||
root *trieNode
|
||||
}
|
||||
|
||||
func NewDomainMatcherGroup() *DomainMatcherGroup {
|
||||
return &DomainMatcherGroup{
|
||||
root: new(trieNode),
|
||||
}
|
||||
}
|
||||
|
||||
// AddDomainMatcher implements MatcherGroupForDomain.AddDomainMatcher.
|
||||
func (g *DomainMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
|
||||
node := g.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]*trieNode)
|
||||
}
|
||||
next := node.children[part]
|
||||
if next == nil {
|
||||
next = new(trieNode)
|
||||
node.children[part] = next
|
||||
}
|
||||
node = next
|
||||
}
|
||||
|
||||
node.values = append(node.values, value)
|
||||
}
|
||||
|
||||
// Match implements MatcherGroup.Match.
|
||||
func (g *DomainMatcherGroup) Match(input string) []uint32 {
|
||||
matches := make([][]uint32, 0, 5)
|
||||
node := g.root
|
||||
for i := len(input); i > 0; {
|
||||
for j := i - 1; ; j-- {
|
||||
if input[j] == '.' { // Domain label found
|
||||
node = node.children[input[j+1:i]]
|
||||
i = j
|
||||
break
|
||||
}
|
||||
if j == 0 { // The last part of domain label
|
||||
node = node.children[input[j:i]]
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if node == nil { // No more match if no trie edge transition
|
||||
break
|
||||
}
|
||||
if len(node.values) > 0 { // Found matched matchers
|
||||
matches = append(matches, node.values)
|
||||
}
|
||||
if node.children == nil { // No more match if leaf node reached
|
||||
break
|
||||
}
|
||||
}
|
||||
return CompositeMatchesReverse(matches)
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherGroup.MatchAny.
|
||||
func (g *DomainMatcherGroup) MatchAny(input string) bool {
|
||||
node := g.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 len(node.values) > 0 {
|
||||
return true
|
||||
}
|
||||
if node.children == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestDomainMatcherGroup(t *testing.T) {
|
||||
patterns := []struct {
|
||||
Pattern string
|
||||
Value uint32
|
||||
}{
|
||||
{
|
||||
Pattern: "example.com",
|
||||
Value: 1,
|
||||
},
|
||||
{
|
||||
Pattern: "google.com",
|
||||
Value: 2,
|
||||
},
|
||||
{
|
||||
Pattern: "x.a.com",
|
||||
Value: 3,
|
||||
},
|
||||
{
|
||||
Pattern: "a.b.com",
|
||||
Value: 4,
|
||||
},
|
||||
{
|
||||
Pattern: "c.a.b.com",
|
||||
Value: 5,
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
Value: 4,
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
Value: 6,
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
Domain string
|
||||
Result []uint32
|
||||
}{
|
||||
{
|
||||
Domain: "x.example.com",
|
||||
Result: []uint32{1},
|
||||
},
|
||||
{
|
||||
Domain: "y.com",
|
||||
Result: nil,
|
||||
},
|
||||
{
|
||||
Domain: "a.b.com",
|
||||
Result: []uint32{4},
|
||||
},
|
||||
{ // Matches [c.a.b.com, a.b.com]
|
||||
Domain: "c.a.b.com",
|
||||
Result: []uint32{5, 4},
|
||||
},
|
||||
{
|
||||
Domain: "c.a..b.com",
|
||||
Result: nil,
|
||||
},
|
||||
{
|
||||
Domain: ".com",
|
||||
Result: nil,
|
||||
},
|
||||
{
|
||||
Domain: "com",
|
||||
Result: nil,
|
||||
},
|
||||
{
|
||||
Domain: "",
|
||||
Result: nil,
|
||||
},
|
||||
{
|
||||
Domain: "x.y.com",
|
||||
Result: []uint32{4, 6},
|
||||
},
|
||||
}
|
||||
g := NewDomainMatcherGroup()
|
||||
for _, pattern := range patterns {
|
||||
AddMatcherToGroup(g, DomainMatcher(pattern.Pattern), pattern.Value)
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
r := g.Match(testCase.Domain)
|
||||
if !reflect.DeepEqual(r, testCase.Result) {
|
||||
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyDomainMatcherGroup(t *testing.T) {
|
||||
g := NewDomainMatcherGroup()
|
||||
r := g.Match("example.com")
|
||||
if len(r) != 0 {
|
||||
t.Error("Expect [], but ", r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package strmatcher
|
||||
|
||||
// FullMatcherGroup is an implementation of MatcherGroup.
|
||||
// It uses a hash table to facilitate exact match lookup.
|
||||
type FullMatcherGroup struct {
|
||||
matchers map[string][]uint32
|
||||
}
|
||||
|
||||
func NewFullMatcherGroup() *FullMatcherGroup {
|
||||
return &FullMatcherGroup{
|
||||
matchers: make(map[string][]uint32),
|
||||
}
|
||||
}
|
||||
|
||||
// AddFullMatcher implements MatcherGroupForFull.AddFullMatcher.
|
||||
func (g *FullMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
|
||||
domain := matcher.Pattern()
|
||||
g.matchers[domain] = append(g.matchers[domain], value)
|
||||
}
|
||||
|
||||
// Match implements MatcherGroup.Match.
|
||||
func (g *FullMatcherGroup) Match(input string) []uint32 {
|
||||
return g.matchers[input]
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherGroup.Any.
|
||||
func (g *FullMatcherGroup) MatchAny(input string) bool {
|
||||
_, found := g.matchers[input]
|
||||
return found
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestFullMatcherGroup(t *testing.T) {
|
||||
patterns := []struct {
|
||||
Pattern string
|
||||
Value uint32
|
||||
}{
|
||||
{
|
||||
Pattern: "example.com",
|
||||
Value: 1,
|
||||
},
|
||||
{
|
||||
Pattern: "google.com",
|
||||
Value: 2,
|
||||
},
|
||||
{
|
||||
Pattern: "x.a.com",
|
||||
Value: 3,
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
Value: 4,
|
||||
},
|
||||
{
|
||||
Pattern: "x.y.com",
|
||||
Value: 6,
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
Domain string
|
||||
Result []uint32
|
||||
}{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Result: []uint32{1},
|
||||
},
|
||||
{
|
||||
Domain: "y.com",
|
||||
Result: nil,
|
||||
},
|
||||
{
|
||||
Domain: "x.y.com",
|
||||
Result: []uint32{4, 6},
|
||||
},
|
||||
}
|
||||
g := NewFullMatcherGroup()
|
||||
for _, pattern := range patterns {
|
||||
AddMatcherToGroup(g, FullMatcher(pattern.Pattern), pattern.Value)
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
r := g.Match(testCase.Domain)
|
||||
if !reflect.DeepEqual(r, testCase.Result) {
|
||||
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFullMatcherGroup(t *testing.T) {
|
||||
g := NewFullMatcherGroup()
|
||||
r := g.Match("example.com")
|
||||
if len(r) != 0 {
|
||||
t.Error("Expect [], but ", r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package strmatcher
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// PrimeRK is the prime base used in Rabin-Karp algorithm.
|
||||
const PrimeRK = 16777619
|
||||
|
||||
// RollingHash calculates the rolling murmurHash of given string based on a provided suffix hash.
|
||||
func RollingHash(hash uint32, input string) uint32 {
|
||||
for i := len(input) - 1; i >= 0; i-- {
|
||||
hash = hash*PrimeRK + uint32(input[i])
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// MemHash is the hash function used by go map, it utilizes available hardware instructions(behaves
|
||||
// as aeshash if aes instruction is available).
|
||||
// With different seed, each MemHash<seed> performs as distinct hash functions.
|
||||
func MemHash(seed uint32, input string) uint32 {
|
||||
return uint32(strhash(unsafe.Pointer(&input), uintptr(seed))) // nosemgrep
|
||||
}
|
||||
|
||||
const (
|
||||
mphMatchTypeCount = 2 // Full and Domain
|
||||
)
|
||||
|
||||
type mphRuleInfo struct {
|
||||
rollingHash uint32
|
||||
matchers [mphMatchTypeCount][]uint32
|
||||
}
|
||||
|
||||
// MphMatcherGroup is an implementation of MatcherGroup.
|
||||
// It implements Rabin-Karp algorithm and minimal perfect hash table for Full and Domain matcher.
|
||||
type MphMatcherGroup struct {
|
||||
rules []string // RuleIdx -> pattern string, index 0 reserved for failed lookup
|
||||
values [][]uint32 // RuleIdx -> registered matcher values for the pattern (Full Matcher takes precedence)
|
||||
level0 []uint32 // RollingHash & Mask -> seed for Memhash
|
||||
level0Mask uint32 // Mask restricting RollingHash to 0 ~ len(level0)
|
||||
level1 []uint32 // Memhash<seed> & Mask -> stored index for rules
|
||||
level1Mask uint32 // Mask for restricting Memhash<seed> to 0 ~ len(level1)
|
||||
ruleInfos *map[string]mphRuleInfo
|
||||
}
|
||||
|
||||
func NewMphMatcherGroup() *MphMatcherGroup {
|
||||
return &MphMatcherGroup{
|
||||
rules: []string{""},
|
||||
values: [][]uint32{nil},
|
||||
level0: nil,
|
||||
level0Mask: 0,
|
||||
level1: nil,
|
||||
level1Mask: 0,
|
||||
ruleInfos: &map[string]mphRuleInfo{}, // Only used for building, destroyed after build complete
|
||||
}
|
||||
}
|
||||
|
||||
// AddFullMatcher implements MatcherGroupForFull.
|
||||
func (g *MphMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
|
||||
pattern := strings.ToLower(matcher.Pattern())
|
||||
g.addPattern(0, "", pattern, matcher.Type(), value)
|
||||
}
|
||||
|
||||
// AddDomainMatcher implements MatcherGroupForDomain.
|
||||
func (g *MphMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
|
||||
pattern := strings.ToLower(matcher.Pattern())
|
||||
hash := g.addPattern(0, "", pattern, matcher.Type(), value) // For full domain match
|
||||
g.addPattern(hash, pattern, ".", matcher.Type(), value) // For partial domain match
|
||||
}
|
||||
|
||||
func (g *MphMatcherGroup) addPattern(suffixHash uint32, suffixPattern string, pattern string, matcherType Type, value uint32) uint32 {
|
||||
fullPattern := pattern + suffixPattern
|
||||
info, found := (*g.ruleInfos)[fullPattern]
|
||||
if !found {
|
||||
info = mphRuleInfo{rollingHash: RollingHash(suffixHash, pattern)}
|
||||
g.rules = append(g.rules, fullPattern)
|
||||
g.values = append(g.values, nil)
|
||||
}
|
||||
info.matchers[matcherType] = append(info.matchers[matcherType], value)
|
||||
(*g.ruleInfos)[fullPattern] = info
|
||||
return info.rollingHash
|
||||
}
|
||||
|
||||
// Build builds a minimal perfect hash table for insert rules.
|
||||
// Algorithm used: Hash, displace, and compress. See http://cmph.sourceforge.net/papers/esa09.pdf
|
||||
func (g *MphMatcherGroup) Build() error {
|
||||
ruleCount := len(*g.ruleInfos)
|
||||
g.level0 = make([]uint32, nextPow2(ruleCount/4))
|
||||
g.level0Mask = uint32(len(g.level0) - 1)
|
||||
g.level1 = make([]uint32, nextPow2(ruleCount))
|
||||
g.level1Mask = uint32(len(g.level1) - 1)
|
||||
|
||||
// Create buckets based on all rule's rolling hash
|
||||
buckets := make([][]uint32, len(g.level0))
|
||||
for ruleIdx := 1; ruleIdx < len(g.rules); ruleIdx++ { // Traverse rules starting from index 1 (0 reserved for failed lookup)
|
||||
ruleInfo := (*g.ruleInfos)[g.rules[ruleIdx]]
|
||||
bucketIdx := ruleInfo.rollingHash & g.level0Mask
|
||||
buckets[bucketIdx] = append(buckets[bucketIdx], uint32(ruleIdx))
|
||||
g.values[ruleIdx] = append(ruleInfo.matchers[Full], ruleInfo.matchers[Domain]...) // nolint:gocritic
|
||||
}
|
||||
g.ruleInfos = nil // Set ruleInfos nil to release memory
|
||||
runtime.GC() // peak mem
|
||||
|
||||
// Sort buckets in descending order with respect to each bucket's size
|
||||
bucketIdxs := make([]int, len(buckets))
|
||||
for bucketIdx := range buckets {
|
||||
bucketIdxs[bucketIdx] = bucketIdx
|
||||
}
|
||||
sort.Slice(bucketIdxs, func(i, j int) bool { return len(buckets[bucketIdxs[i]]) > len(buckets[bucketIdxs[j]]) })
|
||||
|
||||
// Exercise Hash, Displace, and Compress algorithm to construct minimal perfect hash table
|
||||
occupied := make([]bool, len(g.level1)) // Whether a second-level hash has been already used
|
||||
hashedBucket := make([]uint32, 0, 4) // Second-level hashes for each rule in a specific bucket
|
||||
for _, bucketIdx := range bucketIdxs {
|
||||
bucket := buckets[bucketIdx]
|
||||
hashedBucket = hashedBucket[:0]
|
||||
seed := uint32(0)
|
||||
for len(hashedBucket) != len(bucket) {
|
||||
for _, ruleIdx := range bucket {
|
||||
memHash := MemHash(seed, g.rules[ruleIdx]) & g.level1Mask
|
||||
if occupied[memHash] { // Collision occurred with this seed
|
||||
for _, hash := range hashedBucket { // Revert all values in this hashed bucket
|
||||
occupied[hash] = false
|
||||
g.level1[hash] = 0
|
||||
}
|
||||
hashedBucket = hashedBucket[:0]
|
||||
seed++ // Try next seed
|
||||
break
|
||||
}
|
||||
occupied[memHash] = true
|
||||
g.level1[memHash] = ruleIdx // The final value in the hash table
|
||||
hashedBucket = append(hashedBucket, memHash)
|
||||
}
|
||||
}
|
||||
g.level0[bucketIdx] = seed // Displacement value for this bucket
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup searches for input in minimal perfect hash table and returns its index. 0 indicates not found.
|
||||
func (g *MphMatcherGroup) Lookup(rollingHash uint32, input string) uint32 {
|
||||
i0 := rollingHash & g.level0Mask
|
||||
seed := g.level0[i0]
|
||||
i1 := MemHash(seed, input) & g.level1Mask
|
||||
if n := g.level1[i1]; g.rules[n] == input {
|
||||
return n
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Match implements MatcherGroup.Match.
|
||||
func (g *MphMatcherGroup) Match(input string) []uint32 {
|
||||
matches := make([][]uint32, 0, 5)
|
||||
hash := uint32(0)
|
||||
for i := len(input) - 1; i >= 0; i-- {
|
||||
hash = hash*PrimeRK + uint32(input[i])
|
||||
if input[i] == '.' {
|
||||
if mphIdx := g.Lookup(hash, input[i:]); mphIdx != 0 {
|
||||
matches = append(matches, g.values[mphIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
if mphIdx := g.Lookup(hash, input); mphIdx != 0 {
|
||||
matches = append(matches, g.values[mphIdx])
|
||||
}
|
||||
return CompositeMatchesReverse(matches)
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherGroup.MatchAny.
|
||||
func (g *MphMatcherGroup) MatchAny(input string) bool {
|
||||
hash := uint32(0)
|
||||
for i := len(input) - 1; i >= 0; i-- {
|
||||
hash = hash*PrimeRK + uint32(input[i])
|
||||
if input[i] == '.' {
|
||||
if g.Lookup(hash, input[i:]) != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return g.Lookup(hash, input) != 0
|
||||
}
|
||||
|
||||
func nextPow2(v int) int {
|
||||
if v <= 1 {
|
||||
return 1
|
||||
}
|
||||
const MaxUInt = ^uint(0)
|
||||
n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1
|
||||
return int(n)
|
||||
}
|
||||
|
||||
//go:noescape
|
||||
//go:linkname strhash runtime.strhash
|
||||
func strhash(p unsafe.Pointer, h uintptr) uintptr
|
||||
@@ -0,0 +1,278 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestMphMatcherGroup(t *testing.T) {
|
||||
cases1 := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
input string
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "www.example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "www.e3ample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "xample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "xexample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
input: "xexample.com",
|
||||
output: false,
|
||||
},
|
||||
}
|
||||
for _, test := range cases1 {
|
||||
mph := NewMphMatcherGroup()
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(mph, matcher, 0))
|
||||
mph.Build()
|
||||
if m := mph.MatchAny(test.input); m != test.output {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
{
|
||||
cases2Input := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "163.com",
|
||||
mType: Domain,
|
||||
},
|
||||
{
|
||||
pattern: "m.126.com",
|
||||
mType: Full,
|
||||
},
|
||||
{
|
||||
pattern: "3.com",
|
||||
mType: Full,
|
||||
},
|
||||
}
|
||||
mph := NewMphMatcherGroup()
|
||||
for _, test := range cases2Input {
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(mph, matcher, 0))
|
||||
}
|
||||
mph.Build()
|
||||
cases2Output := []struct {
|
||||
pattern string
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
pattern: "126.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "m.163.com",
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
pattern: "mm163.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "m.126.com",
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
pattern: "163.com",
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
pattern: "63.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "oogle.com",
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
pattern: "vvgoogle.com",
|
||||
res: false,
|
||||
},
|
||||
}
|
||||
for _, test := range cases2Output {
|
||||
if m := mph.MatchAny(test.pattern); m != test.res {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
cases3Input := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
}{
|
||||
{
|
||||
pattern: "video.google.com",
|
||||
mType: Domain,
|
||||
},
|
||||
{
|
||||
pattern: "gle.com",
|
||||
mType: Domain,
|
||||
},
|
||||
}
|
||||
mph := NewMphMatcherGroup()
|
||||
for _, test := range cases3Input {
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(mph, matcher, 0))
|
||||
}
|
||||
mph.Build()
|
||||
cases3Output := []struct {
|
||||
pattern string
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
pattern: "google.com",
|
||||
res: false,
|
||||
},
|
||||
}
|
||||
for _, test := range cases3Output {
|
||||
if m := mph.MatchAny(test.pattern); m != test.res {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
|
||||
func TestMphMatcherGroupAsIndexMatcher(t *testing.T) {
|
||||
rules := []struct {
|
||||
Type Type
|
||||
Domain string
|
||||
}{
|
||||
// Regex not supported by MphMatcherGroup
|
||||
// {
|
||||
// Type: Regex,
|
||||
// Domain: "apis\\.us$",
|
||||
// },
|
||||
// Substr not supported by MphMatcherGroup
|
||||
// {
|
||||
// Type: Substr,
|
||||
// Domain: "apis",
|
||||
// },
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
// Substr not supported by MphMatcherGroup, We add another matcher to preserve index
|
||||
{
|
||||
Type: Domain, // Substr,
|
||||
Domain: "example.com", // "apis",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "fonts.googleapis.com",
|
||||
},
|
||||
{
|
||||
Type: Full,
|
||||
Domain: "www.baidu.com",
|
||||
},
|
||||
{ // This matcher (index 10) is swapped with matcher (index 6) to test that full matcher takes high priority.
|
||||
Type: Full,
|
||||
Domain: "example.com",
|
||||
},
|
||||
{
|
||||
Type: Domain,
|
||||
Domain: "example.com",
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
Input string
|
||||
Output []uint32
|
||||
}{
|
||||
{
|
||||
Input: "www.baidu.com",
|
||||
Output: []uint32{5, 9, 4},
|
||||
},
|
||||
{
|
||||
Input: "fonts.googleapis.com",
|
||||
Output: []uint32{8, 3, 7, 4 /*2, 6*/},
|
||||
},
|
||||
{
|
||||
Input: "example.googleapis.com",
|
||||
Output: []uint32{3, 7, 4 /*2, 6*/},
|
||||
},
|
||||
{
|
||||
Input: "testapis.us",
|
||||
// Output: []uint32{ /*2, 6*/ /*1,*/ },
|
||||
Output: nil,
|
||||
},
|
||||
{
|
||||
Input: "example.com",
|
||||
Output: []uint32{10, 6, 11, 4},
|
||||
},
|
||||
}
|
||||
matcherGroup := NewMphMatcherGroup()
|
||||
for i, rule := range rules {
|
||||
matcher, err := rule.Type.New(rule.Domain)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(i+3)))
|
||||
}
|
||||
matcherGroup.Build()
|
||||
for _, test := range cases {
|
||||
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyMphMatcherGroup(t *testing.T) {
|
||||
g := NewMphMatcherGroup()
|
||||
g.Build()
|
||||
r := g.Match("example.com")
|
||||
if len(r) != 0 {
|
||||
t.Error("Expect [], but ", r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package strmatcher
|
||||
|
||||
type matcherEntry struct {
|
||||
matcher Matcher
|
||||
value uint32
|
||||
}
|
||||
|
||||
// SimpleMatcherGroup is an implementation of MatcherGroup.
|
||||
// It simply stores all matchers in an array and sequentially matches them.
|
||||
type SimpleMatcherGroup struct {
|
||||
matchers []matcherEntry
|
||||
}
|
||||
|
||||
// AddMatcher implements MatcherGroupForAll.AddMatcher.
|
||||
func (g *SimpleMatcherGroup) AddMatcher(matcher Matcher, value uint32) {
|
||||
g.matchers = append(g.matchers, matcherEntry{
|
||||
matcher: matcher,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
|
||||
// Match implements MatcherGroup.Match.
|
||||
func (g *SimpleMatcherGroup) Match(input string) []uint32 {
|
||||
result := []uint32{}
|
||||
for _, e := range g.matchers {
|
||||
if e.matcher.Match(input) {
|
||||
result = append(result, e.value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherGroup.MatchAny.
|
||||
func (g *SimpleMatcherGroup) MatchAny(input string) bool {
|
||||
for _, e := range g.matchers {
|
||||
if e.matcher.Match(input) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -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 TestSimpleMatcherGroup(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 []uint32
|
||||
}{
|
||||
{
|
||||
input: "www.example.com",
|
||||
output: []uint32{0, 2},
|
||||
},
|
||||
{
|
||||
input: "example.com",
|
||||
output: []uint32{0, 1, 2},
|
||||
},
|
||||
{
|
||||
input: "www.e3ample.com",
|
||||
output: []uint32{},
|
||||
},
|
||||
{
|
||||
input: "xample.com",
|
||||
output: []uint32{},
|
||||
},
|
||||
{
|
||||
input: "xexample.com",
|
||||
output: []uint32{2},
|
||||
},
|
||||
{
|
||||
input: "examplexcom",
|
||||
output: []uint32{2},
|
||||
},
|
||||
}
|
||||
matcherGroup := &SimpleMatcherGroup{}
|
||||
for id, entry := range patterns {
|
||||
matcher, err := entry.mType.New(entry.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(id)))
|
||||
}
|
||||
for _, test := range cases {
|
||||
if r := matcherGroup.Match(test.input); !reflect.DeepEqual(r, test.output) {
|
||||
t.Error("unexpected output: ", r, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package strmatcher
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SubstrMatcherGroup is implementation of MatcherGroup,
|
||||
// It is simply implmeneted to comply with the priority specification of Substr matchers.
|
||||
type SubstrMatcherGroup struct {
|
||||
patterns []string
|
||||
values []uint32
|
||||
}
|
||||
|
||||
// AddSubstrMatcher implements MatcherGroupForSubstr.AddSubstrMatcher.
|
||||
func (g *SubstrMatcherGroup) AddSubstrMatcher(matcher SubstrMatcher, value uint32) {
|
||||
g.patterns = append(g.patterns, matcher.Pattern())
|
||||
g.values = append(g.values, value)
|
||||
}
|
||||
|
||||
// Match implements MatcherGroup.Match.
|
||||
func (g *SubstrMatcherGroup) Match(input string) []uint32 {
|
||||
var result []uint32
|
||||
for i, pattern := range g.patterns {
|
||||
for j := strings.LastIndex(input, pattern); j != -1; j = strings.LastIndex(input[:j], pattern) {
|
||||
result = append(result, uint32(j)<<16|uint32(i)&0xffff) // uint32: position (higher 16 bit) | patternIdx (lower 16 bit)
|
||||
}
|
||||
}
|
||||
// sort.Slice will trigger allocation no matter what input is. See https://github.com/golang/go/issues/17332
|
||||
// We optimize the sorting by length to prevent memory allocation as possible.
|
||||
switch len(result) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
// No need to sort
|
||||
case 2:
|
||||
// Do a simple swap if unsorted
|
||||
if result[0] > result[1] {
|
||||
result[0], result[1] = result[1], result[0]
|
||||
}
|
||||
default:
|
||||
// Sort the match results in dictionary order, so that:
|
||||
// 1. Pattern matched at smaller position (meaning matched further) takes precedence.
|
||||
// 2. When patterns matched at same position, pattern with smaller index (meaning inserted early) takes precedence.
|
||||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||
}
|
||||
for i, entry := range result {
|
||||
result[i] = g.values[entry&0xffff] // Get pattern value from its index (the lower 16 bit)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MatchAny implements MatcherGroup.MatchAny.
|
||||
func (g *SubstrMatcherGroup) MatchAny(input string) bool {
|
||||
for _, pattern := range g.patterns {
|
||||
if strings.Contains(input, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestSubstrMatcherGroup(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 []uint32
|
||||
}{
|
||||
{
|
||||
input: "google.com",
|
||||
output: []uint32{1},
|
||||
},
|
||||
{
|
||||
input: "apis.com",
|
||||
output: []uint32{0, 2},
|
||||
},
|
||||
{
|
||||
input: "googleapis.com",
|
||||
output: []uint32{1, 0, 2},
|
||||
},
|
||||
{
|
||||
input: "fonts.googleapis.com",
|
||||
output: []uint32{1, 0, 2},
|
||||
},
|
||||
{
|
||||
input: "apis.googleapis.com",
|
||||
output: []uint32{0, 2, 1, 0, 2},
|
||||
},
|
||||
}
|
||||
matcherGroup := &SubstrMatcherGroup{}
|
||||
for id, entry := range patterns {
|
||||
matcher, err := entry.mType.New(entry.pattern)
|
||||
common.Must(err)
|
||||
common.Must(AddMatcherToGroup(matcherGroup, matcher, uint32(id)))
|
||||
}
|
||||
for _, test := range cases {
|
||||
if r := matcherGroup.Match(test.input); !reflect.DeepEqual(r, test.output) {
|
||||
t.Error("unexpected output: ", r, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package strmatcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// FullMatcher is an implementation of Matcher.
|
||||
type FullMatcher string
|
||||
|
||||
func (FullMatcher) Type() Type {
|
||||
return Full
|
||||
}
|
||||
|
||||
func (m FullMatcher) Pattern() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
func (m FullMatcher) String() string {
|
||||
return "full:" + m.Pattern()
|
||||
}
|
||||
|
||||
func (m FullMatcher) Match(s string) bool {
|
||||
return string(m) == s
|
||||
}
|
||||
|
||||
// DomainMatcher is an implementation of Matcher.
|
||||
type DomainMatcher string
|
||||
|
||||
func (DomainMatcher) Type() Type {
|
||||
return Domain
|
||||
}
|
||||
|
||||
func (m DomainMatcher) Pattern() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
func (m DomainMatcher) String() string {
|
||||
return "domain:" + m.Pattern()
|
||||
}
|
||||
|
||||
func (m DomainMatcher) Match(s string) bool {
|
||||
pattern := m.Pattern()
|
||||
if !strings.HasSuffix(s, pattern) {
|
||||
return false
|
||||
}
|
||||
return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.'
|
||||
}
|
||||
|
||||
// SubstrMatcher is an implementation of Matcher.
|
||||
type SubstrMatcher string
|
||||
|
||||
func (SubstrMatcher) Type() Type {
|
||||
return Substr
|
||||
}
|
||||
|
||||
func (m SubstrMatcher) Pattern() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
func (m SubstrMatcher) String() string {
|
||||
return "keyword:" + m.Pattern()
|
||||
}
|
||||
|
||||
func (m SubstrMatcher) Match(s string) bool {
|
||||
return strings.Contains(s, m.Pattern())
|
||||
}
|
||||
|
||||
// RegexMatcher is an implementation of Matcher.
|
||||
type RegexMatcher struct {
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
func (*RegexMatcher) Type() Type {
|
||||
return Regex
|
||||
}
|
||||
|
||||
func (m *RegexMatcher) Pattern() string {
|
||||
return m.pattern.String()
|
||||
}
|
||||
|
||||
func (m *RegexMatcher) String() string {
|
||||
return "regexp:" + m.Pattern()
|
||||
}
|
||||
|
||||
func (m *RegexMatcher) Match(s string) bool {
|
||||
return m.pattern.MatchString(s)
|
||||
}
|
||||
|
||||
// New creates a new Matcher based on the given pattern.
|
||||
func (t Type) New(pattern string) (Matcher, error) {
|
||||
switch t {
|
||||
case Full:
|
||||
return FullMatcher(pattern), nil
|
||||
case Substr:
|
||||
return SubstrMatcher(pattern), nil
|
||||
case Domain:
|
||||
pattern, err := ToDomain(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DomainMatcher(pattern), nil
|
||||
case Regex: // 1. regex matching is case-sensitive
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RegexMatcher{pattern: regex}, nil
|
||||
default:
|
||||
return nil, errors.New("unknown matcher type")
|
||||
}
|
||||
}
|
||||
|
||||
// NewDomainPattern creates a new Matcher based on the given domain pattern.
|
||||
// It works like `Type.New`, but will do validation and conversion to ensure it's a valid domain pattern.
|
||||
func (t Type) NewDomainPattern(pattern string) (Matcher, error) {
|
||||
switch t {
|
||||
case Full:
|
||||
pattern, err := ToDomain(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FullMatcher(pattern), nil
|
||||
case Substr:
|
||||
pattern, err := ToDomain(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return SubstrMatcher(pattern), nil
|
||||
case Domain:
|
||||
pattern, err := ToDomain(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DomainMatcher(pattern), nil
|
||||
case Regex: // Regex's charset not in LDH subset
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RegexMatcher{pattern: regex}, nil
|
||||
default:
|
||||
return nil, errors.New("unknown matcher type")
|
||||
}
|
||||
}
|
||||
|
||||
// ToDomain converts input pattern to a domain string, and return error if such a conversion cannot be made.
|
||||
// 1. Conforms to Letter-Digit-Hyphen (LDH) subset (https://tools.ietf.org/html/rfc952):
|
||||
// * Letters A to Z (no distinction between uppercase and lowercase, we convert to lowers)
|
||||
// * Digits 0 to 9
|
||||
// * Hyphens(-) and Periods(.)
|
||||
// 2. If any non-ASCII characters, domain are converted from Internationalized domain name to Punycode.
|
||||
func ToDomain(pattern string) (string, error) {
|
||||
for {
|
||||
isASCII, hasUpper := true, false
|
||||
for i := 0; i < len(pattern); i++ {
|
||||
c := pattern[i]
|
||||
if c >= utf8.RuneSelf {
|
||||
isASCII = false
|
||||
break
|
||||
}
|
||||
switch {
|
||||
case 'A' <= c && c <= 'Z':
|
||||
hasUpper = true
|
||||
case 'a' <= c && c <= 'z':
|
||||
case '0' <= c && c <= '9':
|
||||
case c == '-':
|
||||
case c == '.':
|
||||
default:
|
||||
return "", errors.New("pattern string does not conform to Letter-Digit-Hyphen (LDH) subset")
|
||||
}
|
||||
}
|
||||
if !isASCII {
|
||||
var err error
|
||||
pattern, err = idna.Punycode.ToASCII(pattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if hasUpper {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
break
|
||||
}
|
||||
return pattern, nil
|
||||
}
|
||||
|
||||
// MatcherGroupForAll is an interface indicating a MatcherGroup could accept all types of matchers.
|
||||
type MatcherGroupForAll interface {
|
||||
AddMatcher(matcher Matcher, value uint32)
|
||||
}
|
||||
|
||||
// MatcherGroupForFull is an interface indicating a MatcherGroup could accept FullMatchers.
|
||||
type MatcherGroupForFull interface {
|
||||
AddFullMatcher(matcher FullMatcher, value uint32)
|
||||
}
|
||||
|
||||
// MatcherGroupForDomain is an interface indicating a MatcherGroup could accept DomainMatchers.
|
||||
type MatcherGroupForDomain interface {
|
||||
AddDomainMatcher(matcher DomainMatcher, value uint32)
|
||||
}
|
||||
|
||||
// MatcherGroupForSubstr is an interface indicating a MatcherGroup could accept SubstrMatchers.
|
||||
type MatcherGroupForSubstr interface {
|
||||
AddSubstrMatcher(matcher SubstrMatcher, value uint32)
|
||||
}
|
||||
|
||||
// MatcherGroupForRegex is an interface indicating a MatcherGroup could accept RegexMatchers.
|
||||
type MatcherGroupForRegex interface {
|
||||
AddRegexMatcher(matcher *RegexMatcher, value uint32)
|
||||
}
|
||||
|
||||
// AddMatcherToGroup is a helper function to try to add a Matcher to any kind of MatcherGroup.
|
||||
// It returns error if the MatcherGroup does not accept the provided Matcher's type.
|
||||
// This function is provided to help writing code to test a MatcherGroup.
|
||||
func AddMatcherToGroup(g MatcherGroup, matcher Matcher, value uint32) error {
|
||||
if g, ok := g.(IndexMatcher); ok {
|
||||
g.Add(matcher)
|
||||
return nil
|
||||
}
|
||||
if g, ok := g.(MatcherGroupForAll); ok {
|
||||
g.AddMatcher(matcher, value)
|
||||
return nil
|
||||
}
|
||||
switch matcher := matcher.(type) {
|
||||
case FullMatcher:
|
||||
if g, ok := g.(MatcherGroupForFull); ok {
|
||||
g.AddFullMatcher(matcher, value)
|
||||
return nil
|
||||
}
|
||||
case DomainMatcher:
|
||||
if g, ok := g.(MatcherGroupForDomain); ok {
|
||||
g.AddDomainMatcher(matcher, value)
|
||||
return nil
|
||||
}
|
||||
case SubstrMatcher:
|
||||
if g, ok := g.(MatcherGroupForSubstr); ok {
|
||||
g.AddSubstrMatcher(matcher, value)
|
||||
return nil
|
||||
}
|
||||
case *RegexMatcher:
|
||||
if g, ok := g.(MatcherGroupForRegex); ok {
|
||||
g.AddRegexMatcher(matcher, value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("cannot add matcher to matcher group")
|
||||
}
|
||||
|
||||
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
|
||||
// It is designed to avoid new memory allocation as possible.
|
||||
func CompositeMatches(matches [][]uint32) []uint32 {
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return matches[0]
|
||||
default:
|
||||
result := make([]uint32, 0, 5)
|
||||
for i := 0; i < len(matches); i++ {
|
||||
result = append(result, matches[i]...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// CompositeMatches flattens the matches slice to produce a single matched indices slice.
|
||||
// It is designed that:
|
||||
// 1. All matchers are concatenated in reverse order, so the matcher that matches further ranks higher.
|
||||
// 2. Indices in the same matcher keeps their original order.
|
||||
// 3. Avoid new memory allocation as possible.
|
||||
func CompositeMatchesReverse(matches [][]uint32) []uint32 {
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return matches[0]
|
||||
default:
|
||||
result := make([]uint32, 0, 5)
|
||||
for i := len(matches) - 1; i >= 0; i-- {
|
||||
result = append(result, matches[i]...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
. "github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||
)
|
||||
|
||||
func TestMatcher(t *testing.T) {
|
||||
cases := []struct {
|
||||
pattern string
|
||||
mType Type
|
||||
input string
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "www.example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "www.e3ample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "xample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Domain,
|
||||
input: "xexample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
input: "example.com",
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Full,
|
||||
input: "xexample.com",
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
pattern: "example.com",
|
||||
mType: Regex,
|
||||
input: "examplexcom",
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
for _, test := range cases {
|
||||
matcher, err := test.mType.New(test.pattern)
|
||||
common.Must(err)
|
||||
if m := matcher.Match(test.input); m != test.output {
|
||||
t.Error("unexpected output: ", m, " for test case ", test)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDomain(t *testing.T) {
|
||||
{ // Test normal ASCII domain, which should not trigger new string data allocation
|
||||
input := "example.com"
|
||||
domain, err := ToDomain(input)
|
||||
if err != nil {
|
||||
t.Error("unexpected error: ", err)
|
||||
}
|
||||
if domain != input {
|
||||
t.Error("unexpected output: ", domain, " for test case ", input)
|
||||
}
|
||||
if (*reflect.StringHeader)(unsafe.Pointer(&input)).Data != (*reflect.StringHeader)(unsafe.Pointer(&domain)).Data {
|
||||
t.Error("different string data of output: ", domain, " and test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test ASCII domain containing upper case letter, which should be converted to lower case
|
||||
input := "eXAMPLE.cOm"
|
||||
domain, err := ToDomain(input)
|
||||
if err != nil {
|
||||
t.Error("unexpected error: ", err)
|
||||
}
|
||||
if domain != "example.com" {
|
||||
t.Error("unexpected output: ", domain, " for test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test internationalized domain, which should be translated to ASCII punycode
|
||||
input := "example.公益"
|
||||
domain, err := ToDomain(input)
|
||||
if err != nil {
|
||||
t.Error("unexpected error: ", err)
|
||||
}
|
||||
if domain != "example.xn--55qw42g" {
|
||||
t.Error("unexpected output: ", domain, " for test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test internationalized domain containing upper case letter
|
||||
input := "eXAMPLE.公益"
|
||||
domain, err := ToDomain(input)
|
||||
if err != nil {
|
||||
t.Error("unexpected error: ", err)
|
||||
}
|
||||
if domain != "example.xn--55qw42g" {
|
||||
t.Error("unexpected output: ", domain, " for test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test domain name of invalid character, which should return with error
|
||||
input := "{"
|
||||
_, err := ToDomain(input)
|
||||
if err == nil {
|
||||
t.Error("unexpected non error for test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test domain name containing a space, which should return with error
|
||||
input := "Mijia Cloud"
|
||||
_, err := ToDomain(input)
|
||||
if err == nil {
|
||||
t.Error("unexpected non error for test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test domain name containing an underscore, which should return with error
|
||||
input := "Mijia_Cloud.com"
|
||||
_, err := ToDomain(input)
|
||||
if err == nil {
|
||||
t.Error("unexpected non error for test case ", input)
|
||||
}
|
||||
}
|
||||
{ // Test internationalized domain containing invalid character
|
||||
input := "Mijia Cloud.公司"
|
||||
_, err := ToDomain(input)
|
||||
if err == nil {
|
||||
t.Error("unexpected non error for test case ", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package strmatcher
|
||||
|
||||
// Type is the type of the matcher.
|
||||
type Type byte
|
||||
|
||||
const (
|
||||
// Full is the type of matcher that the input string must exactly equal to the pattern.
|
||||
Full Type = 0
|
||||
// Domain is the type of matcher that the input string must be a sub-domain or itself of the pattern.
|
||||
Domain Type = 1
|
||||
// Substr is the type of matcher that the input string must contain the pattern as a sub-string.
|
||||
Substr Type = 2
|
||||
// Regex is the type of matcher that the input string must matches the regular-expression pattern.
|
||||
Regex Type = 3
|
||||
)
|
||||
|
||||
// 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).
|
||||
type Matcher interface {
|
||||
// Type returns the matcher's type.
|
||||
Type() Type
|
||||
|
||||
// Pattern returns the matcher's raw string representation.
|
||||
Pattern() string
|
||||
|
||||
// String returns a string representation of the matcher containing its type and pattern.
|
||||
String() string
|
||||
|
||||
// Match returns true if the given string matches a predefined pattern.
|
||||
// * This method is seldom used for performance reason
|
||||
// and is generally taken over by their corresponding MatcherGroup.
|
||||
Match(input string) bool
|
||||
}
|
||||
|
||||
// MatcherGroup is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
|
||||
// For example:
|
||||
// - FullMatcherGroup accepts FullMatcher and uses a hash table to facilitate lookup.
|
||||
// - DomainMatcherGroup accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
|
||||
type MatcherGroup interface {
|
||||
// Match returns all matched matchers with their corresponding values.
|
||||
Match(input string) []uint32
|
||||
|
||||
// MatchAny returns true as soon as one matching matcher is found.
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
|
||||
// IndexMatcher is a general type of matcher thats accepts all kinds of basic matchers.
|
||||
// It should:
|
||||
// - Accept all Matcher types with no exception.
|
||||
// - Optimize string matching with a combination of MatcherGroups.
|
||||
// - Obey certain priority order specification when returning matched Matchers.
|
||||
type IndexMatcher interface {
|
||||
// Size returns number of matchers added to IndexMatcher.
|
||||
Size() uint32
|
||||
|
||||
// Add adds a new Matcher to IndexMatcher, and returns its index. The index will never be 0.
|
||||
Add(matcher Matcher) uint32
|
||||
|
||||
// Build builds the IndexMatcher to be ready for matching.
|
||||
Build() error
|
||||
|
||||
// Match returns the indices of all matchers that matches the input.
|
||||
// * Empty array is returned if no such matcher exists.
|
||||
// * The order of returned matchers should follow priority specification.
|
||||
// Priority specification:
|
||||
// 1. Priority between matcher types: full > domain > substr > regex.
|
||||
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
|
||||
// 3. Priority of domain matchers matching at different levels: the further matched domain takes precedence.
|
||||
// 4. Priority of substr matchers matching at different positions: the further matched substr takes precedence.
|
||||
Match(input string) []uint32
|
||||
|
||||
// MatchAny returns true as soon as one matching matcher is found.
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
|
||||
// ValueMatcher is a general type of matcher that accepts all kinds of basic matchers.
|
||||
// It should:
|
||||
// - Accept all Matcher types with no exception.
|
||||
// - Optimize string matching with a combination of MatcherGroups.
|
||||
// - Obey certain priority order specification when returning matched values.
|
||||
type ValueMatcher interface {
|
||||
// Add adds a new Matcher to ValueMatcher, binding it to the provided value.
|
||||
Add(matcher Matcher, value uint32)
|
||||
|
||||
// Build builds the ValueMatcher to be ready for matching.
|
||||
Build() error
|
||||
|
||||
// Match returns the values of all matchers that matches the input.
|
||||
// * Empty array is returned if no such matcher exists.
|
||||
// * The order of returned values should follow priority specification.
|
||||
// * Same value may appear multiple times if multiple matched matchers were added with that value.
|
||||
// Priority specification:
|
||||
// 1. Priority between matcher types: full > domain > substr > regex.
|
||||
// 2. Priority of same-priority matchers matching at same position: the early added takes precedence.
|
||||
// 3. Priority of domain matchers matching at different levels: the further matched domain takes precedence.
|
||||
// 4. Priority of substr matchers matching at different positions: the further matched substr takes precedence.
|
||||
Match(input string) []uint32
|
||||
|
||||
// MatchAny returns true as soon as one matching matcher is found.
|
||||
MatchAny(input string) bool
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package strmatcher
|
||||
|
||||
// LinearValueMatcher is an implementation of ValueMatcher.
|
||||
type LinearValueMatcher struct {
|
||||
full *FullMatcherGroup
|
||||
domain *DomainMatcherGroup
|
||||
substr *SubstrMatcherGroup
|
||||
regex *SimpleMatcherGroup
|
||||
}
|
||||
|
||||
func NewLinearValueMatcher() *LinearValueMatcher {
|
||||
return new(LinearValueMatcher)
|
||||
}
|
||||
|
||||
// Add implements ValueMatcher.Add.
|
||||
func (g *LinearValueMatcher) Add(matcher Matcher, value uint32) {
|
||||
switch matcher := matcher.(type) {
|
||||
case FullMatcher:
|
||||
if g.full == nil {
|
||||
g.full = NewFullMatcherGroup()
|
||||
}
|
||||
g.full.AddFullMatcher(matcher, value)
|
||||
case DomainMatcher:
|
||||
if g.domain == nil {
|
||||
g.domain = NewDomainMatcherGroup()
|
||||
}
|
||||
g.domain.AddDomainMatcher(matcher, value)
|
||||
case SubstrMatcher:
|
||||
if g.substr == nil {
|
||||
g.substr = new(SubstrMatcherGroup)
|
||||
}
|
||||
g.substr.AddSubstrMatcher(matcher, value)
|
||||
default:
|
||||
if g.regex == nil {
|
||||
g.regex = new(SimpleMatcherGroup)
|
||||
}
|
||||
g.regex.AddMatcher(matcher, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Build implements ValueMatcher.Build.
|
||||
func (*LinearValueMatcher) Build() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match implements ValueMatcher.Match.
|
||||
func (g *LinearValueMatcher) Match(input string) []uint32 {
|
||||
// Allocate capacity to prevent matches escaping to heap
|
||||
result := make([][]uint32, 0, 5)
|
||||
if g.full != nil {
|
||||
if matches := g.full.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.domain != nil {
|
||||
if matches := g.domain.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.substr != nil {
|
||||
if matches := g.substr.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.regex != nil {
|
||||
if matches := g.regex.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
return CompositeMatches(result)
|
||||
}
|
||||
|
||||
// MatchAny implements ValueMatcher.MatchAny.
|
||||
func (g *LinearValueMatcher) MatchAny(input string) bool {
|
||||
if g.full != nil && g.full.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
if g.domain != nil && g.domain.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
if g.substr != nil && g.substr.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
return g.regex != nil && g.regex.MatchAny(input)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package strmatcher
|
||||
|
||||
import "runtime"
|
||||
|
||||
// A MphValueMatcher is divided into three parts:
|
||||
// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table;
|
||||
// 2. `substr` patterns are matched by ac automaton;
|
||||
// 3. `regex` patterns are matched with the regex library.
|
||||
type MphValueMatcher struct {
|
||||
mph *MphMatcherGroup
|
||||
ac *ACAutomatonMatcherGroup
|
||||
regex *SimpleMatcherGroup
|
||||
}
|
||||
|
||||
func NewMphValueMatcher() *MphValueMatcher {
|
||||
return new(MphValueMatcher)
|
||||
}
|
||||
|
||||
// Add implements ValueMatcher.Add.
|
||||
func (g *MphValueMatcher) Add(matcher Matcher, value uint32) {
|
||||
switch matcher := matcher.(type) {
|
||||
case FullMatcher:
|
||||
if g.mph == nil {
|
||||
g.mph = NewMphMatcherGroup()
|
||||
}
|
||||
g.mph.AddFullMatcher(matcher, value)
|
||||
case DomainMatcher:
|
||||
if g.mph == nil {
|
||||
g.mph = NewMphMatcherGroup()
|
||||
}
|
||||
g.mph.AddDomainMatcher(matcher, value)
|
||||
case SubstrMatcher:
|
||||
if g.ac == nil {
|
||||
g.ac = NewACAutomatonMatcherGroup()
|
||||
}
|
||||
g.ac.AddSubstrMatcher(matcher, value)
|
||||
case *RegexMatcher:
|
||||
if g.regex == nil {
|
||||
g.regex = &SimpleMatcherGroup{}
|
||||
}
|
||||
g.regex.AddMatcher(matcher, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Build implements ValueMatcher.Build.
|
||||
func (g *MphValueMatcher) Build() error {
|
||||
if g.mph != nil {
|
||||
runtime.GC() // peak mem
|
||||
g.mph.Build()
|
||||
}
|
||||
runtime.GC() // peak mem
|
||||
if g.ac != nil {
|
||||
g.ac.Build()
|
||||
runtime.GC() // peak mem
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match implements ValueMatcher.Match.
|
||||
func (g *MphValueMatcher) Match(input string) []uint32 {
|
||||
result := make([][]uint32, 0, 5)
|
||||
if g.mph != nil {
|
||||
if matches := g.mph.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.ac != nil {
|
||||
if matches := g.ac.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
if g.regex != nil {
|
||||
if matches := g.regex.Match(input); len(matches) > 0 {
|
||||
result = append(result, matches)
|
||||
}
|
||||
}
|
||||
return CompositeMatches(result)
|
||||
}
|
||||
|
||||
// MatchAny implements ValueMatcher.MatchAny.
|
||||
func (g *MphValueMatcher) MatchAny(input string) bool {
|
||||
if g.mph != nil && g.mph.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
if g.ac != nil && g.ac.MatchAny(input) {
|
||||
return true
|
||||
}
|
||||
return g.regex != nil && g.regex.MatchAny(input)
|
||||
}
|
||||
Reference in new issue
Block a user