diff options
author | Russ Cox <rsc@golang.org> | 2020-10-22 12:11:29 -0400 |
---|---|---|
committer | Russ Cox <rsc@golang.org> | 2020-10-23 14:59:03 +0000 |
commit | b5ddc42b465dd5b9532ee336d98343d81a6d35b2 (patch) | |
tree | d3ea8f2fe5c41f4befebaeda5600832839b0bbbb /src/path | |
parent | 4a2cc73f8789e3df43c1c96944c90f55757a23b0 (diff) | |
download | go-b5ddc42b465dd5b9532ee336d98343d81a6d35b2.tar.gz go-b5ddc42b465dd5b9532ee336d98343d81a6d35b2.zip |
io/fs, path, path/filepath, testing/fstest: validate patterns in Match, Glob
According to #28614, proposal review agreed in December 2018 that
Match should return an error for failed matches where the unmatched
part of the pattern has a syntax error. (The failed match has to date
caused the scan of the pattern to stop early.)
This change implements that behavior: the match loop continues
scanning to the end of the pattern, even after a confirmed mismatch,
to check whether the pattern is even well-formed.
The change applies to both path.Match and filepath.Match.
Then filepath.Glob and fs.Glob make a single validity-checking
call to Match before beginning their usual processing.
Also update fstest.TestFS to check for correct validation in custom
Glob implementations.
Fixes #28614.
Change-Id: Ic1d35a4bb9c3565184ae83dbefc425c5c96318e7
Reviewed-on: https://go-review.googlesource.com/c/go/+/264397
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
Diffstat (limited to 'src/path')
-rw-r--r-- | src/path/filepath/match.go | 61 | ||||
-rw-r--r-- | src/path/filepath/match_test.go | 12 | ||||
-rw-r--r-- | src/path/match.go | 60 | ||||
-rw-r--r-- | src/path/match_test.go | 4 |
4 files changed, 89 insertions, 48 deletions
diff --git a/src/path/filepath/match.go b/src/path/filepath/match.go index 20a334805b..c77a26952a 100644 --- a/src/path/filepath/match.go +++ b/src/path/filepath/match.go @@ -122,25 +122,28 @@ Scan: // If so, it returns the remainder of s (after the match). // Chunk is all single-character operators: literals, char classes, and ?. func matchChunk(chunk, s string) (rest string, ok bool, err error) { + // failed records whether the match has failed. + // After the match fails, the loop continues on processing chunk, + // checking that the pattern is well-formed but no longer reading s. + failed := false for len(chunk) > 0 { - if len(s) == 0 { - return + if !failed && len(s) == 0 { + failed = true } switch chunk[0] { case '[': // character class - r, n := utf8.DecodeRuneInString(s) - s = s[n:] - chunk = chunk[1:] - // We can't end right after '[', we're expecting at least - // a closing bracket and possibly a caret. - if len(chunk) == 0 { - err = ErrBadPattern - return + var r rune + if !failed { + var n int + r, n = utf8.DecodeRuneInString(s) + s = s[n:] } + chunk = chunk[1:] // possibly negated - negated := chunk[0] == '^' - if negated { + negated := false + if len(chunk) > 0 && chunk[0] == '^' { + negated = true chunk = chunk[1:] } // parse all ranges @@ -153,12 +156,12 @@ func matchChunk(chunk, s string) (rest string, ok bool, err error) { } var lo, hi rune if lo, chunk, err = getEsc(chunk); err != nil { - return + return "", false, err } hi = lo if chunk[0] == '-' { if hi, chunk, err = getEsc(chunk[1:]); err != nil { - return + return "", false, err } } if lo <= r && r <= hi { @@ -167,35 +170,41 @@ func matchChunk(chunk, s string) (rest string, ok bool, err error) { nrange++ } if match == negated { - return + failed = true } case '?': - if s[0] == Separator { - return + if !failed { + if s[0] == Separator { + failed = true + } + _, n := utf8.DecodeRuneInString(s) + s = s[n:] } - _, n := utf8.DecodeRuneInString(s) - s = s[n:] chunk = chunk[1:] case '\\': if runtime.GOOS != "windows" { chunk = chunk[1:] if len(chunk) == 0 { - err = ErrBadPattern - return + return "", false, ErrBadPattern } } fallthrough default: - if chunk[0] != s[0] { - return + if !failed { + if chunk[0] != s[0] { + failed = true + } + s = s[1:] } - s = s[1:] chunk = chunk[1:] } } + if failed { + return "", false, nil + } return s, true, nil } @@ -232,6 +241,10 @@ func getEsc(chunk string) (r rune, nchunk string, err error) { // The only possible returned error is ErrBadPattern, when pattern // is malformed. func Glob(pattern string) (matches []string, err error) { + // Check pattern is well-formed. + if _, err := Match(pattern, ""); err != nil { + return nil, err + } if !hasMeta(pattern) { if _, err = os.Lstat(pattern); err != nil { return nil, nil diff --git a/src/path/filepath/match_test.go b/src/path/filepath/match_test.go index b8657626bc..1c3b567fa3 100644 --- a/src/path/filepath/match_test.go +++ b/src/path/filepath/match_test.go @@ -75,8 +75,10 @@ var matchTests = []MatchTest{ {"[", "a", false, ErrBadPattern}, {"[^", "a", false, ErrBadPattern}, {"[^bc", "a", false, ErrBadPattern}, - {"a[", "a", false, nil}, + {"a[", "a", false, ErrBadPattern}, {"a[", "ab", false, ErrBadPattern}, + {"a[", "x", false, ErrBadPattern}, + {"a/b[", "x", false, ErrBadPattern}, {"*x", "xxx", true, nil}, } @@ -155,9 +157,11 @@ func TestGlob(t *testing.T) { } func TestGlobError(t *testing.T) { - _, err := Glob("[]") - if err == nil { - t.Error("expected error for bad pattern; got none") + bad := []string{`[]`, `nonexist/[]`} + for _, pattern := range bad { + if _, err := Glob(pattern); err != ErrBadPattern { + t.Errorf("Glob(%#q) returned err=%v, want ErrBadPattern", pattern, err) + } } } diff --git a/src/path/match.go b/src/path/match.go index 837eb8bb8b..918624c60e 100644 --- a/src/path/match.go +++ b/src/path/match.go @@ -75,6 +75,14 @@ Pattern: } } } + // Before returning false with no error, + // check that the remainder of the pattern is syntactically valid. + for len(pattern) > 0 { + _, chunk, pattern = scanChunk(pattern) + if _, _, err := matchChunk(chunk, ""); err != nil { + return false, err + } + } return false, nil } return len(name) == 0, nil @@ -114,20 +122,28 @@ Scan: // If so, it returns the remainder of s (after the match). // Chunk is all single-character operators: literals, char classes, and ?. func matchChunk(chunk, s string) (rest string, ok bool, err error) { + // failed records whether the match has failed. + // After the match fails, the loop continues on processing chunk, + // checking that the pattern is well-formed but no longer reading s. + failed := false for len(chunk) > 0 { - if len(s) == 0 { - return + if !failed && len(s) == 0 { + failed = true } switch chunk[0] { case '[': // character class - r, n := utf8.DecodeRuneInString(s) - s = s[n:] + var r rune + if !failed { + var n int + r, n = utf8.DecodeRuneInString(s) + s = s[n:] + } chunk = chunk[1:] // possibly negated - notNegated := true + negated := false if len(chunk) > 0 && chunk[0] == '^' { - notNegated = false + negated = true chunk = chunk[1:] } // parse all ranges @@ -140,12 +156,12 @@ func matchChunk(chunk, s string) (rest string, ok bool, err error) { } var lo, hi rune if lo, chunk, err = getEsc(chunk); err != nil { - return + return "", false, err } hi = lo if chunk[0] == '-' { if hi, chunk, err = getEsc(chunk[1:]); err != nil { - return + return "", false, err } } if lo <= r && r <= hi { @@ -153,34 +169,40 @@ func matchChunk(chunk, s string) (rest string, ok bool, err error) { } nrange++ } - if match != notNegated { - return + if match == negated { + failed = true } case '?': - if s[0] == '/' { - return + if !failed { + if s[0] == '/' { + failed = true + } + _, n := utf8.DecodeRuneInString(s) + s = s[n:] } - _, n := utf8.DecodeRuneInString(s) - s = s[n:] chunk = chunk[1:] case '\\': chunk = chunk[1:] if len(chunk) == 0 { - err = ErrBadPattern - return + return "", false, ErrBadPattern } fallthrough default: - if chunk[0] != s[0] { - return + if !failed { + if chunk[0] != s[0] { + failed = true + } + s = s[1:] } - s = s[1:] chunk = chunk[1:] } } + if failed { + return "", false, nil + } return s, true, nil } diff --git a/src/path/match_test.go b/src/path/match_test.go index 3e027e1f68..996bd691eb 100644 --- a/src/path/match_test.go +++ b/src/path/match_test.go @@ -67,8 +67,10 @@ var matchTests = []MatchTest{ {"[", "a", false, ErrBadPattern}, {"[^", "a", false, ErrBadPattern}, {"[^bc", "a", false, ErrBadPattern}, - {"a[", "a", false, nil}, + {"a[", "a", false, ErrBadPattern}, {"a[", "ab", false, ErrBadPattern}, + {"a[", "x", false, ErrBadPattern}, + {"a/b[", "x", false, ErrBadPattern}, {"*x", "xxx", true, nil}, } |