Skip to content

Commit a2f9fef

Browse files
committed
domain: Add adguard matcher
1 parent 7893a74 commit a2f9fef

5 files changed

Lines changed: 325 additions & 94 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package domain_test
2+
3+
import (
4+
"sort"
5+
"testing"
6+
7+
"github.com/sagernet/sing/common/domain"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestAdGuardMatcher(t *testing.T) {
13+
t.Parallel()
14+
ruleLines := []string{
15+
"||example.org^",
16+
"|example.com^",
17+
"example.net^",
18+
"||example.edu",
19+
"||example.edu.tw^",
20+
"|example.gov",
21+
"example.arpa",
22+
}
23+
matcher := domain.NewAdGuardMatcher(ruleLines)
24+
require.NotNil(t, matcher)
25+
matchDomain := []string{
26+
"example.org",
27+
"www.example.org",
28+
"example.com",
29+
"example.net",
30+
"isexample.net",
31+
"www.example.net",
32+
"example.edu",
33+
"example.edu.cn",
34+
"example.edu.tw",
35+
"www.example.edu",
36+
"www.example.edu.cn",
37+
"example.gov",
38+
"example.gov.cn",
39+
"example.arpa",
40+
"www.example.arpa",
41+
"isexample.arpa",
42+
"example.arpa.cn",
43+
"www.example.arpa.cn",
44+
"isexample.arpa.cn",
45+
}
46+
notMatchDomain := []string{
47+
"example.org.cn",
48+
"notexample.org",
49+
"example.com.cn",
50+
"www.example.com.cn",
51+
"example.net.cn",
52+
"notexample.edu",
53+
"notexample.edu.cn",
54+
"www.example.gov",
55+
"notexample.gov",
56+
}
57+
for _, domain := range matchDomain {
58+
require.True(t, matcher.Match(domain), domain)
59+
}
60+
for _, domain := range notMatchDomain {
61+
require.False(t, matcher.Match(domain), domain)
62+
}
63+
dLines := matcher.Dump()
64+
sort.Strings(ruleLines)
65+
sort.Strings(dLines)
66+
require.Equal(t, ruleLines, dLines)
67+
}

common/domain/adgurad_matcher.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package domain
2+
3+
import (
4+
"bytes"
5+
"sort"
6+
"strings"
7+
8+
"github.com/sagernet/sing/common"
9+
"github.com/sagernet/sing/common/varbin"
10+
)
11+
12+
const (
13+
anyLabel = '*'
14+
suffixLabel = '\b'
15+
)
16+
17+
type AdGuardMatcher struct {
18+
set *succinctSet
19+
}
20+
21+
func NewAdGuardMatcher(ruleLines []string) *AdGuardMatcher {
22+
ruleList := make([]string, 0, len(ruleLines))
23+
for _, ruleLine := range ruleLines {
24+
var (
25+
isSuffix bool // ||
26+
hasStart bool // |
27+
hasEnd bool // ^
28+
)
29+
if strings.HasPrefix(ruleLine, "||") {
30+
ruleLine = ruleLine[2:]
31+
isSuffix = true
32+
} else if strings.HasPrefix(ruleLine, "|") {
33+
ruleLine = ruleLine[1:]
34+
hasStart = true
35+
}
36+
if strings.HasSuffix(ruleLine, "^") {
37+
ruleLine = ruleLine[:len(ruleLine)-1]
38+
hasEnd = true
39+
}
40+
if isSuffix {
41+
ruleLine = string(rootLabel) + ruleLine
42+
} else if !hasStart {
43+
ruleLine = string(prefixLabel) + ruleLine
44+
}
45+
if !hasEnd {
46+
if strings.HasSuffix(ruleLine, ".") {
47+
ruleLine = ruleLine[:len(ruleLine)-1]
48+
}
49+
ruleLine += string(suffixLabel)
50+
}
51+
ruleList = append(ruleList, reverseDomain(ruleLine))
52+
}
53+
ruleList = common.Uniq(ruleList)
54+
sort.Strings(ruleList)
55+
return &AdGuardMatcher{newSuccinctSet(ruleList)}
56+
}
57+
58+
func ReadAdGuardMatcher(reader varbin.Reader) (*AdGuardMatcher, error) {
59+
set, err := readSuccinctSet(reader)
60+
if err != nil {
61+
return nil, err
62+
}
63+
return &AdGuardMatcher{set}, nil
64+
}
65+
66+
func (m *AdGuardMatcher) Write(writer varbin.Writer) error {
67+
return m.set.Write(writer)
68+
}
69+
70+
func (m *AdGuardMatcher) Match(domain string) bool {
71+
key := reverseDomain(domain)
72+
if m.has([]byte(key), 0, 0) {
73+
return true
74+
}
75+
for {
76+
if m.has([]byte(string(suffixLabel)+key), 0, 0) {
77+
return true
78+
}
79+
idx := strings.IndexByte(key, '.')
80+
if idx == -1 {
81+
return false
82+
}
83+
key = key[idx+1:]
84+
}
85+
}
86+
87+
func (m *AdGuardMatcher) has(key []byte, nodeId, bmIdx int) bool {
88+
for i := 0; i < len(key); i++ {
89+
currentChar := key[i]
90+
for ; ; bmIdx++ {
91+
if getBit(m.set.labelBitmap, bmIdx) != 0 {
92+
return false
93+
}
94+
nextLabel := m.set.labels[bmIdx-nodeId]
95+
if nextLabel == prefixLabel {
96+
return true
97+
}
98+
if nextLabel == rootLabel {
99+
nextNodeId := countZeros(m.set.labelBitmap, m.set.ranks, bmIdx+1)
100+
hasNext := getBit(m.set.leaves, nextNodeId) != 0
101+
if currentChar == '.' && hasNext {
102+
return true
103+
}
104+
}
105+
if nextLabel == currentChar {
106+
break
107+
}
108+
if nextLabel == anyLabel {
109+
idx := bytes.IndexRune(key[i:], '.')
110+
nextNodeId := countZeros(m.set.labelBitmap, m.set.ranks, bmIdx+1)
111+
if idx == -1 {
112+
if getBit(m.set.leaves, nextNodeId) != 0 {
113+
return true
114+
}
115+
idx = 0
116+
}
117+
nextBmIdx := selectIthOne(m.set.labelBitmap, m.set.ranks, m.set.selects, nextNodeId-1) + 1
118+
if m.has(key[i+idx:], nextNodeId, nextBmIdx) {
119+
return true
120+
}
121+
}
122+
}
123+
nodeId = countZeros(m.set.labelBitmap, m.set.ranks, bmIdx+1)
124+
bmIdx = selectIthOne(m.set.labelBitmap, m.set.ranks, m.set.selects, nodeId-1) + 1
125+
}
126+
if getBit(m.set.leaves, nodeId) != 0 {
127+
return true
128+
}
129+
for ; ; bmIdx++ {
130+
if getBit(m.set.labelBitmap, bmIdx) != 0 {
131+
return false
132+
}
133+
nextLabel := m.set.labels[bmIdx-nodeId]
134+
if nextLabel == prefixLabel || nextLabel == rootLabel {
135+
return true
136+
}
137+
}
138+
}
139+
140+
func (m *AdGuardMatcher) Dump() (ruleLines []string) {
141+
for _, key := range m.set.keys() {
142+
key = reverseDomain(key)
143+
var (
144+
isSuffix bool
145+
hasStart bool
146+
hasEnd bool
147+
)
148+
if key[0] == prefixLabel {
149+
key = key[1:]
150+
} else if key[0] == rootLabel {
151+
key = key[1:]
152+
isSuffix = true
153+
} else {
154+
hasStart = true
155+
}
156+
if key[len(key)-1] == suffixLabel {
157+
key = key[:len(key)-1]
158+
} else {
159+
hasEnd = true
160+
}
161+
if isSuffix {
162+
key = "||" + key
163+
} else if hasStart {
164+
key = "|" + key
165+
}
166+
if hasEnd {
167+
key += "^"
168+
}
169+
ruleLines = append(ruleLines, key)
170+
}
171+
return
172+
}

common/domain/matcher.go

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package domain
22

33
import (
4-
"encoding/binary"
54
"sort"
65
"unicode/utf8"
76

87
"github.com/sagernet/sing/common/varbin"
98
)
109

10+
const (
11+
prefixLabel = '\r'
12+
rootLabel = '\n'
13+
)
14+
1115
type Matcher struct {
1216
set *succinctSet
1317
}
@@ -21,16 +25,16 @@ func NewMatcher(domains []string, domainSuffix []string, generateLegacy bool) *M
2125
}
2226
seen[domain] = true
2327
if domain[0] == '.' {
24-
domainList = append(domainList, reverseDomainSuffix(domain))
28+
domainList = append(domainList, reverseDomain(string(prefixLabel)+domain))
2529
} else if generateLegacy {
2630
domainList = append(domainList, reverseDomain(domain))
2731
suffixDomain := "." + domain
2832
if !seen[suffixDomain] {
2933
seen[suffixDomain] = true
30-
domainList = append(domainList, reverseDomainSuffix(suffixDomain))
34+
domainList = append(domainList, reverseDomain(string(prefixLabel)+suffixDomain))
3135
}
3236
} else {
33-
domainList = append(domainList, reverseDomainRoot(domain))
37+
domainList = append(domainList, reverseDomain(string(rootLabel)+domain))
3438
}
3539
}
3640
for _, domain := range domains {
@@ -44,38 +48,60 @@ func NewMatcher(domains []string, domainSuffix []string, generateLegacy bool) *M
4448
return &Matcher{newSuccinctSet(domainList)}
4549
}
4650

