1// Copyright 2014 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package pathtools 16 17import ( 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "path/filepath" 24 "slices" 25 "strings" 26) 27 28var GlobMultipleRecursiveErr = errors.New("pattern contains multiple '**'") 29var GlobLastRecursiveErr = errors.New("pattern has '**' as last path element") 30var GlobInvalidRecursiveErr = errors.New("pattern contains other characters between '**' and path separator") 31 32// GlobResult is a container holding the results of a call to Glob. 33type GlobResult struct { 34 // Pattern is the pattern that was passed to Glob. 35 Pattern string 36 // Excludes is the list of excludes that were passed to Glob. 37 Excludes []string 38 39 // Matches is the list of files or directories that matched the pattern but not the excludes. 40 Matches []string 41 42 // Deps is the list of files or directories that must be depended on to regenerate the glob. 43 Deps []string 44} 45 46// FileList returns the list of files matched by a glob for writing to an output file. 47func (result GlobResult) FileList() []byte { 48 return []byte(strings.Join(result.Matches, "\n") + "\n") 49} 50 51func (result GlobResult) Clone() GlobResult { 52 return GlobResult{ 53 Pattern: result.Pattern, 54 Excludes: slices.Clone(result.Excludes), 55 Matches: slices.Clone(result.Matches), 56 Deps: slices.Clone(result.Deps), 57 } 58} 59 60// MultipleGlobResults is a list of GlobResult structs. 61type MultipleGlobResults []GlobResult 62 63// FileList returns the list of files matched by a list of multiple globs for writing to an output file. 64func (results MultipleGlobResults) FileList() []byte { 65 multipleMatches := make([][]string, len(results)) 66 for i, result := range results { 67 multipleMatches[i] = result.Matches 68 } 69 buf, err := json.Marshal(multipleMatches) 70 if err != nil { 71 panic(fmt.Errorf("failed to marshal glob results to json: %w", err)) 72 } 73 return buf 74} 75 76// Deps returns the deps from all of the GlobResults. 77func (results MultipleGlobResults) Deps() []string { 78 var deps []string 79 for _, result := range results { 80 deps = append(deps, result.Deps...) 81 } 82 return deps 83} 84 85// Glob returns the list of files and directories that match the given pattern 86// but do not match the given exclude patterns, along with the list of 87// directories and other dependencies that were searched to construct the file 88// list. The supported glob and exclude patterns are equivalent to 89// filepath.Glob, with an extension that recursive glob (** matching zero or 90// more complete path entries) is supported. Any directories in the matches 91// list will have a '/' suffix. 92// 93// In general ModuleContext.GlobWithDeps or SingletonContext.GlobWithDeps 94// should be used instead, as they will automatically set up dependencies 95// to rerun the primary builder when the list of matching files changes. 96func Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (GlobResult, error) { 97 return startGlob(OsFs, pattern, excludes, follow) 98} 99 100func startGlob(fs FileSystem, pattern string, excludes []string, 101 follow ShouldFollowSymlinks) (GlobResult, error) { 102 103 if filepath.Base(pattern) == "**" { 104 return GlobResult{}, GlobLastRecursiveErr 105 } 106 107 matches, deps, err := glob(fs, pattern, false, follow) 108 109 if err != nil { 110 return GlobResult{}, err 111 } 112 113 matches, err = filterExcludes(matches, excludes) 114 if err != nil { 115 return GlobResult{}, err 116 } 117 118 // If the pattern has wildcards, we added dependencies on the 119 // containing directories to know about changes. 120 // 121 // If the pattern didn't have wildcards, and didn't find matches, the 122 // most specific found directories were added. 123 // 124 // But if it didn't have wildcards, and did find a match, no 125 // dependencies were added, so add the match itself to detect when it 126 // is removed. 127 if !isWild(pattern) { 128 deps = append(deps, matches...) 129 } 130 131 for i, match := range matches { 132 var info os.FileInfo 133 if follow == DontFollowSymlinks { 134 info, err = fs.Lstat(match) 135 } else { 136 info, err = fs.Stat(match) 137 if err != nil && os.IsNotExist(err) { 138 // ErrNotExist from Stat may be due to a dangling symlink, retry with lstat. 139 info, err = fs.Lstat(match) 140 } 141 } 142 if err != nil { 143 return GlobResult{}, err 144 } 145 146 if info.IsDir() { 147 matches[i] = match + "/" 148 } 149 } 150 151 return GlobResult{ 152 Pattern: pattern, 153 Excludes: excludes, 154 Matches: matches, 155 Deps: deps, 156 }, nil 157} 158 159// glob is a recursive helper function to handle globbing each level of the pattern individually, 160// allowing searched directories to be tracked. Also handles the recursive glob pattern, **. 161func glob(fs FileSystem, pattern string, hasRecursive bool, 162 follow ShouldFollowSymlinks) (matches, dirs []string, err error) { 163 164 if !isWild(pattern) { 165 // If there are no wilds in the pattern, check whether the file exists or not. 166 // Uses filepath.Glob instead of manually statting to get consistent results. 167 pattern = filepath.Clean(pattern) 168 matches, err = fs.glob(pattern) 169 if err != nil { 170 return matches, dirs, err 171 } 172 173 if len(matches) == 0 { 174 // Some part of the non-wild pattern didn't exist. Add the last existing directory 175 // as a dependency. 176 var matchDirs []string 177 for len(matchDirs) == 0 { 178 pattern = filepath.Dir(pattern) 179 matchDirs, err = fs.glob(pattern) 180 if err != nil { 181 return matches, dirs, err 182 } 183 } 184 dirs = append(dirs, matchDirs...) 185 } 186 return matches, dirs, err 187 } 188 189 dir, file := quickSplit(pattern) 190 191 if file == "**" { 192 if hasRecursive { 193 return matches, dirs, GlobMultipleRecursiveErr 194 } 195 hasRecursive = true 196 } else if strings.Contains(file, "**") { 197 return matches, dirs, GlobInvalidRecursiveErr 198 } 199 200 dirMatches, dirs, err := glob(fs, dir, hasRecursive, follow) 201 if err != nil { 202 return nil, nil, err 203 } 204 205 for _, m := range dirMatches { 206 isDir, err := fs.IsDir(m) 207 if os.IsNotExist(err) { 208 if isSymlink, _ := fs.IsSymlink(m); isSymlink { 209 return nil, nil, fmt.Errorf("dangling symlink: %s", m) 210 } 211 } 212 if err != nil { 213 return nil, nil, fmt.Errorf("unexpected error after glob: %s", err) 214 } 215 216 if isDir { 217 if file == "**" { 218 recurseDirs, err := fs.ListDirsRecursive(m, follow) 219 if err != nil { 220 return nil, nil, err 221 } 222 matches = append(matches, recurseDirs...) 223 } else { 224 dirs = append(dirs, m) 225 newMatches, err := fs.glob(filepath.Join(MatchEscape(m), file)) 226 if err != nil { 227 return nil, nil, err 228 } 229 if file[0] != '.' { 230 newMatches = filterDotFiles(newMatches) 231 } 232 matches = append(matches, newMatches...) 233 } 234 } 235 } 236 237 return matches, dirs, nil 238} 239 240// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations 241// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is 242// not "/". Returns ".", "" if path is "." 243func quickSplit(path string) (dir, file string) { 244 if path == "." { 245 return ".", "" 246 } 247 dir, file = filepath.Split(path) 248 switch dir { 249 case "": 250 dir = "." 251 case "/": 252 // Nothing 253 default: 254 dir = dir[:len(dir)-1] 255 } 256 return dir, file 257} 258 259func isWild(pattern string) bool { 260 return strings.ContainsAny(pattern, "*?[") 261} 262 263// Filters the strings in matches based on the glob patterns in excludes. Hierarchical (a/*) and 264// recursive (**) glob patterns are supported. 265func filterExcludes(matches []string, excludes []string) ([]string, error) { 266 if len(excludes) == 0 { 267 return matches, nil 268 } 269 270 var ret []string 271matchLoop: 272 for _, m := range matches { 273 for _, e := range excludes { 274 exclude, err := Match(e, m) 275 if err != nil { 276 return nil, err 277 } 278 if exclude { 279 continue matchLoop 280 } 281 } 282 ret = append(ret, m) 283 } 284 285 return ret, nil 286} 287 288// filterDotFiles filters out files that start with '.' 289func filterDotFiles(matches []string) []string { 290 ret := make([]string, 0, len(matches)) 291 292 for _, match := range matches { 293 _, name := filepath.Split(match) 294 if name[0] == '.' { 295 continue 296 } 297 ret = append(ret, match) 298 } 299 300 return ret 301} 302 303// Match returns true if name matches pattern using the same rules as filepath.Match, but supporting 304// recursive globs (**). 305func Match(pattern, name string) (bool, error) { 306 if filepath.Base(pattern) == "**" { 307 return false, GlobLastRecursiveErr 308 } 309 310 patternDir := pattern[len(pattern)-1] == '/' 311 nameDir := name[len(name)-1] == '/' 312 313 if patternDir != nameDir { 314 return false, nil 315 } 316 317 if nameDir { 318 name = name[:len(name)-1] 319 pattern = pattern[:len(pattern)-1] 320 } 321 322 for { 323 var patternFile, nameFile string 324 pattern, patternFile = filepath.Dir(pattern), filepath.Base(pattern) 325 326 if patternFile == "**" { 327 if strings.Contains(pattern, "**") { 328 return false, GlobMultipleRecursiveErr 329 } 330 // Test if the any prefix of name matches the part of the pattern before ** 331 for { 332 if name == "." || name == "/" { 333 return name == pattern, nil 334 } 335 if match, err := filepath.Match(pattern, name); err != nil { 336 return false, err 337 } else if match { 338 return true, nil 339 } 340 name = filepath.Dir(name) 341 } 342 } else if strings.Contains(patternFile, "**") { 343 return false, GlobInvalidRecursiveErr 344 } 345 346 name, nameFile = filepath.Dir(name), filepath.Base(name) 347 348 if nameFile == "." && patternFile == "." { 349 return true, nil 350 } else if nameFile == "/" && patternFile == "/" { 351 return true, nil 352 } else if nameFile == "." || patternFile == "." || nameFile == "/" || patternFile == "/" { 353 return false, nil 354 } 355 356 match, err := filepath.Match(patternFile, nameFile) 357 if err != nil || !match { 358 return match, err 359 } 360 } 361} 362 363// IsGlob returns true if the pattern contains any glob characters (*, ?, or [). 364func IsGlob(pattern string) bool { 365 return strings.IndexAny(pattern, "*?[") >= 0 366} 367 368// HasGlob returns true if any string in the list contains any glob characters (*, ?, or [). 369func HasGlob(in []string) bool { 370 for _, s := range in { 371 if IsGlob(s) { 372 return true 373 } 374 } 375 376 return false 377} 378 379// WriteFileIfChanged wraps ioutil.WriteFile, but only writes the file if 380// the files does not already exist with identical contents. This can be used 381// along with ninja restat rules to skip rebuilding downstream rules if no 382// changes were made by a rule. 383func WriteFileIfChanged(filename string, data []byte, perm os.FileMode) error { 384 var isChanged bool 385 386 dir := filepath.Dir(filename) 387 err := os.MkdirAll(dir, 0777) 388 if err != nil { 389 return err 390 } 391 392 info, err := os.Stat(filename) 393 if err != nil { 394 if os.IsNotExist(err) { 395 // The file does not exist yet. 396 isChanged = true 397 } else { 398 return err 399 } 400 } else { 401 if info.Size() != int64(len(data)) { 402 isChanged = true 403 } else { 404 oldData, err := ioutil.ReadFile(filename) 405 if err != nil { 406 return err 407 } 408 409 if len(oldData) != len(data) { 410 isChanged = true 411 } else { 412 for i := range data { 413 if oldData[i] != data[i] { 414 isChanged = true 415 break 416 } 417 } 418 } 419 } 420 } 421 422 if isChanged { 423 err = ioutil.WriteFile(filename, data, perm) 424 if err != nil { 425 return err 426 } 427 } 428 429 return nil 430} 431 432var matchEscaper = strings.NewReplacer( 433 `*`, `\*`, 434 `?`, `\?`, 435 `[`, `\[`, 436 `]`, `\]`, 437) 438 439// MatchEscape returns its inputs with characters that would be interpreted by 440func MatchEscape(s string) string { 441 return matchEscaper.Replace(s) 442} 443