xref: /aosp_15_r20/build/blueprint/pathtools/glob.go (revision 1fa6dee971e1612fa5cc0aa5ca2d35a22e2c34a3)
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