github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/path/filepath/match.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package filepath 6 7 import ( 8 "errors" 9 "os" 10 "runtime" 11 "sort" 12 "strings" 13 "unicode/utf8" 14 ) 15 16 // ErrBadPattern indicates a pattern was malformed. 17 var ErrBadPattern = errors.New("syntax error in pattern") 18 19 // Match reports whether name matches the shell file name pattern. 20 // The pattern syntax is: 21 // 22 // pattern: 23 // { term } 24 // term: 25 // '*' matches any sequence of non-Separator characters 26 // '?' matches any single non-Separator character 27 // '[' [ '^' ] { character-range } ']' 28 // character class (must be non-empty) 29 // c matches character c (c != '*', '?', '\\', '[') 30 // '\\' c matches character c 31 // 32 // character-range: 33 // c matches character c (c != '\\', '-', ']') 34 // '\\' c matches character c 35 // lo '-' hi matches character c for lo <= c <= hi 36 // 37 // Match requires pattern to match all of name, not just a substring. 38 // The only possible returned error is ErrBadPattern, when pattern 39 // is malformed. 40 // 41 // On Windows, escaping is disabled. Instead, '\\' is treated as 42 // path separator. 43 func Match(pattern, name string) (matched bool, err error) { 44 Pattern: 45 for len(pattern) > 0 { 46 var star bool 47 var chunk string 48 star, chunk, pattern = scanChunk(pattern) 49 if star && chunk == "" { 50 // Trailing * matches rest of string unless it has a /. 51 return !strings.Contains(name, string(Separator)), nil 52 } 53 // Look for match at current position. 54 t, ok, err := matchChunk(chunk, name) 55 // if we're the last chunk, make sure we've exhausted the name 56 // otherwise we'll give a false result even if we could still match 57 // using the star 58 if ok && (len(t) == 0 || len(pattern) > 0) { 59 name = t 60 continue 61 } 62 if err != nil { 63 return false, err 64 } 65 if star { 66 // Look for match skipping i+1 bytes. 67 // Cannot skip /. 68 for i := 0; i < len(name) && name[i] != Separator; i++ { 69 t, ok, err := matchChunk(chunk, name[i+1:]) 70 if ok { 71 // if we're the last chunk, make sure we exhausted the name 72 if len(pattern) == 0 && len(t) > 0 { 73 continue 74 } 75 name = t 76 continue Pattern 77 } 78 if err != nil { 79 return false, err 80 } 81 } 82 } 83 return false, nil 84 } 85 return len(name) == 0, nil 86 } 87 88 // scanChunk gets the next segment of pattern, which is a non-star string 89 // possibly preceded by a star. 90 func scanChunk(pattern string) (star bool, chunk, rest string) { 91 for len(pattern) > 0 && pattern[0] == '*' { 92 pattern = pattern[1:] 93 star = true 94 } 95 inrange := false 96 var i int 97 Scan: 98 for i = 0; i < len(pattern); i++ { 99 switch pattern[i] { 100 case '\\': 101 if runtime.GOOS != "windows" { 102 // error check handled in matchChunk: bad pattern. 103 if i+1 < len(pattern) { 104 i++ 105 } 106 } 107 case '[': 108 inrange = true 109 case ']': 110 inrange = false 111 case '*': 112 if !inrange { 113 break Scan 114 } 115 } 116 } 117 return star, pattern[0:i], pattern[i:] 118 } 119 120 // matchChunk checks whether chunk matches the beginning of s. 121 // If so, it returns the remainder of s (after the match). 122 // Chunk is all single-character operators: literals, char classes, and ?. 123 func matchChunk(chunk, s string) (rest string, ok bool, err error) { 124 // failed records whether the match has failed. 125 // After the match fails, the loop continues on processing chunk, 126 // checking that the pattern is well-formed but no longer reading s. 127 failed := false 128 for len(chunk) > 0 { 129 if !failed && len(s) == 0 { 130 failed = true 131 } 132 switch chunk[0] { 133 case '[': 134 // character class 135 var r rune 136 if !failed { 137 var n int 138 r, n = utf8.DecodeRuneInString(s) 139 s = s[n:] 140 } 141 chunk = chunk[1:] 142 // possibly negated 143 negated := false 144 if len(chunk) > 0 && chunk[0] == '^' { 145 negated = true 146 chunk = chunk[1:] 147 } 148 // parse all ranges 149 match := false 150 nrange := 0 151 for { 152 if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { 153 chunk = chunk[1:] 154 break 155 } 156 var lo, hi rune 157 if lo, chunk, err = getEsc(chunk); err != nil { 158 return "", false, err 159 } 160 hi = lo 161 if chunk[0] == '-' { 162 if hi, chunk, err = getEsc(chunk[1:]); err != nil { 163 return "", false, err 164 } 165 } 166 if lo <= r && r <= hi { 167 match = true 168 } 169 nrange++ 170 } 171 if match == negated { 172 failed = true 173 } 174 175 case '?': 176 if !failed { 177 if s[0] == Separator { 178 failed = true 179 } 180 _, n := utf8.DecodeRuneInString(s) 181 s = s[n:] 182 } 183 chunk = chunk[1:] 184 185 case '\\': 186 if runtime.GOOS != "windows" { 187 chunk = chunk[1:] 188 if len(chunk) == 0 { 189 return "", false, ErrBadPattern 190 } 191 } 192 fallthrough 193 194 default: 195 if !failed { 196 if chunk[0] != s[0] { 197 failed = true 198 } 199 s = s[1:] 200 } 201 chunk = chunk[1:] 202 } 203 } 204 if failed { 205 return "", false, nil 206 } 207 return s, true, nil 208 } 209 210 // getEsc gets a possibly-escaped character from chunk, for a character class. 211 func getEsc(chunk string) (r rune, nchunk string, err error) { 212 if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' { 213 err = ErrBadPattern 214 return 215 } 216 if chunk[0] == '\\' && runtime.GOOS != "windows" { 217 chunk = chunk[1:] 218 if len(chunk) == 0 { 219 err = ErrBadPattern 220 return 221 } 222 } 223 r, n := utf8.DecodeRuneInString(chunk) 224 if r == utf8.RuneError && n == 1 { 225 err = ErrBadPattern 226 } 227 nchunk = chunk[n:] 228 if len(nchunk) == 0 { 229 err = ErrBadPattern 230 } 231 return 232 } 233 234 // Glob returns the names of all files matching pattern or nil 235 // if there is no matching file. The syntax of patterns is the same 236 // as in Match. The pattern may describe hierarchical names such as 237 // /usr/*/bin/ed (assuming the Separator is '/'). 238 // 239 // Glob ignores file system errors such as I/O errors reading directories. 240 // The only possible returned error is ErrBadPattern, when pattern 241 // is malformed. 242 func Glob(pattern string) (matches []string, err error) { 243 return globWithLimit(pattern, 0) 244 } 245 246 func globWithLimit(pattern string, depth int) (matches []string, err error) { 247 // This limit is used prevent stack exhaustion issues. See CVE-2022-30632. 248 const pathSeparatorsLimit = 10000 249 if depth == pathSeparatorsLimit { 250 return nil, ErrBadPattern 251 } 252 253 // Check pattern is well-formed. 254 if _, err := Match(pattern, ""); err != nil { 255 return nil, err 256 } 257 if !hasMeta(pattern) { 258 if _, err = os.Lstat(pattern); err != nil { 259 return nil, nil 260 } 261 return []string{pattern}, nil 262 } 263 264 dir, file := Split(pattern) 265 volumeLen := 0 266 if runtime.GOOS == "windows" { 267 volumeLen, dir = cleanGlobPathWindows(dir) 268 } else { 269 dir = cleanGlobPath(dir) 270 } 271 272 if !hasMeta(dir[volumeLen:]) { 273 return glob(dir, file, nil) 274 } 275 276 // Prevent infinite recursion. See issue 15879. 277 if dir == pattern { 278 return nil, ErrBadPattern 279 } 280 281 var m []string 282 m, err = globWithLimit(dir, depth+1) 283 if err != nil { 284 return 285 } 286 for _, d := range m { 287 matches, err = glob(d, file, matches) 288 if err != nil { 289 return 290 } 291 } 292 return 293 } 294 295 // cleanGlobPath prepares path for glob matching. 296 func cleanGlobPath(path string) string { 297 switch path { 298 case "": 299 return "." 300 case string(Separator): 301 // do nothing to the path 302 return path 303 default: 304 return path[0 : len(path)-1] // chop off trailing separator 305 } 306 } 307 308 // cleanGlobPathWindows is windows version of cleanGlobPath. 309 func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) { 310 vollen := volumeNameLen(path) 311 switch { 312 case path == "": 313 return 0, "." 314 case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /, \, C:\ and C:/ 315 // do nothing to the path 316 return vollen + 1, path 317 case vollen == len(path) && len(path) == 2: // C: 318 return vollen, path + "." // convert C: into C:. 319 default: 320 if vollen >= len(path) { 321 vollen = len(path) - 1 322 } 323 return vollen, path[0 : len(path)-1] // chop off trailing separator 324 } 325 } 326 327 // glob searches for files matching pattern in the directory dir 328 // and appends them to matches. If the directory cannot be 329 // opened, it returns the existing matches. New matches are 330 // added in lexicographical order. 331 func glob(dir, pattern string, matches []string) (m []string, e error) { 332 m = matches 333 fi, err := os.Stat(dir) 334 if err != nil { 335 return // ignore I/O error 336 } 337 if !fi.IsDir() { 338 return // ignore I/O error 339 } 340 d, err := os.Open(dir) 341 if err != nil { 342 return // ignore I/O error 343 } 344 defer d.Close() 345 346 names, _ := d.Readdirnames(-1) 347 sort.Strings(names) 348 349 for _, n := range names { 350 matched, err := Match(pattern, n) 351 if err != nil { 352 return m, err 353 } 354 if matched { 355 m = append(m, Join(dir, n)) 356 } 357 } 358 return 359 } 360 361 // hasMeta reports whether path contains any of the magic characters 362 // recognized by Match. 363 func hasMeta(path string) bool { 364 magicChars := `*?[` 365 if runtime.GOOS != "windows" { 366 magicChars = `*?[\` 367 } 368 return strings.ContainsAny(path, magicChars) 369 }