summaryrefslogtreecommitdiff
path: root/vendor/github.com/moby/patternmatcher/patternmatcher.go
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-05-11 21:12:57 -0600
committermo khan <mo@mokhan.ca>2025-05-11 21:12:57 -0600
commit60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch)
tree2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/moby/patternmatcher/patternmatcher.go
parent05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff)
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/moby/patternmatcher/patternmatcher.go')
-rw-r--r--vendor/github.com/moby/patternmatcher/patternmatcher.go474
1 files changed, 474 insertions, 0 deletions
diff --git a/vendor/github.com/moby/patternmatcher/patternmatcher.go b/vendor/github.com/moby/patternmatcher/patternmatcher.go
new file mode 100644
index 0000000..37a1a59
--- /dev/null
+++ b/vendor/github.com/moby/patternmatcher/patternmatcher.go
@@ -0,0 +1,474 @@
+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)
+}