master
go 265 lines 5.51 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package matcher
4
5 import (
6 "errors"
7 "path/filepath"
8 "regexp"
9 "unicode/utf8"
10 )
11
12 // globMatcher implements Matcher, it uses filepath.MatchString to match.
13 type globMatcher string
14
15 var (
16 errBadGlobPattern = errors.New("bad glob pattern")
17 erGlobPattern = regexp.MustCompile(`(?s)^(?:[*?]|\[\^?([^\\-\]]|\\.|.-.)+]|\\.|[^*?\\\[])*$`)
18 )
19
20 // NewGlobMatcher create a new matcher with glob format
21 func NewGlobMatcher(expr string) (Matcher, error) {
22 switch expr {
23 case "":
24 return stringFullMatcher(""), nil
25 case "*":
26 return TRUE(), nil
27 }
28
29 // any strings pass this regexp check are valid pattern
30 if !erGlobPattern.MatchString(expr) {
31 return nil, errBadGlobPattern
32 }
33
34 size := len(expr)
35 chars := []rune(expr)
36 startWith := true
37 endWith := true
38 startIdx := 0
39 endIdx := size - 1
40 if chars[startIdx] == '*' {
41 startWith = false
42 startIdx = 1
43 }
44 if chars[endIdx] == '*' {
45 endWith = false
46 endIdx--
47 }
48
49 unescapedExpr := make([]rune, 0, endIdx-startIdx+1)
50 for i := startIdx; i <= endIdx; i++ {
51 ch := chars[i]
52 if ch == '\\' {
53 nextCh := chars[i+1]
54 unescapedExpr = append(unescapedExpr, nextCh)
55 i++
56 } else if isGlobMeta(ch) {
57 return globMatcher(expr), nil
58 } else {
59 unescapedExpr = append(unescapedExpr, ch)
60 }
61 }
62
63 return NewStringMatcher(string(unescapedExpr), startWith, endWith)
64 }
65
66 func isGlobMeta(ch rune) bool {
67 switch ch {
68 case '*', '?', '[':
69 return true
70 default:
71 return false
72 }
73 }
74
75 // Match matches.
76 func (m globMatcher) Match(b []byte) bool {
77 return m.MatchString(string(b))
78 }
79
80 // MatchString matches.
81 func (m globMatcher) MatchString(line string) bool {
82 rs, _ := m.globMatch(line)
83 return rs
84 }
85
86 func (m globMatcher) globMatch(name string) (matched bool, err error) {
87 pattern := string(m)
88 Pattern:
89 for len(pattern) > 0 {
90 var star bool
91 var chunk string
92 star, chunk, pattern = scanChunk(pattern)
93 if star && chunk == "" {
94 // Trailing * matches rest of string unless it has a /.
95 // return !strings.Contains(name, string(Separator)), nil
96
97 return true, nil
98 }
99 // Look for match at current position.
100 t, ok, err := matchChunk(chunk, name)
101 // if we're the last chunk, make sure we've exhausted the name
102 // otherwise we'll give a false result even if we could still match
103 // using the star
104 if ok && (len(t) == 0 || len(pattern) > 0) {
105 name = t
106 continue
107 }
108 if err != nil {
109 return false, err
110 }
111 if star {
112 // Look for match skipping i+1 bytes.
113 // Cannot skip /.
114 for i := 0; i < len(name); i++ {
115 //for i := 0; i < len(name) && name[i] != Separator; i++ {
116 t, ok, err := matchChunk(chunk, name[i+1:])
117 if ok {
118 // if we're the last chunk, make sure we exhausted the name
119 if len(pattern) == 0 && len(t) > 0 {
120 continue
121 }
122 name = t
123 continue Pattern
124 }
125 if err != nil {
126 return false, err
127 }
128 }
129 }
130 return false, nil
131 }
132 return len(name) == 0, nil
133 }
134
135 // scanChunk gets the next segment of pattern, which is a non-star string
136 // possibly preceded by a star.
137 func scanChunk(pattern string) (star bool, chunk, rest string) {
138 for len(pattern) > 0 && pattern[0] == '*' {
139 pattern = pattern[1:]
140 star = true
141 }
142 inrange := false
143 var i int
144 Scan:
145 for i = 0; i < len(pattern); i++ {
146 switch pattern[i] {
147 case '\\':
148 if i+1 < len(pattern) {
149 i++
150 }
151 case '[':
152 inrange = true
153 case ']':
154 inrange = false
155 case '*':
156 if !inrange {
157 break Scan
158 }
159 }
160 }
161 return star, pattern[0:i], pattern[i:]
162 }
163
164 // matchChunk checks whether chunk matches the beginning of s.
165 // If so, it returns the remainder of s (after the match).
166 // Chunk is all single-character operators: literals, char classes, and ?.
167 func matchChunk(chunk, s string) (rest string, ok bool, err error) {
168 for len(chunk) > 0 {
169 if len(s) == 0 {
170 return
171 }
172 switch chunk[0] {
173 case '[':
174 // character class
175 r, n := utf8.DecodeRuneInString(s)
176 s = s[n:]
177 chunk = chunk[1:]
178 // We can't end right after '[', we're expecting at least
179 // a closing bracket and possibly a caret.
180 if len(chunk) == 0 {
181 err = filepath.ErrBadPattern
182 return
183 }
184 // possibly negated
185 negated := chunk[0] == '^'
186 if negated {
187 chunk = chunk[1:]
188 }
189 // parse all ranges
190 match := false
191 nrange := 0
192 for {
193 if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
194 chunk = chunk[1:]
195 break
196 }
197 var lo, hi rune
198 if lo, chunk, err = getEsc(chunk); err != nil {
199 return
200 }
201 hi = lo
202 if chunk[0] == '-' {
203 if hi, chunk, err = getEsc(chunk[1:]); err != nil {
204 return
205 }
206 }
207 if lo <= r && r <= hi {
208 match = true
209 }
210 nrange++
211 }
212 if match == negated {
213 return
214 }
215
216 case '?':
217 //if s[0] == Separator {
218 // return
219 //}
220 _, n := utf8.DecodeRuneInString(s)
221 s = s[n:]
222 chunk = chunk[1:]
223
224 case '\\':
225 chunk = chunk[1:]
226 if len(chunk) == 0 {
227 err = filepath.ErrBadPattern
228 return
229 }
230 fallthrough
231
232 default:
233 if chunk[0] != s[0] {
234 return
235 }
236 s = s[1:]
237 chunk = chunk[1:]
238 }
239 }
240 return s, true, nil
241 }
242
243 // getEsc gets a possibly-escaped character from chunk, for a character class.
244 func getEsc(chunk string) (r rune, nchunk string, err error) {
245 if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
246 err = filepath.ErrBadPattern
247 return
248 }
249 if chunk[0] == '\\' {
250 chunk = chunk[1:]
251 if len(chunk) == 0 {
252 err = filepath.ErrBadPattern
253 return
254 }
255 }
256 r, n := utf8.DecodeRuneInString(chunk)
257 if r == utf8.RuneError && n == 1 {
258 err = filepath.ErrBadPattern
259 }
260 nchunk = chunk[n:]
261 if len(nchunk) == 0 {
262 err = filepath.ErrBadPattern
263 }
264 return
265 }