Hunter0x7c7
2022-08-11 a82f9cb69f63aaeba40c024960deda7d75b9fece
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package strmatcher
 
import (
    "container/list"
)
 
const (
    acValidCharCount = 38 // aA-zZ (26), 0-9 (10), - (1), . (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 {
    var suffixMatches [][]uint32
    var substrMatches [][]uint32
    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])
        }
    }
    switch matches := append(substrMatches, suffixMatches...); len(matches) { // nolint: gocritic
    case 0:
        return nil
    case 1:
        return matches[0]
    default:
        result := []uint32{}
        for i := len(matches) - 1; i >= 0; i-- {
            result = append(result, matches[i]...)
        }
        return result
    }
}
 
// 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 = []int{
    'A': 0,
    'a': 0,
    'B': 1,
    'b': 1,
    'C': 2,
    'c': 2,
    'D': 3,
    'd': 3,
    'E': 4,
    'e': 4,
    'F': 5,
    'f': 5,
    'G': 6,
    'g': 6,
    'H': 7,
    'h': 7,
    'I': 8,
    'i': 8,
    'J': 9,
    'j': 9,
    'K': 10,
    'k': 10,
    'L': 11,
    'l': 11,
    'M': 12,
    'm': 12,
    'N': 13,
    'n': 13,
    'O': 14,
    'o': 14,
    'P': 15,
    'p': 15,
    'Q': 16,
    'q': 16,
    'R': 17,
    'r': 17,
    'S': 18,
    's': 18,
    'T': 19,
    't': 19,
    'U': 20,
    'u': 20,
    'V': 21,
    'v': 21,
    'W': 22,
    'w': 22,
    'X': 23,
    'x': 23,
    'Y': 24,
    'y': 24,
    'Z': 25,
    'z': 25,
    '-': 26,
    '.': 27,
    '0': 28,
    '1': 29,
    '2': 30,
    '3': 31,
    '4': 32,
    '5': 33,
    '6': 34,
    '7': 35,
    '8': 36,
    '9': 37,
}