1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

pkg/fileutils: Track incremental pattern match results against each pattern

The existing code does not correctly handle the case where a file
matches one of the patterns, but should not match overall because of an
exclude pattern that applied to a parent directory (see
https://github.com/docker/buildx/issues/850).

Fix this by independently tracking the results of matching against each
pattern. A file should be considered to match any pattern that matched a
parent dir.

Signed-off-by: Aaron Lehmann <alehmann@netflix.com>
This commit is contained in:
Aaron Lehmann 2021-11-19 13:19:18 -08:00
parent ea0f3dc8f4
commit 4555d3aa54
3 changed files with 133 additions and 7 deletions

View file

@ -866,8 +866,8 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
rebaseName := options.RebaseNames[include] rebaseName := options.RebaseNames[include]
var ( var (
parentMatched []bool parentMatchInfo []fileutils.MatchInfo
parentDirs []string parentDirs []string
) )
walkRoot := getWalkRoot(srcPath, include) walkRoot := getWalkRoot(srcPath, include)
@ -902,13 +902,14 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
break break
} }
parentDirs = parentDirs[:len(parentDirs)-1] parentDirs = parentDirs[:len(parentDirs)-1]
parentMatched = parentMatched[:len(parentMatched)-1] parentMatchInfo = parentMatchInfo[:len(parentMatchInfo)-1]
} }
if len(parentMatched) != 0 { var matchInfo fileutils.MatchInfo
skip, err = pm.MatchesUsingParentResult(relFilePath, parentMatched[len(parentMatched)-1]) if len(parentMatchInfo) != 0 {
skip, matchInfo, err = pm.MatchesUsingParentResults(relFilePath, parentMatchInfo[len(parentMatchInfo)-1])
} else { } else {
skip, err = pm.MatchesOrParentMatches(relFilePath) skip, matchInfo, err = pm.MatchesUsingParentResults(relFilePath, fileutils.MatchInfo{})
} }
if err != nil { if err != nil {
logrus.Errorf("Error matching %s: %v", relFilePath, err) logrus.Errorf("Error matching %s: %v", relFilePath, err)
@ -917,7 +918,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
if f.IsDir() { if f.IsDir() {
parentDirs = append(parentDirs, relFilePath) parentDirs = append(parentDirs, relFilePath)
parentMatched = append(parentMatched, skip) parentMatchInfo = append(parentMatchInfo, matchInfo)
} }
} }

View file

@ -172,6 +172,9 @@ func (pm *PatternMatcher) MatchesOrParentMatches(file string) (bool, error) {
// The "file" argument should be a slash-delimited path. // The "file" argument should be a slash-delimited path.
// //
// MatchesUsingParentResult is not safe to call concurrently. // MatchesUsingParentResult is not safe to call concurrently.
//
// Deprecated in favor of MatchesUsingParentResults: this function does behave
// correctly in some cases (see https://github.com/docker/buildx/issues/850).
func (pm *PatternMatcher) MatchesUsingParentResult(file string, parentMatched bool) (bool, error) { func (pm *PatternMatcher) MatchesUsingParentResult(file string, parentMatched bool) (bool, error) {
matched := parentMatched matched := parentMatched
file = filepath.FromSlash(file) file = filepath.FromSlash(file)
@ -196,6 +199,78 @@ func (pm *PatternMatcher) MatchesUsingParentResult(file string, parentMatched bo
return matched, nil 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 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 // Exclusions returns true if any of the patterns define exclusions
func (pm *PatternMatcher) Exclusions() bool { func (pm *PatternMatcher) Exclusions() bool {
return pm.exclusions return pm.exclusions

View file

@ -309,6 +309,12 @@ type matchesTestCase struct {
pass bool pass bool
} }
type multiPatternTestCase struct {
patterns []string
text string
pass bool
}
func TestMatches(t *testing.T) { func TestMatches(t *testing.T) {
tests := []matchesTestCase{ tests := []matchesTestCase{
{"**", "file", true}, {"**", "file", true},
@ -377,6 +383,10 @@ func TestMatches(t *testing.T) {
{"a(b)c/def", "a(b)c/xyz", false}, {"a(b)c/def", "a(b)c/xyz", false},
{"a.|)$(}+{bc", "a.|)$(}+{bc", true}, {"a.|)$(}+{bc", "a.|)$(}+{bc", 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},
}
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
tests = append(tests, []matchesTestCase{ tests = append(tests, []matchesTestCase{
@ -392,6 +402,14 @@ func TestMatches(t *testing.T) {
res, _ := pm.MatchesOrParentMatches(test.text) res, _ := pm.MatchesOrParentMatches(test.text)
assert.Check(t, is.Equal(test.pass, res), desc) assert.Check(t, is.Equal(test.pass, res), desc)
} }
for _, test := range multiPatternTests {
desc := fmt.Sprintf("patterns=%q text=%q", test.patterns, test.text)
pm, err := NewPatternMatcher(test.patterns)
assert.NilError(t, err, desc)
res, _ := pm.MatchesOrParentMatches(test.text)
assert.Check(t, is.Equal(test.pass, res), desc)
}
}) })
t.Run("MatchesUsingParentResult", func(t *testing.T) { t.Run("MatchesUsingParentResult", func(t *testing.T) {
@ -415,6 +433,38 @@ func TestMatches(t *testing.T) {
} }
}) })
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)
assert.Check(t, is.Equal(pass, res), desc)
}
for _, test := range tests {
desc := fmt.Sprintf("pattern=%q text=%q", test.pattern, test.text)
pm, err := NewPatternMatcher([]string{test.pattern})
assert.NilError(t, 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 := NewPatternMatcher(test.patterns)
assert.NilError(t, err, desc)
check(pm, test.text, test.pass, desc)
}
})
} }
func TestCleanPatterns(t *testing.T) { func TestCleanPatterns(t *testing.T) {