From ab6276d0a66cc9e483b3f597bd16b7b9cc321ec2 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Wed, 7 Aug 2024 11:04:06 +0100 Subject: [PATCH] Add `MatchesExact` function This function allows matching on the full path, without attempting to match any parent elements of the path. This was *technically* possible before, by calling the deprecated `MatchesUsingParentResult` and always setting `parentMatched` to `false`. However, this is quite hacky and results in quite tricky-to-read code (and also didn't have tests, etc). So this patch adds in a proper function for this, and ensures it works with some refactored tests. Signed-off-by: Justin Chadwell --- patternmatcher.go | 30 +++++++ patternmatcher_test.go | 195 ++++++++++++++++++++++++----------------- 2 files changed, 143 insertions(+), 82 deletions(-) diff --git a/patternmatcher.go b/patternmatcher.go index 37a1a59..65e9ddf 100644 --- a/patternmatcher.go +++ b/patternmatcher.go @@ -119,6 +119,36 @@ func (pm *PatternMatcher) Matches(file string) (bool, error) { return matched, nil } +// MatchesExact returns true if "file" exactly matches any of the patterns. +// Unlike MatchesOrParentMatches, no parent matching is performed. +// +// The "file" argument should be a slash-delimited path. +// +// MatchesExact is not safe to call concurrently. +func (pm *PatternMatcher) MatchesExact(file string) (bool, error) { + matched := false + 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 +} + // MatchesOrParentMatches returns true if "file" matches any of the patterns // and isn't excluded by any of the subsequent patterns. // diff --git a/patternmatcher_test.go b/patternmatcher_test.go index ea4605a..3ecc6f3 100644 --- a/patternmatcher_test.go +++ b/patternmatcher_test.go @@ -106,98 +106,129 @@ func TestMatchesWithMalformedPatterns(t *testing.T) { type matchesTestCase struct { pattern string text string - pass bool + pass testMatchType } type multiPatternTestCase struct { patterns []string text string - pass bool + pass testMatchType } +type testMatchType int + +const ( + fail testMatchType = iota + exact testMatchType = iota + inexact testMatchType = iota +) + 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}, + {"**", "file", exact}, + {"**", "file/", exact}, + {"**/", "file", exact}, // weird one + {"**/", "file/", exact}, + {"**", "/", exact}, + {"**/", "/", exact}, + {"**", "dir/file", exact}, + {"**/", "dir/file", exact}, + {"**", "dir/file/", exact}, + {"**/", "dir/file/", exact}, + {"**/**", "dir/file", exact}, + {"**/**", "dir/file/", exact}, + {"dir/**", "dir/file", exact}, + {"dir/**", "dir/file/", exact}, + {"dir/**", "dir/dir2/file", exact}, + {"dir/**", "dir/dir2/file/", exact}, + {"**/dir", "dir", exact}, + {"**/dir", "dir/file", inexact}, + {"**/dir2/*", "dir/dir2/file", exact}, + {"**/dir2/*", "dir/dir2/file/", inexact}, + {"**/dir2/**", "dir/dir2/dir3/file", exact}, + {"**/dir2/**", "dir/dir2/dir3/file/", exact}, + {"**file", "file", exact}, + {"**file", "dir/file", exact}, + {"**/file", "dir/file", exact}, + {"**file", "dir/dir/file", exact}, + {"**/file", "dir/dir/file", exact}, + {"**/file*", "dir/dir/file", exact}, + {"**/file*", "dir/dir/file.txt", exact}, + {"**/file*txt", "dir/dir/file.txt", exact}, + {"**/file*.txt", "dir/dir/file.txt", exact}, + {"**/file*.txt*", "dir/dir/file.txt", exact}, + {"**/**/*.txt", "dir/dir/file.txt", exact}, + {"**/**/*.txt2", "dir/dir/file.txt", fail}, + {"**/*.txt", "file.txt", exact}, + {"**/**/*.txt", "file.txt", exact}, + {"a**/*.txt", "a/file.txt", exact}, + {"a**/*.txt", "a/dir/file.txt", exact}, + {"a**/*.txt", "a/dir/dir/file.txt", exact}, + {"a/*.txt", "a/dir/file.txt", fail}, + {"a/*.txt", "a/file.txt", exact}, + {"a/*.txt**", "a/file.txt", exact}, + {"a[b-d]e", "ae", fail}, + {"a[b-d]e", "ace", exact}, + {"a[b-d]e", "aae", fail}, + {"a[^b-d]e", "aze", exact}, + {".*", ".foo", exact}, + {".*", "foo", fail}, + {"abc.def", "abcdef", fail}, + {"abc.def", "abc.def", exact}, + {"abc.def", "abcZdef", fail}, + {"abc?def", "abcZdef", exact}, + {"abc?def", "abcdef", fail}, + {"**/foo/bar", "foo/bar", exact}, + {"**/foo/bar", "dir/foo/bar", exact}, + {"**/foo/bar", "dir/dir2/foo/bar", exact}, + {"abc/**", "abc", fail}, + {"abc/**", "abc/def", exact}, + {"abc/**", "abc/def/ghi", exact}, + {"**/.foo", ".foo", exact}, + {"**/.foo", "bar.foo", fail}, + {"a(b)c/def", "a(b)c/def", exact}, + {"a(b)c/def", "a(b)c/xyz", fail}, + {"a.|)$(}+{bc", "a.|)$(}+{bc", exact}, + {"dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", exact}, + {"dist/*.whl", "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", exact}, } 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}, + {[]string{"**", "!util/docker/web"}, "util/docker/web/foo", fail}, + {[]string{"**", "!util/docker/web", "util/docker/web/foo"}, "util/docker/web/foo", exact}, + {[]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", fail}, + {[]string{"**", "!dist/*.whl"}, "dist/proxy.py-2.4.0rc3.dev36+g08acad9-py3-none-any.whl", fail}, } - if runtime.GOOS != "windows" { + if runtime.GOOS == "windows" { tests = append(tests, []matchesTestCase{ - {"a\\*b", "a*b", true}, + {"a\\\\", "a\\", inexact}, + }...) + } else { + tests = append(tests, []matchesTestCase{ + {"a\\\\", "a\\", exact}, + {"a\\*b", "a*b", exact}, }...) } + t.Run("MatchesExact", func(t *testing.T) { + check := func(pm *PatternMatcher, text string, pass bool, desc string) { + res, _ := pm.MatchesExact(text) + 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 == exact, desc) + } + }) + t.Run("MatchesOrParentMatches", func(t *testing.T) { for _, test := range tests { pm, err := New([]string{test.pattern}) @@ -205,7 +236,7 @@ func TestMatches(t *testing.T) { t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text) } res, _ := pm.MatchesOrParentMatches(test.text) - if test.pass != res { + if (test.pass != fail) != res { t.Fatalf("%v (pattern=%q, text=%q)", err, test.pattern, test.text) } } @@ -216,7 +247,7 @@ func TestMatches(t *testing.T) { t.Fatalf("%v (patterns=%q, text=%q)", err, test.patterns, test.text) } res, _ := pm.MatchesOrParentMatches(test.text) - if test.pass != res { + if (test.pass != fail) != res { t.Errorf("expected: %v, got: %v (patterns=%q, text=%q)", test.pass, res, test.patterns, test.text) } } @@ -240,7 +271,7 @@ func TestMatches(t *testing.T) { } res, _ := pm.MatchesUsingParentResult(test.text, parentMatched) - if test.pass != res { + if (test.pass != fail) != res { t.Errorf("expected: %v, got: %v (pattern=%q, text=%q)", test.pass, res, test.pattern, test.text) } } @@ -271,7 +302,7 @@ func TestMatches(t *testing.T) { t.Fatal(err, desc) } - check(pm, test.text, test.pass, desc) + check(pm, test.text, test.pass != fail, desc) } for _, test := range multiPatternTests { @@ -281,7 +312,7 @@ func TestMatches(t *testing.T) { t.Fatal(err, desc) } - check(pm, test.text, test.pass, desc) + check(pm, test.text, test.pass != fail, desc) } }) @@ -300,7 +331,7 @@ func TestMatches(t *testing.T) { t.Fatal(err, desc) } - check(pm, test.text, test.pass, desc) + check(pm, test.text, test.pass != fail, desc) } for _, test := range multiPatternTests { @@ -310,7 +341,7 @@ func TestMatches(t *testing.T) { t.Fatal(err, desc) } - check(pm, test.text, test.pass, desc) + check(pm, test.text, test.pass != fail, desc) } }) }