pax_global_header00006660000000000000000000000064143210461640014513gustar00rootroot0000000000000052 comment=c5e49da0e746156f613843a81d7873154112843e golang-github-moby-patternmatcher-0.5.0/000077500000000000000000000000001432104616400202275ustar00rootroot00000000000000golang-github-moby-patternmatcher-0.5.0/.github/000077500000000000000000000000001432104616400215675ustar00rootroot00000000000000golang-github-moby-patternmatcher-0.5.0/.github/workflows/000077500000000000000000000000001432104616400236245ustar00rootroot00000000000000golang-github-moby-patternmatcher-0.5.0/.github/workflows/test.yml000066400000000000000000000007001432104616400253230ustar00rootroot00000000000000name: test on: [push, pull_request] permissions: contents: read jobs: test: strategy: matrix: go-version: [1.18.x, 1.19.x] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 steps: - uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - uses: actions/checkout@v3 - name: Test run: go test -v ./... golang-github-moby-patternmatcher-0.5.0/.github/workflows/validate.yml000066400000000000000000000010311432104616400261330ustar00rootroot00000000000000name: validate on: [push, pull_request] permissions: contents: read jobs: linters: strategy: matrix: go-version: [1.19.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 steps: - uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - uses: actions/checkout@v3 - name: lint uses: golangci/golangci-lint-action@v3 with: version: v1.49 args: --print-resources-usage --timeout=10m --verbose golang-github-moby-patternmatcher-0.5.0/LICENSE000066400000000000000000000250151432104616400212370ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2013-2018 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. golang-github-moby-patternmatcher-0.5.0/NOTICE000066400000000000000000000010071432104616400211310ustar00rootroot00000000000000Docker Copyright 2012-2017 Docker, Inc. This product includes software developed at Docker, Inc. (https://www.docker.com). The following is courtesy of our legal counsel: Use and transfer of Docker may be subject to certain restrictions by the United States and other governments. It is your responsibility to ensure that your use and/or transfer does not violate applicable laws. For more information, please see https://www.bis.doc.gov See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. golang-github-moby-patternmatcher-0.5.0/go.mod000066400000000000000000000000571432104616400213370ustar00rootroot00000000000000module github.com/moby/patternmatcher go 1.19 golang-github-moby-patternmatcher-0.5.0/patternmatcher.go000066400000000000000000000313421432104616400236020ustar00rootroot00000000000000package 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) } golang-github-moby-patternmatcher-0.5.0/patternmatcher_test.go000066400000000000000000000411171432104616400246420ustar00rootroot00000000000000package patternmatcher import ( "fmt" "os" "path" "path/filepath" "runtime" "strings" "testing" ) func TestWildcardMatches(t *testing.T) { match, _ := Matches("fileutils.go", []string{"*"}) if !match { t.Errorf("failed to get a wildcard match, got %v", match) } } // A simple pattern match should return true. func TestPatternMatches(t *testing.T) { match, _ := Matches("fileutils.go", []string{"*.go"}) if !match { t.Errorf("failed to get a match, got %v", match) } } // An exclusion followed by an inclusion should return true. func TestExclusionPatternMatchesPatternBefore(t *testing.T) { match, _ := Matches("fileutils.go", []string{"!fileutils.go", "*.go"}) if !match { t.Errorf("failed to get true match on exclusion pattern, got %v", match) } } // A folder pattern followed by an exception should return false. func TestPatternMatchesFolderExclusions(t *testing.T) { match, _ := Matches("docs/README.md", []string{"docs", "!docs/README.md"}) if match { t.Errorf("failed to get a false match on exclusion pattern, got %v", match) } } // A folder pattern followed by an exception should return false. func TestPatternMatchesFolderWithSlashExclusions(t *testing.T) { match, _ := Matches("docs/README.md", []string{"docs/", "!docs/README.md"}) if match { t.Errorf("failed to get a false match on exclusion pattern, got %v", match) } } // A folder pattern followed by an exception should return false. func TestPatternMatchesFolderWildcardExclusions(t *testing.T) { match, _ := Matches("docs/README.md", []string{"docs/*", "!docs/README.md"}) if match { t.Errorf("failed to get a false match on exclusion pattern, got %v", match) } } // A pattern followed by an exclusion should return false. func TestExclusionPatternMatchesPatternAfter(t *testing.T) { match, _ := Matches("fileutils.go", []string{"*.go", "!fileutils.go"}) if match { t.Errorf("failed to get false match on exclusion pattern, got %v", match) } } // A filename evaluating to . should return false. func TestExclusionPatternMatchesWholeDirectory(t *testing.T) { match, _ := Matches(".", []string{"*.go"}) if match { t.Errorf("failed to get false match on ., got %v", match) } } // A single ! pattern should return an error. func TestSingleExclamationError(t *testing.T) { _, err := Matches("fileutils.go", []string{"!"}) if err == nil { t.Errorf("failed to get an error for a single exclamation point, got %v", err) } } // Matches with no patterns func TestMatchesWithNoPatterns(t *testing.T) { matches, err := Matches("/any/path/there", []string{}) if err != nil { t.Fatal(err) } if matches { t.Fatalf("Should not have match anything") } } // Matches with malformed patterns func TestMatchesWithMalformedPatterns(t *testing.T) { matches, err := Matches("/any/path/there", []string{"["}) if err == nil { t.Fatal("Should have failed because of a malformed syntax in the pattern") } if matches { t.Fatalf("Should not have match anything") } } type matchesTestCase struct { pattern string text string pass bool } type multiPatternTestCase struct { patterns []string text string pass bool } func TestMatches(t *testing.T) { tests := []matchesTestCase{ {"**", "file", true}, {"**", "file/", true}, {"**/", "file", true}, // weird one {"**/", "file/", true}, {"**", "/", true}, {"**/", "/", true}, {"**", "dir/file", true}, {"**/", "dir/file", true}, {"**", "dir/file/", true}, {"**/", "dir/file/", true}, {"**/**", "dir/file", true}, {"**/**", "dir/file/", true}, {"dir/**", "dir/file", true}, {"dir/**", "dir/file/", true}, {"dir/**", "dir/dir2/file", true}, {"dir/**", "dir/dir2/file/", true}, {"**/dir", "dir", true}, {"**/dir", "dir/file", true}, {"**/dir2/*", "dir/dir2/file", true}, {"**/dir2/*", "dir/dir2/file/", true}, {"**/dir2/**", "dir/dir2/dir3/file", true}, {"**/dir2/**", "dir/dir2/dir3/file/", true}, {"**file", "file", true}, {"**file", "dir/file", true}, {"**/file", "dir/file", true}, {"**file", "dir/dir/file", true}, {"**/file", "dir/dir/file", true}, {"**/file*", "dir/dir/file", true}, {"**/file*", "dir/dir/file.txt", true}, {"**/file*txt", "dir/dir/file.txt", true}, {"**/file*.txt", "dir/dir/file.txt", true}, {"**/file*.txt*", "dir/dir/file.txt", true}, {"**/**/*.txt", "dir/dir/file.txt", true}, {"**/**/*.txt2", "dir/dir/file.txt", false}, {"**/*.txt", "file.txt", true}, {"**/**/*.txt", "file.txt", true}, {"a**/*.txt", "a/file.txt", true}, {"a**/*.txt", "a/dir/file.txt", true}, {"a**/*.txt", "a/dir/dir/file.txt", true}, {"a/*.txt", "a/dir/file.txt", false}, {"a/*.txt", "a/file.txt", true}, {"a/*.txt**", "a/file.txt", true}, {"a[b-d]e", "ae", false}, {"a[b-d]e", "ace", true}, {"a[b-d]e", "aae", false}, {"a[^b-d]e", "aze", true}, {".*", ".foo", true}, {".*", "foo", false}, {"abc.def", "abcdef", false}, {"abc.def", "abc.def", true}, {"abc.def", "abcZdef", false}, {"abc?def", "abcZdef", true}, {"abc?def", "abcdef", false}, {"a\\\\", "a\\", true}, {"**/foo/bar", "foo/bar", true}, {"**/foo/bar", "dir/foo/bar", true}, {"**/foo/bar", "dir/dir2/foo/bar", true}, {"abc/**", "abc", false}, {"abc/**", "abc/def", true}, {"abc/**", "abc/def/ghi", true}, {"**/.foo", ".foo", true}, {"**/.foo", "bar.foo", false}, {"a(b)c/def", "a(b)c/def", true}, {"a(b)c/def", "a(b)c/xyz", false}, {"a.|)$(}+{bc", "a.|)$(}+{bc", true}, {"dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", true}, {"dist/*.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", true}, } multiPatternTests := []multiPatternTestCase{ {[]string{"**", "!util/docker/web"}, "util/docker/web/foo", false}, {[]string{"**", "!util/docker/web", "util/docker/web/foo"}, "util/docker/web/foo", true}, {[]string{"**", "!dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", false}, {[]string{"**", "!dist/*.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", false}, } if runtime.GOOS != "windows" { tests = append(tests, []matchesTestCase{ {"a\\*b", "a*b", true}, }...) } t.Run("MatchesOrParentMatches", func(t *testing.T) { for _, test := range tests { pm, err := New([]string{test.pattern}) if err != nil { t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text) } res, _ := pm.MatchesOrParentMatches(test.text) if test.pass != res { t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text) } } for _, test := range multiPatternTests { pm, err := New(test.patterns) if err != nil { t.Fatalf("%v (patterns=%q, text=%q)", err, test.patterns, test.text) } res, _ := pm.MatchesOrParentMatches(test.text) if test.pass != res { t.Errorf("expected: %v, got: %v (patterns=%q, text=%q)", test.pass, res, test.patterns, test.text) } } }) t.Run("MatchesUsingParentResult", func(t *testing.T) { for _, test := range tests { pm, err := New([]string{test.pattern}) if err != nil { t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text) } parentPath := filepath.Dir(filepath.FromSlash(test.text)) parentPathDirs := strings.Split(parentPath, string(os.PathSeparator)) parentMatched := false if parentPath != "." { for i := range parentPathDirs { parentMatched, _ = pm.MatchesUsingParentResult(strings.Join(parentPathDirs[:i+1], "/"), parentMatched) } } res, _ := pm.MatchesUsingParentResult(test.text, parentMatched) if test.pass != res { t.Errorf("expected: %v, got: %v (pattern=%q, text=%q)", test.pass, res, test.pattern, test.text) } } }) t.Run("MatchesUsingParentResults", func(t *testing.T) { check := func(pm *PatternMatcher, text string, pass bool, desc string) { parentPath := filepath.Dir(filepath.FromSlash(text)) parentPathDirs := strings.Split(parentPath, string(os.PathSeparator)) parentMatchInfo := MatchInfo{} if parentPath != "." { for i := range parentPathDirs { _, parentMatchInfo, _ = pm.MatchesUsingParentResults(strings.Join(parentPathDirs[:i+1], "/"), parentMatchInfo) } } res, _, _ := pm.MatchesUsingParentResults(text, parentMatchInfo) if pass != res { t.Errorf("expected: %v, got: %v %s", pass, res, desc) } } for _, test := range tests { desc := fmt.Sprintf("(pattern=%q text=%q)", test.pattern, test.text) pm, err := New([]string{test.pattern}) if err != nil { t.Fatal(err, desc) } check(pm, test.text, test.pass, desc) } for _, test := range multiPatternTests { desc := fmt.Sprintf("pattern=%q text=%q", test.patterns, test.text) pm, err := New(test.patterns) if err != nil { t.Fatal(err, desc) } check(pm, test.text, test.pass, desc) } }) t.Run("MatchesUsingParentResultsNoContext", func(t *testing.T) { check := func(pm *PatternMatcher, text string, pass bool, desc string) { res, _, _ := pm.MatchesUsingParentResults(text, MatchInfo{}) if pass != res { t.Errorf("expected: %v, got: %v %s", pass, res, desc) } } for _, test := range tests { desc := fmt.Sprintf("(pattern=%q text=%q)", test.pattern, test.text) pm, err := New([]string{test.pattern}) if err != nil { t.Fatal(err, desc) } check(pm, test.text, test.pass, desc) } for _, test := range multiPatternTests { desc := fmt.Sprintf("(pattern=%q text=%q)", test.patterns, test.text) pm, err := New(test.patterns) if err != nil { t.Fatal(err, desc) } check(pm, test.text, test.pass, desc) } }) } func TestCleanPatterns(t *testing.T) { patterns := []string{"docs", "config"} pm, err := New(patterns) if err != nil { t.Fatalf("invalid pattern %v", patterns) } cleaned := pm.Patterns() if len(cleaned) != 2 { t.Errorf("expected 2 element slice, got %v", len(cleaned)) } } func TestCleanPatternsStripEmptyPatterns(t *testing.T) { patterns := []string{"docs", "config", ""} pm, err := New(patterns) if err != nil { t.Fatalf("invalid pattern %v", patterns) } cleaned := pm.Patterns() if len(cleaned) != 2 { t.Errorf("expected 2 element slice, got %v", len(cleaned)) } } func TestCleanPatternsExceptionFlag(t *testing.T) { patterns := []string{"docs", "!docs/README.md"} pm, err := New(patterns) if err != nil { t.Fatalf("invalid pattern %v", patterns) } if !pm.Exclusions() { t.Errorf("expected exceptions to be true, got %v", pm.Exclusions()) } } func TestCleanPatternsLeadingSpaceTrimmed(t *testing.T) { patterns := []string{"docs", " !docs/README.md"} pm, err := New(patterns) if err != nil { t.Fatalf("invalid pattern %v", patterns) } if !pm.Exclusions() { t.Errorf("expected exceptions to be true, got %v", pm.Exclusions()) } } func TestCleanPatternsTrailingSpaceTrimmed(t *testing.T) { patterns := []string{"docs", "!docs/README.md "} pm, err := New(patterns) if err != nil { t.Fatalf("invalid pattern %v", patterns) } if !pm.Exclusions() { t.Errorf("expected exceptions to be true, got %v", pm.Exclusions()) } } func TestCleanPatternsErrorSingleException(t *testing.T) { patterns := []string{"!"} _, err := New(patterns) if err == nil { t.Errorf("expected error on single exclamation point, got %v", err) } } // These matchTests are stolen from go's filepath Match tests. type matchTest struct { pattern, s string match bool err error } var matchTests = []matchTest{ {"abc", "abc", true, nil}, {"*", "abc", true, nil}, {"*c", "abc", true, nil}, {"a*", "a", true, nil}, {"a*", "abc", true, nil}, {"a*", "ab/c", true, nil}, {"a*/b", "abc/b", true, nil}, {"a*/b", "a/c/b", false, nil}, {"a*b*c*d*e*/f", "axbxcxdxe/f", true, nil}, {"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, nil}, {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, nil}, {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, nil}, {"a*b?c*x", "abxbbxdbxebxczzx", true, nil}, {"a*b?c*x", "abxbbxdbxebxczzy", false, nil}, {"ab[c]", "abc", true, nil}, {"ab[b-d]", "abc", true, nil}, {"ab[e-g]", "abc", false, nil}, {"ab[^c]", "abc", false, nil}, {"ab[^b-d]", "abc", false, nil}, {"ab[^e-g]", "abc", true, nil}, {"a\\*b", "a*b", true, nil}, {"a\\*b", "ab", false, nil}, {"a?b", "a☺b", true, nil}, {"a[^a]b", "a☺b", true, nil}, {"a???b", "a☺b", false, nil}, {"a[^a][^a][^a]b", "a☺b", false, nil}, {"[a-ζ]*", "α", true, nil}, {"*[a-ζ]", "A", false, nil}, {"a?b", "a/b", false, nil}, {"a*b", "a/b", false, nil}, {"[\\]a]", "]", true, nil}, {"[\\-]", "-", true, nil}, {"[x\\-]", "x", true, nil}, {"[x\\-]", "-", true, nil}, {"[x\\-]", "z", false, nil}, {"[\\-x]", "x", true, nil}, {"[\\-x]", "-", true, nil}, {"[\\-x]", "a", false, nil}, {"[]a]", "]", false, filepath.ErrBadPattern}, {"[-]", "-", false, filepath.ErrBadPattern}, {"[x-]", "x", false, filepath.ErrBadPattern}, {"[x-]", "-", false, filepath.ErrBadPattern}, {"[x-]", "z", false, filepath.ErrBadPattern}, {"[-x]", "x", false, filepath.ErrBadPattern}, {"[-x]", "-", false, filepath.ErrBadPattern}, {"[-x]", "a", false, filepath.ErrBadPattern}, {"\\", "a", false, filepath.ErrBadPattern}, {"[a-b-c]", "a", false, filepath.ErrBadPattern}, {"[", "a", false, filepath.ErrBadPattern}, {"[^", "a", false, filepath.ErrBadPattern}, {"[^bc", "a", false, filepath.ErrBadPattern}, {"a[", "a", false, filepath.ErrBadPattern}, // was nil but IMO its wrong {"a[", "ab", false, filepath.ErrBadPattern}, {"*x", "xxx", true, nil}, } func errp(e error) string { if e == nil { return "" } return e.Error() } // TestMatch tests our version of filepath.Match, called Matches. func TestMatch(t *testing.T) { for _, tt := range matchTests { pattern := tt.pattern s := tt.s if runtime.GOOS == "windows" { if strings.Contains(pattern, "\\") { // no escape allowed on windows. continue } pattern = filepath.Clean(pattern) s = filepath.Clean(s) } ok, err := Matches(s, []string{pattern}) if ok != tt.match || err != tt.err { t.Fatalf("Match(%#q, %#q) = %v, %q want %v, %q", pattern, s, ok, errp(err), tt.match, errp(tt.err)) } } } type compileTestCase struct { pattern string matchType matchType compiledRegexp string windowsCompiledRegexp string } var compileTests = []compileTestCase{ {"*", regexpMatch, `^[^/]*$`, `^[^\\]*$`}, {"file*", regexpMatch, `^file[^/]*$`, `^file[^\\]*$`}, {"*file", regexpMatch, `^[^/]*file$`, `^[^\\]*file$`}, {"a*/b", regexpMatch, `^a[^/]*/b$`, `^a[^\\]*\\b$`}, {"**", suffixMatch, "", ""}, {"**/**", regexpMatch, `^(.*/)?.*$`, `^(.*\\)?.*$`}, {"dir/**", prefixMatch, "", ""}, {"**/dir", suffixMatch, "", ""}, {"**/dir2/*", regexpMatch, `^(.*/)?dir2/[^/]*$`, `^(.*\\)?dir2\\[^\\]*$`}, {"**/dir2/**", regexpMatch, `^(.*/)?dir2/.*$`, `^(.*\\)?dir2\\.*$`}, {"**file", suffixMatch, "", ""}, {"**/file*txt", regexpMatch, `^(.*/)?file[^/]*txt$`, `^(.*\\)?file[^\\]*txt$`}, {"**/**/*.txt", regexpMatch, `^(.*/)?(.*/)?[^/]*\.txt$`, `^(.*\\)?(.*\\)?[^\\]*\.txt$`}, {"a[b-d]e", regexpMatch, `^a[b-d]e$`, `^a[b-d]e$`}, {".*", regexpMatch, `^\.[^/]*$`, `^\.[^\\]*$`}, {"abc.def", exactMatch, "", ""}, {"abc?def", regexpMatch, `^abc[^/]def$`, `^abc[^\\]def$`}, {"**/foo/bar", suffixMatch, "", ""}, {"a(b)c/def", exactMatch, "", ""}, {"a.|)$(}+{bc", exactMatch, "", ""}, {"dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", exactMatch, "", ""}, } // TestCompile confirms that "compile" assigns the correct match type to a // variety of test case patterns. If the match type is regexp, it also confirms // that the compiled regexp matches the expected regexp. func TestCompile(t *testing.T) { t.Run("slash", testCompile("/")) t.Run("backslash", testCompile(`\`)) } func testCompile(sl string) func(*testing.T) { return func(t *testing.T) { for _, tt := range compileTests { // Avoid NewPatternMatcher, which has platform-specific behavior pm := &PatternMatcher{ patterns: make([]*Pattern, 1), } pattern := path.Clean(tt.pattern) if sl != "/" { pattern = strings.ReplaceAll(pattern, "/", sl) } newp := &Pattern{} newp.cleanedPattern = pattern newp.dirs = strings.Split(pattern, sl) pm.patterns[0] = newp if err := pm.patterns[0].compile(sl); err != nil { t.Fatalf("Failed to compile pattern %q: %v", pattern, err) } if pm.patterns[0].matchType != tt.matchType { t.Errorf("pattern %q: matchType = %v, want %v", pattern, pm.patterns[0].matchType, tt.matchType) continue } if tt.matchType == regexpMatch { if sl == `\` { if pm.patterns[0].regexp.String() != tt.windowsCompiledRegexp { t.Errorf("pattern %q: regexp = %s, want %s", pattern, pm.patterns[0].regexp, tt.windowsCompiledRegexp) } } else if pm.patterns[0].regexp.String() != tt.compiledRegexp { t.Errorf("pattern %q: regexp = %s, want %s", pattern, pm.patterns[0].regexp, tt.compiledRegexp) } } } } }