47-
type matcherData struct {
48-
Version uint8
49-
Leaves []uint64
50-
LabelBitmap []uint64
51-
Labels []byte
52-
}
53-
5451
func ReadMatcher(reader varbin.Reader) (*Matcher, error) {
55-
matcher, err := varbin.ReadValue[matcherData](reader, binary.BigEndian)
52+
set, err := readSuccinctSet(reader)
5653
if err != nil {
5754
return nil, err
5855
}
59-
set := &succinctSet{
60-
leaves: matcher.Leaves,
61-
labelBitmap: matcher.LabelBitmap,
62-
labels: matcher.Labels,
63-
}
64-
set.init()
6556
return &Matcher{set}, nil
6657
}
6758

59+
func (m *Matcher) Write(writer varbin.Writer) error {
60+
return m.set.Write(writer)
61+
}
62+
6863
func (m *Matcher) Match(domain string) bool {
69-
return m.set.Has(reverseDomain(domain))
64+
return m.has(reverseDomain(domain))
7065
}
7166

72-
func (m *Matcher) Write(writer varbin.Writer) error {
73-
return varbin.Write(writer, binary.BigEndian, matcherData{
74-
Version: 1,
75-
Leaves: m.set.leaves,
76-
LabelBitmap: m.set.labelBitmap,
77-
Labels: m.set.labels,
78-
})
67+
func (m *Matcher) has(key string) bool {
68+
var nodeId, bmIdx int
69+
for i := 0; i < len(key); i++ {
70+
currentChar := key[i]
71+
for ; ; bmIdx++ {
72+
if getBit(m.set.labelBitmap, bmIdx) != 0 {
73+
return false
74+
}
75+
nextLabel := m.set.labels[bmIdx-nodeId]
76+
if nextLabel == prefixLabel {
77+
return true
78+
}
79+
if nextLabel == rootLabel {
80+
nextNodeId := countZeros(m.set.labelBitmap, m.set.ranks, bmIdx+1)
81+
hasNext := getBit(m.set.leaves, nextNodeId) != 0
82+
if currentChar == '.' && hasNext {
83+
return true
84+
}
85+
}
86+
if nextLabel == currentChar {
87+
break
88+
}
89+
}
90+
nodeId = countZeros(m.set.labelBitmap, m.set.ranks, bmIdx+1)
91+
bmIdx = selectIthOne(m.set.labelBitmap, m.set.ranks, m.set.selects, nodeId-1) + 1
92+
}
93+
if getBit(m.set.leaves, nodeId) != 0 {
94+
return true
95+
}
96+
for ; ; bmIdx++ {
97+
if getBit(m.set.labelBitmap, bmIdx) != 0 {
98+
return false
99+
}
100+
nextLabel := m.set.labels[bmIdx-nodeId]
101+
if nextLabel == prefixLabel || nextLabel == rootLabel {
102+
return true
103+
}
104+
}
79105
}
80106

81107
func (m *Matcher) Dump() (domainList []string, prefixList []string) {
@@ -119,27 +145,3 @@ func reverseDomain(domain string) string {
119145
}
120146
return string(b)
121147
}
122-
123-
func reverseDomainSuffix(domain string) string {
124-
l := len(domain)
125-
b := make([]byte, l+1)
126-
for i := 0; i < l; {
127-
r, n := utf8.DecodeRuneInString(domain[i:])
128-
i += n
129-
utf8.EncodeRune(b[l-i:], r)
130-
}
131-
b[l] = prefixLabel
132-
return string(b)
133-
}
134-
135-
func reverseDomainRoot(domain string) string {
136-
l := len(domain)
137-
b := make([]byte, l+1)
138-
for i := 0; i < l; {
139-
r, n := utf8.DecodeRuneInString(domain[i:])
140-
i += n
141-
utf8.EncodeRune(b[l-i:], r)
142-
}
143-
b[l] = rootLabel
144-
return string(b)
145-
}

0 commit comments

Comments
 (0)