From fdd3b14db333c0fd099a21dd51f6a01081e15154 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 17:06:47 +0200 Subject: [PATCH 01/12] filter: test some corner cases --- internal/filter/filter_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 97df452fb..0aedaeb71 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -244,6 +244,7 @@ var filterListTests = []struct { path string match bool }{ + {[]string{}, "/foo/bar/test.go", false}, {[]string{"*.go"}, "/foo/bar/test.go", true}, {[]string{"*.c"}, "/foo/bar/test.go", false}, {[]string{"*.go", "*.c"}, "/foo/bar/test.go", true}, @@ -279,6 +280,38 @@ func ExampleList() { // match: true } +func TestInvalidStrs(t *testing.T) { + _, err := filter.Match("test", "") + if err == nil { + t.Error("Match accepted invalid path") + } + + _, err = filter.ChildMatch("test", "") + if err == nil { + t.Error("ChildMatch accepted invalid path") + } + + patterns := []string{"test"} + _, _, err = filter.List(patterns, "") + if err == nil { + t.Error("List accepted invalid path") + } +} + +func TestInvalidPattern(t *testing.T) { + patterns := []string{"test/["} + _, _, err := filter.List(patterns, "test/example") + if err == nil { + t.Error("List accepted invalid pattern") + } + + patterns = []string{"test/**/["} + _, _, err = filter.List(patterns, "test/example") + if err == nil { + t.Error("List accepted invalid pattern") + } +} + func extractTestLines(t testing.TB) (lines []string) { f, err := os.Open("testdata/libreoffice.txt.bz2") if err != nil { From e73c28114237bfe7d023e6afea53eaafe6c1bd8f Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 15:00:07 +0200 Subject: [PATCH 02/12] filter: Benchmark absolute paths, wildcards and long filter lists --- internal/filter/filter_test.go | 74 ++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 0aedaeb71..6ff83a43b 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -393,30 +393,60 @@ func BenchmarkFilterLines(b *testing.B) { } func BenchmarkFilterPatterns(b *testing.B) { - patterns := []string{ - "sdk/*", - "*.html", - } lines := extractTestLines(b) - var c uint - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - c = 0 - for _, line := range lines { - match, _, err := filter.List(patterns, line) - if err != nil { - b.Fatal(err) - } - - if match { - c++ - } + modlines := make([]string, 200) + for i, line := range lines { + if i >= len(modlines) { + break } + modlines[i] = line + "-does-not-match" + } + tests := []struct { + name string + patterns []string + matches uint + }{ + {"Relative", []string{ + "does-not-match", + "sdk/*", + "*.html", + }, 22185}, + {"Absolute", []string{ + "/etc", + "/home/*/test", + "/usr/share/doc/libreoffice/sdk/docs/java", + }, 150}, + {"Wildcard", []string{ + "/etc/**/example", + "/home/**/test", + "/usr/**/java", + }, 150}, + {"ManyNoMatch", modlines, 0}, + } - if c != 22185 { - b.Fatalf("wrong number of matches: expected 22185, got %d", c) - } + for _, test := range tests { + b.Run(test.name, func(b *testing.B) { + var c uint + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + c = 0 + for _, line := range lines { + match, _, err := filter.List(test.patterns, line) + if err != nil { + b.Fatal(err) + } + + if match { + c++ + } + } + + if c != test.matches { + b.Fatalf("wrong number of matches: expected %d, got %d", test.matches, c) + } + } + }) } } From b8eacd13641525d939200dcee5749abf928c9ec9 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 14:27:59 +0200 Subject: [PATCH 03/12] filter: Reduce redundant path and pattern splitting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single call to filter.List will split the path only once and also split each search pattern only once and use it for both match and childMatch. name old time/op new time/op delta FilterPatterns/Relative-4 62.1ms ±15% 30.3ms ±10% -51.22% (p=0.000 n=9+10) FilterPatterns/Absolute-4 111ms ±10% 49ms ± 3% -56.08% (p=0.000 n=10+8) FilterPatterns/Wildcard-4 393ms ±15% 345ms ± 9% -12.30% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-4 10.0s ± 3% 3.9s ± 2% -60.53% (p=0.000 n=10+9) name old alloc/op new alloc/op delta FilterPatterns/Relative-4 16.4MB ± 0% 4.6MB ± 0% -71.76% (p=0.000 n=10+9) FilterPatterns/Absolute-4 31.4MB ± 0% 8.5MB ± 0% -72.77% (p=0.000 n=9+10) FilterPatterns/Wildcard-4 168MB ± 0% 146MB ± 0% -13.19% (p=0.000 n=10+9) FilterPatterns/ManyNoMatch-4 3.23GB ± 0% 0.91GB ± 0% -71.96% (p=0.000 n=10+9) name old allocs/op new allocs/op delta FilterPatterns/Relative-4 178k ± 0% 67k ± 0% -62.50% (p=0.000 n=10+10) FilterPatterns/Absolute-4 266k ± 0% 89k ± 0% -66.67% (p=0.000 n=10+10) FilterPatterns/Wildcard-4 1.87M ± 0% 1.70M ± 0% -9.47% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-4 17.7M ± 0% 4.5M ± 0% -74.87% (p=0.000 n=9+10) --- internal/filter/filter.go | 71 +++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 74deddb03..795afd179 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -11,6 +11,32 @@ import ( // second argument. var ErrBadString = errors.New("filter.Match: string is empty") +type filterPattern []string + +func prepareStr(str string) ([]string, error) { + if str == "" { + return nil, ErrBadString + } + + // convert file path separator to '/' + if filepath.Separator != '/' { + str = strings.Replace(str, string(filepath.Separator), "/", -1) + } + + return strings.Split(str, "/"), nil +} + +func preparePattern(pattern string) filterPattern { + pattern = filepath.Clean(pattern) + + // convert file path separator to '/' + if filepath.Separator != '/' { + pattern = strings.Replace(pattern, string(filepath.Separator), "/", -1) + } + + return strings.Split(pattern, "/") +} + // Match returns true if str matches the pattern. When the pattern is // malformed, filepath.ErrBadPattern is returned. The empty pattern matches // everything, when str is the empty string ErrBadString is returned. @@ -26,21 +52,13 @@ func Match(pattern, str string) (matched bool, err error) { return true, nil } - pattern = filepath.Clean(pattern) + patterns := preparePattern(pattern) + strs, err := prepareStr(str) - if str == "" { - return false, ErrBadString + if err != nil { + return false, err } - // convert file path separator to '/' - if filepath.Separator != '/' { - pattern = strings.Replace(pattern, string(filepath.Separator), "/", -1) - str = strings.Replace(str, string(filepath.Separator), "/", -1) - } - - patterns := strings.Split(pattern, "/") - strs := strings.Split(str, "/") - return match(patterns, strs) } @@ -59,21 +77,13 @@ func ChildMatch(pattern, str string) (matched bool, err error) { return true, nil } - pattern = filepath.Clean(pattern) + patterns := preparePattern(pattern) + strs, err := prepareStr(str) - if str == "" { - return false, ErrBadString + if err != nil { + return false, err } - // convert file path separator to '/' - if filepath.Separator != '/' { - pattern = strings.Replace(pattern, string(filepath.Separator), "/", -1) - str = strings.Replace(str, string(filepath.Separator), "/", -1) - } - - patterns := strings.Split(pattern, "/") - strs := strings.Split(str, "/") - return childMatch(patterns, strs) } @@ -162,17 +172,26 @@ func match(patterns, strs []string) (matched bool, err error) { // List returns true if str matches one of the patterns. Empty patterns are // ignored. func List(patterns []string, str string) (matched bool, childMayMatch bool, err error) { + if len(patterns) == 0 { + return false, false, nil + } + + strs, err := prepareStr(str) + if err != nil { + return false, false, err + } for _, pat := range patterns { if pat == "" { continue } - m, err := Match(pat, str) + pats := preparePattern(pat) + m, err := match(pats, strs) if err != nil { return false, false, err } - c, err := ChildMatch(pat, str) + c, err := childMatch(pats, strs) if err != nil { return false, false, err } From 375c2a56deee2a53bf035c943ff31b4bd050890c Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 14:39:51 +0200 Subject: [PATCH 04/12] filter: Parse filter patterns only once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit name old time/op new time/op delta FilterPatterns/Relative-4 30.3ms ±10% 23.6ms ±20% -22.12% (p=0.000 n=10+10) FilterPatterns/Absolute-4 49.0ms ± 3% 32.3ms ± 8% -33.94% (p=0.000 n=8+10) FilterPatterns/Wildcard-4 345ms ± 9% 334ms ±17% ~ (p=0.315 n=10+10) FilterPatterns/ManyNoMatch-4 3.93s ± 2% 0.71s ± 7% -81.98% (p=0.000 n=9+10) name old alloc/op new alloc/op delta FilterPatterns/Relative-4 4.63MB ± 0% 3.57MB ± 0% -22.98% (p=0.000 n=9+9) FilterPatterns/Absolute-4 8.54MB ± 0% 3.57MB ± 0% -58.20% (p=0.000 n=10+10) FilterPatterns/Wildcard-4 146MB ± 0% 141MB ± 0% -2.93% (p=0.000 n=9+9) FilterPatterns/ManyNoMatch-4 907MB ± 0% 4MB ± 0% -99.61% (p=0.000 n=9+9) name old allocs/op new allocs/op delta FilterPatterns/Relative-4 66.6k ± 0% 22.2k ± 0% -66.67% (p=0.000 n=10+10) FilterPatterns/Absolute-4 88.7k ± 0% 22.2k ± 0% -75.00% (p=0.000 n=10+10) FilterPatterns/Wildcard-4 1.70M ± 0% 1.63M ± 0% -3.92% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-4 4.46M ± 0% 0.02M ± 0% -99.50% (p=0.000 n=10+10) --- cmd/restic/cmd_restore.go | 12 ++++++---- cmd/restic/exclude.go | 3 ++- internal/filter/filter.go | 41 +++++++++++++++++++++------------- internal/filter/filter_test.go | 28 ++++++++++++----------- 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index e87bafbba..331f2d86f 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -140,13 +140,15 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { return nil } + excludePatterns := filter.ParsePatterns(opts.Exclude) + insensitiveExcludePatterns := filter.ParsePatterns(opts.InsensitiveExclude) selectExcludeFilter := func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) { - matched, _, err := filter.List(opts.Exclude, item) + matched, _, err := filter.List(excludePatterns, item) if err != nil { Warnf("error for exclude pattern: %v", err) } - matchedInsensitive, _, err := filter.List(opts.InsensitiveExclude, strings.ToLower(item)) + matchedInsensitive, _, err := filter.List(insensitiveExcludePatterns, strings.ToLower(item)) if err != nil { Warnf("error for iexclude pattern: %v", err) } @@ -161,13 +163,15 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { return selectedForRestore, childMayBeSelected } + includePatterns := filter.ParsePatterns(opts.Include) + insensitiveIncludePatterns := filter.ParsePatterns(opts.InsensitiveInclude) selectIncludeFilter := func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) { - matched, childMayMatch, err := filter.List(opts.Include, item) + matched, childMayMatch, err := filter.List(includePatterns, item) if err != nil { Warnf("error for include pattern: %v", err) } - matchedInsensitive, childMayMatchInsensitive, err := filter.List(opts.InsensitiveInclude, strings.ToLower(item)) + matchedInsensitive, childMayMatchInsensitive, err := filter.List(insensitiveIncludePatterns, strings.ToLower(item)) if err != nil { Warnf("error for iexclude pattern: %v", err) } diff --git a/cmd/restic/exclude.go b/cmd/restic/exclude.go index 8d5585cfc..0295b00f1 100644 --- a/cmd/restic/exclude.go +++ b/cmd/restic/exclude.go @@ -74,8 +74,9 @@ type RejectFunc func(path string, fi os.FileInfo) bool // rejectByPattern returns a RejectByNameFunc which rejects files that match // one of the patterns. func rejectByPattern(patterns []string) RejectByNameFunc { + parsedPatterns := filter.ParsePatterns(patterns) return func(item string) bool { - matched, _, err := filter.List(patterns, item) + matched, _, err := filter.List(parsedPatterns, item) if err != nil { Warnf("error for exclude pattern: %v", err) } diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 795afd179..67aa70f3e 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -11,7 +11,8 @@ import ( // second argument. var ErrBadString = errors.New("filter.Match: string is empty") -type filterPattern []string +// Pattern represents a preparsed filter pattern +type Pattern []string func prepareStr(str string) ([]string, error) { if str == "" { @@ -26,7 +27,7 @@ func prepareStr(str string) ([]string, error) { return strings.Split(str, "/"), nil } -func preparePattern(pattern string) filterPattern { +func preparePattern(pattern string) Pattern { pattern = filepath.Clean(pattern) // convert file path separator to '/' @@ -87,7 +88,7 @@ func ChildMatch(pattern, str string) (matched bool, err error) { return childMatch(patterns, strs) } -func childMatch(patterns, strs []string) (matched bool, err error) { +func childMatch(patterns Pattern, strs []string) (matched bool, err error) { if patterns[0] != "" { // relative pattern can always be nested down return true, nil @@ -109,7 +110,7 @@ func childMatch(patterns, strs []string) (matched bool, err error) { return match(patterns[0:l], strs) } -func hasDoubleWildcard(list []string) (ok bool, pos int) { +func hasDoubleWildcard(list Pattern) (ok bool, pos int) { for i, item := range list { if item == "**" { return true, i @@ -119,11 +120,11 @@ func hasDoubleWildcard(list []string) (ok bool, pos int) { return false, 0 } -func match(patterns, strs []string) (matched bool, err error) { +func match(patterns Pattern, strs []string) (matched bool, err error) { if ok, pos := hasDoubleWildcard(patterns); ok { // gradually expand '**' into separate wildcards for i := 0; i <= len(strs)-len(patterns)+1; i++ { - newPat := make([]string, pos) + newPat := make(Pattern, pos) copy(newPat, patterns[:pos]) for k := 0; k < i; k++ { newPat = append(newPat, "*") @@ -169,9 +170,22 @@ func match(patterns, strs []string) (matched bool, err error) { return false, nil } -// List returns true if str matches one of the patterns. Empty patterns are -// ignored. -func List(patterns []string, str string) (matched bool, childMayMatch bool, err error) { +// ParsePatterns prepares a list of patterns for use with List. +func ParsePatterns(patterns []string) []Pattern { + patpat := make([]Pattern, 0) + for _, pat := range patterns { + if pat == "" { + continue + } + + pats := preparePattern(pat) + patpat = append(patpat, pats) + } + return patpat +} + +// List returns true if str matches one of the patterns. Empty patterns are ignored. +func List(patterns []Pattern, str string) (matched bool, childMayMatch bool, err error) { if len(patterns) == 0 { return false, false, nil } @@ -181,17 +195,12 @@ func List(patterns []string, str string) (matched bool, childMayMatch bool, err return false, false, err } for _, pat := range patterns { - if pat == "" { - continue - } - - pats := preparePattern(pat) - m, err := match(pats, strs) + m, err := match(pat, strs) if err != nil { return false, false, err } - c, err := childMatch(pats, strs) + c, err := childMatch(pat, strs) if err != nil { return false, false, err } diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 6ff83a43b..f7cf1dd82 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -259,7 +259,8 @@ var filterListTests = []struct { func TestList(t *testing.T) { for i, test := range filterListTests { - match, _, err := filter.List(test.patterns, test.path) + patterns := filter.ParsePatterns(test.patterns) + match, _, err := filter.List(patterns, test.path) if err != nil { t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v", i, test.patterns, err) @@ -274,7 +275,8 @@ func TestList(t *testing.T) { } func ExampleList() { - match, _, _ := filter.List([]string{"*.c", "*.go"}, "/home/user/file.go") + patterns := filter.ParsePatterns([]string{"*.c", "*.go"}) + match, _, _ := filter.List(patterns, "/home/user/file.go") fmt.Printf("match: %v\n", match) // Output: // match: true @@ -292,7 +294,7 @@ func TestInvalidStrs(t *testing.T) { } patterns := []string{"test"} - _, _, err = filter.List(patterns, "") + _, _, err = filter.List(filter.ParsePatterns(patterns), "") if err == nil { t.Error("List accepted invalid path") } @@ -300,13 +302,13 @@ func TestInvalidStrs(t *testing.T) { func TestInvalidPattern(t *testing.T) { patterns := []string{"test/["} - _, _, err := filter.List(patterns, "test/example") + _, _, err := filter.List(filter.ParsePatterns(patterns), "test/example") if err == nil { t.Error("List accepted invalid pattern") } patterns = []string{"test/**/["} - _, _, err = filter.List(patterns, "test/example") + _, _, err = filter.List(filter.ParsePatterns(patterns), "test/example") if err == nil { t.Error("List accepted invalid pattern") } @@ -403,25 +405,25 @@ func BenchmarkFilterPatterns(b *testing.B) { } tests := []struct { name string - patterns []string + patterns []filter.Pattern matches uint }{ - {"Relative", []string{ + {"Relative", filter.ParsePatterns([]string{ "does-not-match", "sdk/*", "*.html", - }, 22185}, - {"Absolute", []string{ + }), 22185}, + {"Absolute", filter.ParsePatterns([]string{ "/etc", "/home/*/test", "/usr/share/doc/libreoffice/sdk/docs/java", - }, 150}, - {"Wildcard", []string{ + }), 150}, + {"Wildcard", filter.ParsePatterns([]string{ "/etc/**/example", "/home/**/test", "/usr/**/java", - }, 150}, - {"ManyNoMatch", modlines, 0}, + }), 150}, + {"ManyNoMatch", filter.ParsePatterns(modlines), 0}, } for _, test := range tests { From 79597962690714abcbeba34b477d2191f431138f Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 6 Jul 2019 22:12:24 +0200 Subject: [PATCH 05/12] filter: Special case for absolute paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit name old time/op new time/op delta FilterPatterns/Relative-4 23.6ms ±20% 22.7ms ± 5% ~ (p=0.684 n=10+10) FilterPatterns/Absolute-4 32.3ms ± 8% 14.2ms ±13% -56.01% (p=0.000 n=10+10) FilterPatterns/Wildcard-4 334ms ±17% 266ms ±16% -20.56% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-4 709ms ± 7% 554ms ± 6% -21.89% (p=0.000 n=10+10) name old alloc/op new alloc/op delta FilterPatterns/Relative-4 3.57MB ± 0% 3.57MB ± 0% +0.00% (p=0.046 n=9+10) FilterPatterns/Absolute-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.464 n=10+10) FilterPatterns/Wildcard-4 141MB ± 0% 141MB ± 0% ~ (p=0.163 n=9+10) FilterPatterns/ManyNoMatch-4 3.57MB ± 0% 3.57MB ± 0% ~ (all equal) name old allocs/op new allocs/op delta FilterPatterns/Relative-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Absolute-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Wildcard-4 1.63M ± 0% 1.63M ± 0% ~ (p=0.072 n=10+10) FilterPatterns/ManyNoMatch-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) --- internal/filter/filter.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 67aa70f3e..cb4fb1702 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -149,8 +149,13 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { } if len(patterns) <= len(strs) { + maxOffset := len(strs) - len(patterns) + // special case absolute patterns + if patterns[0] == "" { + maxOffset = 0 + } outer: - for offset := len(strs) - len(patterns); offset >= 0; offset-- { + for offset := maxOffset; offset >= 0; offset-- { for i := len(patterns) - 1; i >= 0; i-- { ok, err := filepath.Match(patterns[i], strs[offset+i]) From 17c53efb0d4d9e0f38ba91231a92dea27315d3c5 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 15:51:17 +0200 Subject: [PATCH 06/12] filter: Optimize double wildcard expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This only allocates a single slice to expand the double wildcard and only copies the pattern prefix once. name old time/op new time/op delta FilterPatterns/Relative-4 22.7ms ± 5% 23.3ms ± 9% ~ (p=0.353 n=10+10) FilterPatterns/Absolute-4 14.2ms ±13% 13.9ms ± 7% ~ (p=0.853 n=10+10) FilterPatterns/Wildcard-4 266ms ±16% 51ms ± 7% -80.67% (p=0.000 n=10+9) FilterPatterns/ManyNoMatch-4 554ms ± 6% 551ms ± 9% ~ (p=0.436 n=10+10) name old alloc/op new alloc/op delta FilterPatterns/Relative-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.349 n=10+10) FilterPatterns/Absolute-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.073 n=10+9) FilterPatterns/Wildcard-4 141MB ± 0% 14MB ± 0% -89.89% (p=0.000 n=10+9) FilterPatterns/ManyNoMatch-4 3.57MB ± 0% 3.57MB ± 0% ~ (all equal) name old allocs/op new allocs/op delta FilterPatterns/Relative-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Absolute-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Wildcard-4 1.63M ± 0% 0.09M ± 0% -94.56% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) --- internal/filter/filter.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index cb4fb1702..5eb119f49 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -123,11 +123,15 @@ func hasDoubleWildcard(list Pattern) (ok bool, pos int) { func match(patterns Pattern, strs []string) (matched bool, err error) { if ok, pos := hasDoubleWildcard(patterns); ok { // gradually expand '**' into separate wildcards + newPat := make(Pattern, len(strs)) + // copy static prefix once + copy(newPat, patterns[:pos]) for i := 0; i <= len(strs)-len(patterns)+1; i++ { - newPat := make(Pattern, pos) - copy(newPat, patterns[:pos]) - for k := 0; k < i; k++ { - newPat = append(newPat, "*") + // limit to static prefix and already appended '*' + newPat := newPat[:pos+i] + // in the first iteration the wildcard expands to nothing + if i > 0 { + newPat[pos+i-1] = "*" } newPat = append(newPat, patterns[pos+1:]...) From bcc3bddcf43ae008de314485dd5bc27ad65b369e Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 19:46:41 +0200 Subject: [PATCH 07/12] filter: only check whether a child path could match when necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When checking excludes there is no need to test whether a child path could also match the pattern, as it is by definition excluded. Previously childMayMatch was calculated but then discarded. For simple absolute paths this can account for half the time spent for checking pattern matches. name old time/op new time/op delta FilterPatterns/Relative-4 23.3ms ± 9% 21.7ms ± 6% -6.68% (p=0.004 n=10+10) FilterPatterns/Absolute-4 13.9ms ± 7% 10.0ms ± 5% -27.61% (p=0.000 n=10+10) FilterPatterns/Wildcard-4 51.4ms ± 7% 47.0ms ± 7% -8.51% (p=0.001 n=9+9) FilterPatterns/ManyNoMatch-4 551ms ± 9% 190ms ± 1% -65.41% (p=0.000 n=10+8) name old alloc/op new alloc/op delta FilterPatterns/Relative-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.665 n=10+9) FilterPatterns/Absolute-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.480 n=9+10) FilterPatterns/Wildcard-4 14.3MB ± 0% 14.3MB ± 0% ~ (p=0.431 n=9+10) FilterPatterns/ManyNoMatch-4 3.57MB ± 0% 3.57MB ± 0% ~ (all equal) name old allocs/op new allocs/op delta FilterPatterns/Relative-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Absolute-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Wildcard-4 88.7k ± 0% 88.7k ± 0% ~ (all equal) FilterPatterns/ManyNoMatch-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) --- cmd/restic/cmd_restore.go | 8 ++++---- cmd/restic/exclude.go | 2 +- internal/filter/filter.go | 24 ++++++++++++++++++++---- internal/filter/filter_test.go | 12 ++++++------ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 331f2d86f..da3818b63 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -143,12 +143,12 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { excludePatterns := filter.ParsePatterns(opts.Exclude) insensitiveExcludePatterns := filter.ParsePatterns(opts.InsensitiveExclude) selectExcludeFilter := func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) { - matched, _, err := filter.List(excludePatterns, item) + matched, err := filter.List(excludePatterns, item) if err != nil { Warnf("error for exclude pattern: %v", err) } - matchedInsensitive, _, err := filter.List(insensitiveExcludePatterns, strings.ToLower(item)) + matchedInsensitive, err := filter.List(insensitiveExcludePatterns, strings.ToLower(item)) if err != nil { Warnf("error for iexclude pattern: %v", err) } @@ -166,12 +166,12 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { includePatterns := filter.ParsePatterns(opts.Include) insensitiveIncludePatterns := filter.ParsePatterns(opts.InsensitiveInclude) selectIncludeFilter := func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) { - matched, childMayMatch, err := filter.List(includePatterns, item) + matched, childMayMatch, err := filter.ListWithChild(includePatterns, item) if err != nil { Warnf("error for include pattern: %v", err) } - matchedInsensitive, childMayMatchInsensitive, err := filter.List(insensitiveIncludePatterns, strings.ToLower(item)) + matchedInsensitive, childMayMatchInsensitive, err := filter.ListWithChild(insensitiveIncludePatterns, strings.ToLower(item)) if err != nil { Warnf("error for iexclude pattern: %v", err) } diff --git a/cmd/restic/exclude.go b/cmd/restic/exclude.go index 0295b00f1..db603c04e 100644 --- a/cmd/restic/exclude.go +++ b/cmd/restic/exclude.go @@ -76,7 +76,7 @@ type RejectFunc func(path string, fi os.FileInfo) bool func rejectByPattern(patterns []string) RejectByNameFunc { parsedPatterns := filter.ParsePatterns(patterns) return func(item string) bool { - matched, _, err := filter.List(parsedPatterns, item) + matched, err := filter.List(parsedPatterns, item) if err != nil { Warnf("error for exclude pattern: %v", err) } diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 5eb119f49..90526ddcc 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -194,7 +194,18 @@ func ParsePatterns(patterns []string) []Pattern { } // List returns true if str matches one of the patterns. Empty patterns are ignored. -func List(patterns []Pattern, str string) (matched bool, childMayMatch bool, err error) { +func List(patterns []Pattern, str string) (matched bool, err error) { + matched, _, err = list(patterns, false, str) + return matched, err +} + +// ListWithChild returns true if str matches one of the patterns. Empty patterns are ignored. +func ListWithChild(patterns []Pattern, str string) (matched bool, childMayMatch bool, err error) { + return list(patterns, true, str) +} + +// List returns true if str matches one of the patterns. Empty patterns are ignored. +func list(patterns []Pattern, checkChildMatches bool, str string) (matched bool, childMayMatch bool, err error) { if len(patterns) == 0 { return false, false, nil } @@ -209,9 +220,14 @@ func List(patterns []Pattern, str string) (matched bool, childMayMatch bool, err return false, false, err } - c, err := childMatch(pat, strs) - if err != nil { - return false, false, err + var c bool + if checkChildMatches { + c, err = childMatch(pat, strs) + if err != nil { + return false, false, err + } + } else { + c = true } matched = matched || m diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index f7cf1dd82..9d487a582 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -260,7 +260,7 @@ var filterListTests = []struct { func TestList(t *testing.T) { for i, test := range filterListTests { patterns := filter.ParsePatterns(test.patterns) - match, _, err := filter.List(patterns, test.path) + match, err := filter.List(patterns, test.path) if err != nil { t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v", i, test.patterns, err) @@ -276,7 +276,7 @@ func TestList(t *testing.T) { func ExampleList() { patterns := filter.ParsePatterns([]string{"*.c", "*.go"}) - match, _, _ := filter.List(patterns, "/home/user/file.go") + match, _ := filter.List(patterns, "/home/user/file.go") fmt.Printf("match: %v\n", match) // Output: // match: true @@ -294,7 +294,7 @@ func TestInvalidStrs(t *testing.T) { } patterns := []string{"test"} - _, _, err = filter.List(filter.ParsePatterns(patterns), "") + _, err = filter.List(filter.ParsePatterns(patterns), "") if err == nil { t.Error("List accepted invalid path") } @@ -302,13 +302,13 @@ func TestInvalidStrs(t *testing.T) { func TestInvalidPattern(t *testing.T) { patterns := []string{"test/["} - _, _, err := filter.List(filter.ParsePatterns(patterns), "test/example") + _, err := filter.List(filter.ParsePatterns(patterns), "test/example") if err == nil { t.Error("List accepted invalid pattern") } patterns = []string{"test/**/["} - _, _, err = filter.List(filter.ParsePatterns(patterns), "test/example") + _, err = filter.List(filter.ParsePatterns(patterns), "test/example") if err == nil { t.Error("List accepted invalid pattern") } @@ -435,7 +435,7 @@ func BenchmarkFilterPatterns(b *testing.B) { for i := 0; i < b.N; i++ { c = 0 for _, line := range lines { - match, _, err := filter.List(test.patterns, line) + match, err := filter.List(test.patterns, line) if err != nil { b.Fatal(err) } From 54a124de3b2083931aa0240460ec6c8d459e51d8 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 20:12:38 +0200 Subject: [PATCH 08/12] filter: explicitly test separate ListWithChild function --- internal/filter/filter_test.go | 43 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go index 9d487a582..ee7f05a32 100644 --- a/internal/filter/filter_test.go +++ b/internal/filter/filter_test.go @@ -240,21 +240,22 @@ func ExampleMatch_wildcards() { } var filterListTests = []struct { - patterns []string - path string - match bool + patterns []string + path string + match bool + childMatch bool }{ - {[]string{}, "/foo/bar/test.go", false}, - {[]string{"*.go"}, "/foo/bar/test.go", true}, - {[]string{"*.c"}, "/foo/bar/test.go", false}, - {[]string{"*.go", "*.c"}, "/foo/bar/test.go", true}, - {[]string{"*"}, "/foo/bar/test.go", true}, - {[]string{"x"}, "/foo/bar/test.go", false}, - {[]string{"?"}, "/foo/bar/test.go", false}, - {[]string{"?", "x"}, "/foo/bar/x", true}, - {[]string{"/*/*/bar/test.*"}, "/foo/bar/test.go", false}, - {[]string{"/*/*/bar/test.*", "*.go"}, "/foo/bar/test.go", true}, - {[]string{"", "*.c"}, "/foo/bar/test.go", false}, + {[]string{}, "/foo/bar/test.go", false, false}, + {[]string{"*.go"}, "/foo/bar/test.go", true, true}, + {[]string{"*.c"}, "/foo/bar/test.go", false, true}, + {[]string{"*.go", "*.c"}, "/foo/bar/test.go", true, true}, + {[]string{"*"}, "/foo/bar/test.go", true, true}, + {[]string{"x"}, "/foo/bar/test.go", false, true}, + {[]string{"?"}, "/foo/bar/test.go", false, true}, + {[]string{"?", "x"}, "/foo/bar/x", true, true}, + {[]string{"/*/*/bar/test.*"}, "/foo/bar/test.go", false, false}, + {[]string{"/*/*/bar/test.*", "*.go"}, "/foo/bar/test.go", true, true}, + {[]string{"", "*.c"}, "/foo/bar/test.go", false, true}, } func TestList(t *testing.T) { @@ -268,9 +269,21 @@ func TestList(t *testing.T) { } if match != test.match { - t.Errorf("test %d: filter.MatchList(%q, %q): expected %v, got %v", + t.Errorf("test %d: filter.List(%q, %q): expected %v, got %v", i, test.patterns, test.path, test.match, match) } + + match, childMatch, err := filter.ListWithChild(patterns, test.path) + if err != nil { + t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v", + i, test.patterns, err) + continue + } + + if match != test.match || childMatch != test.childMatch { + t.Errorf("test %d: filter.ListWithChild(%q, %q): expected %v, %v, got %v, %v", + i, test.patterns, test.path, test.match, test.childMatch, match, childMatch) + } } } From 0acc3c592347db1ea65bae980e5c2b92406163c6 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 20:55:43 +0200 Subject: [PATCH 09/12] filter: special case patterns without globbing characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In case a part of a path is a simple string, we can just check for equality without complex parsing in filepath.Match. name old time/op new time/op delta FilterLines-4 34.8ms ±17% 41.2ms ±23% +18.36% (p=0.000 n=10+10) FilterPatterns/Relative-4 21.7ms ± 6% 12.1ms ±23% -44.46% (p=0.000 n=10+10) FilterPatterns/Absolute-4 10.0ms ± 5% 9.1ms ±11% -9.80% (p=0.006 n=10+9) FilterPatterns/Wildcard-4 47.0ms ± 7% 42.2ms ± 5% -10.19% (p=0.000 n=9+10) FilterPatterns/ManyNoMatch-4 190ms ± 1% 131ms ±20% -31.47% (p=0.000 n=8+10) name old alloc/op new alloc/op delta FilterPatterns/Relative-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.870 n=9+9) FilterPatterns/Absolute-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.145 n=10+10) FilterPatterns/Wildcard-4 14.3MB ± 0% 19.7MB ± 0% +37.91% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-4 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.421 n=10+9) name old allocs/op new allocs/op delta FilterPatterns/Relative-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Absolute-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Wildcard-4 88.7k ± 0% 88.7k ± 0% ~ (all equal) FilterPatterns/ManyNoMatch-4 22.2k ± 0% 22.2k ± 0% ~ (all equal) --- internal/filter/filter.go | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 90526ddcc..f1aa20e94 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -11,8 +11,13 @@ import ( // second argument. var ErrBadString = errors.New("filter.Match: string is empty") +type patternPart struct { + pattern string + isSimple bool +} + // Pattern represents a preparsed filter pattern -type Pattern []string +type Pattern []patternPart func prepareStr(str string) ([]string, error) { if str == "" { @@ -35,7 +40,14 @@ func preparePattern(pattern string) Pattern { pattern = strings.Replace(pattern, string(filepath.Separator), "/", -1) } - return strings.Split(pattern, "/") + parts := strings.Split(pattern, "/") + patterns := make([]patternPart, len(parts)) + for i, part := range parts { + isSimple := !strings.ContainsAny(part, "\\[]*?") + patterns[i] = patternPart{part, isSimple} + } + + return patterns } // Match returns true if str matches the pattern. When the pattern is @@ -89,7 +101,7 @@ func ChildMatch(pattern, str string) (matched bool, err error) { } func childMatch(patterns Pattern, strs []string) (matched bool, err error) { - if patterns[0] != "" { + if patterns[0].pattern != "" { // relative pattern can always be nested down return true, nil } @@ -112,7 +124,7 @@ func childMatch(patterns Pattern, strs []string) (matched bool, err error) { func hasDoubleWildcard(list Pattern) (ok bool, pos int) { for i, item := range list { - if item == "**" { + if item.pattern == "**" { return true, i } } @@ -131,7 +143,7 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { newPat := newPat[:pos+i] // in the first iteration the wildcard expands to nothing if i > 0 { - newPat[pos+i-1] = "*" + newPat[pos+i-1] = patternPart{"*", false} } newPat = append(newPat, patterns[pos+1:]...) @@ -155,16 +167,21 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { if len(patterns) <= len(strs) { maxOffset := len(strs) - len(patterns) // special case absolute patterns - if patterns[0] == "" { + if patterns[0].pattern == "" { maxOffset = 0 } outer: for offset := maxOffset; offset >= 0; offset-- { for i := len(patterns) - 1; i >= 0; i-- { - ok, err := filepath.Match(patterns[i], strs[offset+i]) - if err != nil { - return false, errors.Wrap(err, "Match") + var ok bool + if patterns[i].isSimple { + ok = patterns[i].pattern == strs[offset+i] + } else { + ok, err = filepath.Match(patterns[i].pattern, strs[offset+i]) + if err != nil { + return false, errors.Wrap(err, "Match") + } } if !ok { From 8388e67c4bf7632f93688585bfa006adc97d02f9 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Wed, 7 Oct 2020 21:14:07 +0200 Subject: [PATCH 10/12] filter: cleanup path separator conversion --- internal/filter/filter.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index f1aa20e94..1589819da 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -24,21 +24,13 @@ func prepareStr(str string) ([]string, error) { return nil, ErrBadString } - // convert file path separator to '/' - if filepath.Separator != '/' { - str = strings.Replace(str, string(filepath.Separator), "/", -1) - } - + str = filepath.ToSlash(str) return strings.Split(str, "/"), nil } func preparePattern(pattern string) Pattern { pattern = filepath.Clean(pattern) - - // convert file path separator to '/' - if filepath.Separator != '/' { - pattern = strings.Replace(pattern, string(filepath.Separator), "/", -1) - } + pattern = filepath.ToSlash(pattern) parts := strings.Split(pattern, "/") patterns := make([]patternPart, len(parts)) From 740758a5fa512707155bea83a6faacca8ad991d1 Mon Sep 17 00:00:00 2001 From: greatroar <@> Date: Thu, 8 Oct 2020 11:00:25 +0200 Subject: [PATCH 11/12] Optimize filter pattern matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By replacing "**" with "", checking for this special path component can be reduced to a length-zero check. name old time/op new time/op delta FilterLines-8 44.7ms ± 5% 44.9ms ± 5% ~ (p=0.631 n=10+10) FilterPatterns/Relative-8 13.6ms ± 4% 13.4ms ± 5% ~ (p=0.165 n=10+10) FilterPatterns/Absolute-8 10.9ms ± 5% 10.7ms ± 4% ~ (p=0.052 n=10+10) FilterPatterns/Wildcard-8 53.7ms ± 5% 50.4ms ± 5% -6.00% (p=0.000 n=10+10) FilterPatterns/ManyNoMatch-8 128ms ± 2% 95ms ± 1% -25.54% (p=0.000 n=10+10) name old alloc/op new alloc/op delta FilterPatterns/Relative-8 3.57MB ± 0% 3.57MB ± 0% ~ (p=1.000 n=9+8) FilterPatterns/Absolute-8 3.57MB ± 0% 3.57MB ± 0% ~ (p=0.903 n=9+8) FilterPatterns/Wildcard-8 19.7MB ± 0% 19.7MB ± 0% -0.00% (p=0.022 n=10+9) FilterPatterns/ManyNoMatch-8 3.57MB ± 0% 3.57MB ± 0% ~ (all equal) name old allocs/op new allocs/op delta FilterPatterns/Relative-8 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Absolute-8 22.2k ± 0% 22.2k ± 0% ~ (all equal) FilterPatterns/Wildcard-8 88.7k ± 0% 88.7k ± 0% ~ (all equal) FilterPatterns/ManyNoMatch-8 22.2k ± 0% 22.2k ± 0% ~ (all equal) --- internal/filter/filter.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 1589819da..50ed6d7fd 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -12,7 +12,7 @@ import ( var ErrBadString = errors.New("filter.Match: string is empty") type patternPart struct { - pattern string + pattern string // First is "/" for absolute pattern; "" for "**". isSimple bool } @@ -23,25 +23,35 @@ func prepareStr(str string) ([]string, error) { if str == "" { return nil, ErrBadString } - - str = filepath.ToSlash(str) - return strings.Split(str, "/"), nil + return splitPath(str), nil } func preparePattern(pattern string) Pattern { - pattern = filepath.Clean(pattern) - pattern = filepath.ToSlash(pattern) - - parts := strings.Split(pattern, "/") + parts := splitPath(filepath.Clean(pattern)) patterns := make([]patternPart, len(parts)) for i, part := range parts { isSimple := !strings.ContainsAny(part, "\\[]*?") + // Replace "**" with the empty string to get faster comparisons + // (length-check only) in hasDoubleWildcard. + if part == "**" { + part = "" + } patterns[i] = patternPart{part, isSimple} } return patterns } +// Split p into path components. Assuming p has been Cleaned, no component +// will be empty. For absolute paths, the first component is "/". +func splitPath(p string) []string { + parts := strings.Split(filepath.ToSlash(p), "/") + if parts[0] == "" { + parts[0] = "/" + } + return parts +} + // Match returns true if str matches the pattern. When the pattern is // malformed, filepath.ErrBadPattern is returned. The empty pattern matches // everything, when str is the empty string ErrBadString is returned. @@ -93,7 +103,7 @@ func ChildMatch(pattern, str string) (matched bool, err error) { } func childMatch(patterns Pattern, strs []string) (matched bool, err error) { - if patterns[0].pattern != "" { + if patterns[0].pattern != "/" { // relative pattern can always be nested down return true, nil } @@ -116,7 +126,7 @@ func childMatch(patterns Pattern, strs []string) (matched bool, err error) { func hasDoubleWildcard(list Pattern) (ok bool, pos int) { for i, item := range list { - if item.pattern == "**" { + if item.pattern == "" { return true, i } } @@ -159,7 +169,7 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { if len(patterns) <= len(strs) { maxOffset := len(strs) - len(patterns) // special case absolute patterns - if patterns[0].pattern == "" { + if patterns[0].pattern == "/" { maxOffset = 0 } outer: From 88c8e903d25d664f75be6987bb477b5072d8d488 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 9 Oct 2020 16:11:05 +0200 Subject: [PATCH 12/12] filter: Fix glob matching on absolute path marker on windows A pattern part containing "/" is used to mark a path or a pattern as absolute. However, on Windows the path separator is "\" such that glob patterns like "?" could match the marker. The code now explicitly skips the marker when the pattern does not represent an absolute path. --- internal/filter/filter.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 50ed6d7fd..1f6f04133 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -167,13 +167,17 @@ func match(patterns Pattern, strs []string) (matched bool, err error) { } if len(patterns) <= len(strs) { + minOffset := 0 maxOffset := len(strs) - len(patterns) // special case absolute patterns if patterns[0].pattern == "/" { maxOffset = 0 + } else if strs[0] == "/" { + // skip absolute path marker if pattern is not rooted + minOffset = 1 } outer: - for offset := maxOffset; offset >= 0; offset-- { + for offset := maxOffset; offset >= minOffset; offset-- { for i := len(patterns) - 1; i >= 0; i-- { var ok bool