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