mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 01:53:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			475 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			475 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package patternmatcher
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"text/scanner"
 | 
						|
	"unicode/utf8"
 | 
						|
)
 | 
						|
 | 
						|
// escapeBytes is a bitmap used to check whether a character should be escaped when creating the regex.
 | 
						|
var escapeBytes [8]byte
 | 
						|
 | 
						|
// shouldEscape reports whether a rune should be escaped as part of the regex.
 | 
						|
//
 | 
						|
// This only includes characters that require escaping in regex but are also NOT valid filepath pattern characters.
 | 
						|
// Additionally, '\' is not excluded because there is specific logic to properly handle this, as it's a path separator
 | 
						|
// on Windows.
 | 
						|
//
 | 
						|
// Adapted from regexp::QuoteMeta in go stdlib.
 | 
						|
// See https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/regexp/regexp.go;l=703-715;drc=refs%2Ftags%2Fgo1.17.2
 | 
						|
func shouldEscape(b rune) bool {
 | 
						|
	return b < utf8.RuneSelf && escapeBytes[b%8]&(1<<(b/8)) != 0
 | 
						|
}
 | 
						|
 | 
						|
func init() {
 | 
						|
	for _, b := range []byte(`.+()|{}$`) {
 | 
						|
		escapeBytes[b%8] |= 1 << (b / 8)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// PatternMatcher allows checking paths against a list of patterns
 | 
						|
type PatternMatcher struct {
 | 
						|
	patterns   []*Pattern
 | 
						|
	exclusions bool
 | 
						|
}
 | 
						|
 | 
						|
// New creates a new matcher object for specific patterns that can
 | 
						|
// be used later to match against patterns against paths
 | 
						|
func New(patterns []string) (*PatternMatcher, error) {
 | 
						|
	pm := &PatternMatcher{
 | 
						|
		patterns: make([]*Pattern, 0, len(patterns)),
 | 
						|
	}
 | 
						|
	for _, p := range patterns {
 | 
						|
		// Eliminate leading and trailing whitespace.
 | 
						|
		p = strings.TrimSpace(p)
 | 
						|
		if p == "" {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		p = filepath.Clean(p)
 | 
						|
		newp := &Pattern{}
 | 
						|
		if p[0] == '!' {
 | 
						|
			if len(p) == 1 {
 | 
						|
				return nil, errors.New("illegal exclusion pattern: \"!\"")
 | 
						|
			}
 | 
						|
			newp.exclusion = true
 | 
						|
			p = p[1:]
 | 
						|
			pm.exclusions = true
 | 
						|
		}
 | 
						|
		// Do some syntax checking on the pattern.
 | 
						|
		// filepath's Match() has some really weird rules that are inconsistent
 | 
						|
		// so instead of trying to dup their logic, just call Match() for its
 | 
						|
		// error state and if there is an error in the pattern return it.
 | 
						|
		// If this becomes an issue we can remove this since its really only
 | 
						|
		// needed in the error (syntax) case - which isn't really critical.
 | 
						|
		if _, err := filepath.Match(p, "."); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		newp.cleanedPattern = p
 | 
						|
		newp.dirs = strings.Split(p, string(os.PathSeparator))
 | 
						|
		pm.patterns = append(pm.patterns, newp)
 | 
						|
	}
 | 
						|
	return pm, nil
 | 
						|
}
 | 
						|
 | 
						|
// Matches returns true if "file" matches any of the patterns
 | 
						|
// and isn't excluded by any of the subsequent patterns.
 | 
						|
//
 | 
						|
// The "file" argument should be a slash-delimited path.
 | 
						|
//
 | 
						|
// Matches is not safe to call concurrently.
 | 
						|
//
 | 
						|
// Deprecated: This implementation is buggy (it only checks a single parent dir
 | 
						|
// against the pattern) and will be removed soon. Use either
 | 
						|
// MatchesOrParentMatches or MatchesUsingParentResults instead.
 | 
						|
func (pm *PatternMatcher) Matches(file string) (bool, error) {
 | 
						|
	matched := false
 | 
						|
	file = filepath.FromSlash(file)
 | 
						|
	parentPath := filepath.Dir(file)
 | 
						|
	parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
 | 
						|
 | 
						|
	for _, pattern := range pm.patterns {
 | 
						|
		// Skip evaluation if this is an inclusion and the filename
 | 
						|
		// already matched the pattern, or it's an exclusion and it has
 | 
						|
		// not matched the pattern yet.
 | 
						|
		if pattern.exclusion != matched {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		match, err := pattern.match(file)
 | 
						|
		if err != nil {
 | 
						|
			return false, err
 | 
						|
		}
 | 
						|
 | 
						|
		if !match && parentPath != "." {
 | 
						|
			// Check to see if the pattern matches one of our parent dirs.
 | 
						|
			if len(pattern.dirs) <= len(parentPathDirs) {
 | 
						|
				match, _ = pattern.match(strings.Join(parentPathDirs[:len(pattern.dirs)], string(os.PathSeparator)))
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if match {
 | 
						|
			matched = !pattern.exclusion
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return matched, nil
 | 
						|
}
 | 
						|
 | 
						|
// MatchesOrParentMatches returns true if "file" matches any of the patterns
 | 
						|
// and isn't excluded by any of the subsequent patterns.
 | 
						|
//
 | 
						|
// The "file" argument should be a slash-delimited path.
 | 
						|
//
 | 
						|
// Matches is not safe to call concurrently.
 | 
						|
func (pm *PatternMatcher) MatchesOrParentMatches(file string) (bool, error) {
 | 
						|
	matched := false
 | 
						|
	file = filepath.FromSlash(file)
 | 
						|
	parentPath := filepath.Dir(file)
 | 
						|
	parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
 | 
						|
 | 
						|
	for _, pattern := range pm.patterns {
 | 
						|
		// Skip evaluation if this is an inclusion and the filename
 | 
						|
		// already matched the pattern, or it's an exclusion and it has
 | 
						|
		// not matched the pattern yet.
 | 
						|
		if pattern.exclusion != matched {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		match, err := pattern.match(file)
 | 
						|
		if err != nil {
 | 
						|
			return false, err
 | 
						|
		}
 | 
						|
 | 
						|
		if !match && parentPath != "." {
 | 
						|
			// Check to see if the pattern matches one of our parent dirs.
 | 
						|
			for i := range parentPathDirs {
 | 
						|
				match, _ = pattern.match(strings.Join(parentPathDirs[:i+1], string(os.PathSeparator)))
 | 
						|
				if match {
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if match {
 | 
						|
			matched = !pattern.exclusion
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return matched, nil
 | 
						|
}
 | 
						|
 | 
						|
// MatchesUsingParentResult returns true if "file" matches any of the patterns
 | 
						|
// and isn't excluded by any of the subsequent patterns. The functionality is
 | 
						|
// the same as Matches, but as an optimization, the caller keeps track of
 | 
						|
// whether the parent directory matched.
 | 
						|
//
 | 
						|
// The "file" argument should be a slash-delimited path.
 | 
						|
//
 | 
						|
// MatchesUsingParentResult is not safe to call concurrently.
 | 
						|
//
 | 
						|
// Deprecated: this function does behave correctly in some cases (see
 | 
						|
// https://github.com/docker/buildx/issues/850).
 | 
						|
//
 | 
						|
// Use MatchesUsingParentResults instead.
 | 
						|
func (pm *PatternMatcher) MatchesUsingParentResult(file string, parentMatched bool) (bool, error) {
 | 
						|
	matched := parentMatched
 | 
						|
	file = filepath.FromSlash(file)
 | 
						|
 | 
						|
	for _, pattern := range pm.patterns {
 | 
						|
		// Skip evaluation if this is an inclusion and the filename
 | 
						|
		// already matched the pattern, or it's an exclusion and it has
 | 
						|
		// not matched the pattern yet.
 | 
						|
		if pattern.exclusion != matched {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		match, err := pattern.match(file)
 | 
						|
		if err != nil {
 | 
						|
			return false, err
 | 
						|
		}
 | 
						|
 | 
						|
		if match {
 | 
						|
			matched = !pattern.exclusion
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return matched, nil
 | 
						|
}
 | 
						|
 | 
						|
// MatchInfo tracks information about parent dir matches while traversing a
 | 
						|
// filesystem.
 | 
						|
type MatchInfo struct {
 | 
						|
	parentMatched []bool
 | 
						|
}
 | 
						|
 | 
						|
// MatchesUsingParentResults returns true if "file" matches any of the patterns
 | 
						|
// and isn't excluded by any of the subsequent patterns. The functionality is
 | 
						|
// the same as Matches, but as an optimization, the caller passes in
 | 
						|
// intermediate results from matching the parent directory.
 | 
						|
//
 | 
						|
// The "file" argument should be a slash-delimited path.
 | 
						|
//
 | 
						|
// MatchesUsingParentResults is not safe to call concurrently.
 | 
						|
func (pm *PatternMatcher) MatchesUsingParentResults(file string, parentMatchInfo MatchInfo) (bool, MatchInfo, error) {
 | 
						|
	parentMatched := parentMatchInfo.parentMatched
 | 
						|
	if len(parentMatched) != 0 && len(parentMatched) != len(pm.patterns) {
 | 
						|
		return false, MatchInfo{}, errors.New("wrong number of values in parentMatched")
 | 
						|
	}
 | 
						|
 | 
						|
	file = filepath.FromSlash(file)
 | 
						|
	matched := false
 | 
						|
 | 
						|
	matchInfo := MatchInfo{
 | 
						|
		parentMatched: make([]bool, len(pm.patterns)),
 | 
						|
	}
 | 
						|
	for i, pattern := range pm.patterns {
 | 
						|
		match := false
 | 
						|
		// If the parent matched this pattern, we don't need to recheck.
 | 
						|
		if len(parentMatched) != 0 {
 | 
						|
			match = parentMatched[i]
 | 
						|
		}
 | 
						|
 | 
						|
		if !match {
 | 
						|
			// Skip evaluation if this is an inclusion and the filename
 | 
						|
			// already matched the pattern, or it's an exclusion and it has
 | 
						|
			// not matched the pattern yet.
 | 
						|
			if pattern.exclusion != matched {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			var err error
 | 
						|
			match, err = pattern.match(file)
 | 
						|
			if err != nil {
 | 
						|
				return false, matchInfo, err
 | 
						|
			}
 | 
						|
 | 
						|
			// If the zero value of MatchInfo was passed in, we don't have
 | 
						|
			// any information about the parent dir's match results, and we
 | 
						|
			// apply the same logic as MatchesOrParentMatches.
 | 
						|
			if !match && len(parentMatched) == 0 {
 | 
						|
				if parentPath := filepath.Dir(file); parentPath != "." {
 | 
						|
					parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
 | 
						|
					// Check to see if the pattern matches one of our parent dirs.
 | 
						|
					for i := range parentPathDirs {
 | 
						|
						match, _ = pattern.match(strings.Join(parentPathDirs[:i+1], string(os.PathSeparator)))
 | 
						|
						if match {
 | 
						|
							break
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		matchInfo.parentMatched[i] = match
 | 
						|
 | 
						|
		if match {
 | 
						|
			matched = !pattern.exclusion
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return matched, matchInfo, nil
 | 
						|
}
 | 
						|
 | 
						|
// Exclusions returns true if any of the patterns define exclusions
 | 
						|
func (pm *PatternMatcher) Exclusions() bool {
 | 
						|
	return pm.exclusions
 | 
						|
}
 | 
						|
 | 
						|
// Patterns returns array of active patterns
 | 
						|
func (pm *PatternMatcher) Patterns() []*Pattern {
 | 
						|
	return pm.patterns
 | 
						|
}
 | 
						|
 | 
						|
// Pattern defines a single regexp used to filter file paths.
 | 
						|
type Pattern struct {
 | 
						|
	matchType      matchType
 | 
						|
	cleanedPattern string
 | 
						|
	dirs           []string
 | 
						|
	regexp         *regexp.Regexp
 | 
						|
	exclusion      bool
 | 
						|
}
 | 
						|
 | 
						|
type matchType int
 | 
						|
 | 
						|
const (
 | 
						|
	unknownMatch matchType = iota
 | 
						|
	exactMatch
 | 
						|
	prefixMatch
 | 
						|
	suffixMatch
 | 
						|
	regexpMatch
 | 
						|
)
 | 
						|
 | 
						|
func (p *Pattern) String() string {
 | 
						|
	return p.cleanedPattern
 | 
						|
}
 | 
						|
 | 
						|
// Exclusion returns true if this pattern defines exclusion
 | 
						|
func (p *Pattern) Exclusion() bool {
 | 
						|
	return p.exclusion
 | 
						|
}
 | 
						|
 | 
						|
func (p *Pattern) match(path string) (bool, error) {
 | 
						|
	if p.matchType == unknownMatch {
 | 
						|
		if err := p.compile(string(os.PathSeparator)); err != nil {
 | 
						|
			return false, filepath.ErrBadPattern
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	switch p.matchType {
 | 
						|
	case exactMatch:
 | 
						|
		return path == p.cleanedPattern, nil
 | 
						|
	case prefixMatch:
 | 
						|
		// strip trailing **
 | 
						|
		return strings.HasPrefix(path, p.cleanedPattern[:len(p.cleanedPattern)-2]), nil
 | 
						|
	case suffixMatch:
 | 
						|
		// strip leading **
 | 
						|
		suffix := p.cleanedPattern[2:]
 | 
						|
		if strings.HasSuffix(path, suffix) {
 | 
						|
			return true, nil
 | 
						|
		}
 | 
						|
		// **/foo matches "foo"
 | 
						|
		return suffix[0] == os.PathSeparator && path == suffix[1:], nil
 | 
						|
	case regexpMatch:
 | 
						|
		return p.regexp.MatchString(path), nil
 | 
						|
	}
 | 
						|
 | 
						|
	return false, nil
 | 
						|
}
 | 
						|
 | 
						|
func (p *Pattern) compile(sl string) error {
 | 
						|
	regStr := "^"
 | 
						|
	pattern := p.cleanedPattern
 | 
						|
	// Go through the pattern and convert it to a regexp.
 | 
						|
	// We use a scanner so we can support utf-8 chars.
 | 
						|
	var scan scanner.Scanner
 | 
						|
	scan.Init(strings.NewReader(pattern))
 | 
						|
 | 
						|
	escSL := sl
 | 
						|
	if sl == `\` {
 | 
						|
		escSL += `\`
 | 
						|
	}
 | 
						|
 | 
						|
	p.matchType = exactMatch
 | 
						|
	for i := 0; scan.Peek() != scanner.EOF; i++ {
 | 
						|
		ch := scan.Next()
 | 
						|
 | 
						|
		if ch == '*' {
 | 
						|
			if scan.Peek() == '*' {
 | 
						|
				// is some flavor of "**"
 | 
						|
				scan.Next()
 | 
						|
 | 
						|
				// Treat **/ as ** so eat the "/"
 | 
						|
				if string(scan.Peek()) == sl {
 | 
						|
					scan.Next()
 | 
						|
				}
 | 
						|
 | 
						|
				if scan.Peek() == scanner.EOF {
 | 
						|
					// is "**EOF" - to align with .gitignore just accept all
 | 
						|
					if p.matchType == exactMatch {
 | 
						|
						p.matchType = prefixMatch
 | 
						|
					} else {
 | 
						|
						regStr += ".*"
 | 
						|
						p.matchType = regexpMatch
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					// is "**"
 | 
						|
					// Note that this allows for any # of /'s (even 0) because
 | 
						|
					// the .* will eat everything, even /'s
 | 
						|
					regStr += "(.*" + escSL + ")?"
 | 
						|
					p.matchType = regexpMatch
 | 
						|
				}
 | 
						|
 | 
						|
				if i == 0 {
 | 
						|
					p.matchType = suffixMatch
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				// is "*" so map it to anything but "/"
 | 
						|
				regStr += "[^" + escSL + "]*"
 | 
						|
				p.matchType = regexpMatch
 | 
						|
			}
 | 
						|
		} else if ch == '?' {
 | 
						|
			// "?" is any char except "/"
 | 
						|
			regStr += "[^" + escSL + "]"
 | 
						|
			p.matchType = regexpMatch
 | 
						|
		} else if shouldEscape(ch) {
 | 
						|
			// Escape some regexp special chars that have no meaning
 | 
						|
			// in golang's filepath.Match
 | 
						|
			regStr += `\` + string(ch)
 | 
						|
		} else if ch == '\\' {
 | 
						|
			// escape next char. Note that a trailing \ in the pattern
 | 
						|
			// will be left alone (but need to escape it)
 | 
						|
			if sl == `\` {
 | 
						|
				// On windows map "\" to "\\", meaning an escaped backslash,
 | 
						|
				// and then just continue because filepath.Match on
 | 
						|
				// Windows doesn't allow escaping at all
 | 
						|
				regStr += escSL
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			if scan.Peek() != scanner.EOF {
 | 
						|
				regStr += `\` + string(scan.Next())
 | 
						|
				p.matchType = regexpMatch
 | 
						|
			} else {
 | 
						|
				regStr += `\`
 | 
						|
			}
 | 
						|
		} else if ch == '[' || ch == ']' {
 | 
						|
			regStr += string(ch)
 | 
						|
			p.matchType = regexpMatch
 | 
						|
		} else {
 | 
						|
			regStr += string(ch)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if p.matchType != regexpMatch {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	regStr += "$"
 | 
						|
 | 
						|
	re, err := regexp.Compile(regStr)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	p.regexp = re
 | 
						|
	p.matchType = regexpMatch
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Matches returns true if file matches any of the patterns
 | 
						|
// and isn't excluded by any of the subsequent patterns.
 | 
						|
//
 | 
						|
// This implementation is buggy (it only checks a single parent dir against the
 | 
						|
// pattern) and will be removed soon. Use MatchesOrParentMatches instead.
 | 
						|
func Matches(file string, patterns []string) (bool, error) {
 | 
						|
	pm, err := New(patterns)
 | 
						|
	if err != nil {
 | 
						|
		return false, err
 | 
						|
	}
 | 
						|
	file = filepath.Clean(file)
 | 
						|
 | 
						|
	if file == "." {
 | 
						|
		// Don't let them exclude everything, kind of silly.
 | 
						|
		return false, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return pm.Matches(file)
 | 
						|
}
 | 
						|
 | 
						|
// MatchesOrParentMatches returns true if file matches any of the patterns
 | 
						|
// and isn't excluded by any of the subsequent patterns.
 | 
						|
func MatchesOrParentMatches(file string, patterns []string) (bool, error) {
 | 
						|
	pm, err := New(patterns)
 | 
						|
	if err != nil {
 | 
						|
		return false, err
 | 
						|
	}
 | 
						|
	file = filepath.Clean(file)
 | 
						|
 | 
						|
	if file == "." {
 | 
						|
		// Don't let them exclude everything, kind of silly.
 | 
						|
		return false, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return pm.MatchesOrParentMatches(file)
 | 
						|
}
 |