1
2
3
4
5 package filepath
6
7 import (
8 "errors"
9 "internal/filepathlite"
10 "os"
11 "runtime"
12 "slices"
13 "strings"
14 "unicode/utf8"
15 )
16
17
18 var ErrBadPattern = errors.New("syntax error in pattern")
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 func Match(pattern, name string) (matched bool, err error) {
45 Pattern:
46 for len(pattern) > 0 {
47 var star bool
48 var chunk string
49 star, chunk, pattern = scanChunk(pattern)
50 if star && chunk == "" {
51
52 return !strings.Contains(name, string(Separator)), nil
53 }
54
55 t, ok, err := matchChunk(chunk, name)
56
57
58
59 if ok && (len(t) == 0 || len(pattern) > 0) {
60 name = t
61 continue
62 }
63 if err != nil {
64 return false, err
65 }
66 if star {
67
68
69 for i := 0; i < len(name) && name[i] != Separator; i++ {
70 t, ok, err := matchChunk(chunk, name[i+1:])
71 if ok {
72
73 if len(pattern) == 0 && len(t) > 0 {
74 continue
75 }
76 name = t
77 continue Pattern
78 }
79 if err != nil {
80 return false, err
81 }
82 }
83 }
84 return false, nil
85 }
86 return len(name) == 0, nil
87 }
88
89
90
91 func scanChunk(pattern string) (star bool, chunk, rest string) {
92 for len(pattern) > 0 && pattern[0] == '*' {
93 pattern = pattern[1:]
94 star = true
95 }
96 inrange := false
97 for i := 0; i < len(pattern); i++ {
98 switch pattern[i] {
99 case '\\':
100
101 if runtime.GOOS != "windows" && i+1 < len(pattern) {
102 i++
103 }
104 case '[':
105 inrange = true
106 case ']':
107 inrange = false
108 case '*':
109 if !inrange {
110 return star, pattern[:i], pattern[i:]
111 }
112 }
113 }
114 return star, pattern, ""
115 }
116
117
118
119
120 func matchChunk(chunk, s string) (rest string, ok bool, err error) {
121
122
123
124 failed := false
125 for len(chunk) > 0 {
126 failed = failed || len(s) == 0
127 switch chunk[0] {
128 case '[':
129
130 var r rune
131 if !failed {
132 var n int
133 r, n = utf8.DecodeRuneInString(s)
134 s = s[n:]
135 }
136 chunk = chunk[1:]
137
138 negated := false
139 if len(chunk) > 0 && chunk[0] == '^' {
140 negated = true
141 chunk = chunk[1:]
142 }
143
144 match := false
145 nrange := 0
146 for {
147 if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
148 chunk = chunk[1:]
149 break
150 }
151 var lo, hi rune
152 if lo, chunk, err = getEsc(chunk); err != nil {
153 return "", false, err
154 }
155 hi = lo
156 if chunk[0] == '-' {
157 if hi, chunk, err = getEsc(chunk[1:]); err != nil {
158 return "", false, err
159 }
160 }
161 match = match || lo <= r && r <= hi
162 nrange++
163 }
164 failed = failed || match == negated
165
166 case '?':
167 if !failed {
168 failed = s[0] == Separator
169 _, n := utf8.DecodeRuneInString(s)
170 s = s[n:]
171 }
172 chunk = chunk[1:]
173
174 case '\\':
175 if runtime.GOOS != "windows" {
176 chunk = chunk[1:]
177 if len(chunk) == 0 {
178 return "", false, ErrBadPattern
179 }
180 }
181 fallthrough
182
183 default:
184 if !failed {
185 failed = chunk[0] != s[0]
186 s = s[1:]
187 }
188 chunk = chunk[1:]
189 }
190 }
191 if failed {
192 return "", false, nil
193 }
194 return s, true, nil
195 }
196
197
198 func getEsc(chunk string) (r rune, nchunk string, err error) {
199 if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
200 err = ErrBadPattern
201 return
202 }
203 if chunk[0] == '\\' && runtime.GOOS != "windows" {
204 chunk = chunk[1:]
205 if len(chunk) == 0 {
206 err = ErrBadPattern
207 return
208 }
209 }
210 r, n := utf8.DecodeRuneInString(chunk)
211 if r == utf8.RuneError && n == 1 {
212 err = ErrBadPattern
213 }
214 nchunk = chunk[n:]
215 if len(nchunk) == 0 {
216 err = ErrBadPattern
217 }
218 return
219 }
220
221
222
223
224
225
226
227
228
229 func Glob(pattern string) (matches []string, err error) {
230 return globWithLimit(pattern, 0)
231 }
232
233 func globWithLimit(pattern string, depth int) (matches []string, err error) {
234
235 const pathSeparatorsLimit = 10000
236 if depth == pathSeparatorsLimit {
237 return nil, ErrBadPattern
238 }
239
240
241 if _, err := Match(pattern, ""); err != nil {
242 return nil, err
243 }
244 if !hasMeta(pattern) {
245 if _, err = os.Lstat(pattern); err != nil {
246 return nil, nil
247 }
248 return []string{pattern}, nil
249 }
250
251 dir, file := Split(pattern)
252 volumeLen := 0
253 if runtime.GOOS == "windows" {
254 volumeLen, dir = cleanGlobPathWindows(dir)
255 } else {
256 dir = cleanGlobPath(dir)
257 }
258
259 if !hasMeta(dir[volumeLen:]) {
260 return glob(dir, file, nil)
261 }
262
263
264 if dir == pattern {
265 return nil, ErrBadPattern
266 }
267
268 var m []string
269 m, err = globWithLimit(dir, depth+1)
270 if err != nil {
271 return
272 }
273 for _, d := range m {
274 matches, err = glob(d, file, matches)
275 if err != nil {
276 return
277 }
278 }
279 return
280 }
281
282
283 func cleanGlobPath(path string) string {
284 switch path {
285 case "":
286 return "."
287 case string(Separator):
288
289 return path
290 default:
291 return path[0 : len(path)-1]
292 }
293 }
294
295
296 func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) {
297 vollen := filepathlite.VolumeNameLen(path)
298 switch {
299 case path == "":
300 return 0, "."
301 case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]):
302
303 return vollen + 1, path
304 case vollen == len(path) && len(path) == 2:
305 return vollen, path + "."
306 default:
307 if vollen >= len(path) {
308 vollen = len(path) - 1
309 }
310 return vollen, path[0 : len(path)-1]
311 }
312 }
313
314
315
316
317
318 func glob(dir, pattern string, matches []string) (m []string, e error) {
319 m = matches
320 fi, err := os.Stat(dir)
321 if err != nil {
322 return
323 }
324 if !fi.IsDir() {
325 return
326 }
327 d, err := os.Open(dir)
328 if err != nil {
329 return
330 }
331 defer d.Close()
332
333 names, _ := d.Readdirnames(-1)
334 slices.Sort(names)
335
336 for _, n := range names {
337 matched, err := Match(pattern, n)
338 if err != nil {
339 return m, err
340 }
341 if matched {
342 m = append(m, Join(dir, n))
343 }
344 }
345 return
346 }
347
348
349
350 func hasMeta(path string) bool {
351 magicChars := `*?[`
352 if runtime.GOOS != "windows" {
353 magicChars = `*?[\`
354 }
355 return strings.ContainsAny(path, magicChars)
356 }
357
View as plain text