Skip to content

Commit 987a85e

Browse files
committed
pkg: forbid ** in path.Match and tool/file.Glob
Currently the double-star pattern term `**` behaves like a star `*`, as one star matches any sequence of non-separator characters and the second star matches nothing. This is confusing, as one may write the pattern `**/*.json` and it can match strings like `foo/bar.json`, making it seem like `**` works as a double star that would also match `foo.json` or `foo/bar/baz.json` when in fact it does not. If we choose to support `**` patterns in the future to match separators, this would mean changing the behavior for existing users in a subtle way which could lead to unintended behavior or latent bugs. It is clearer for existing users to treat `**` as an invalid pattern, which would allow us to add the feature in the future without concern about existing users already using such patterns without error. We also mirror Go's path.Match behavior to consume the entire pattern to validate that it is always syntactically valid, so that we may use it to check for `**` even when the matching has otherwise ended. See https://go.dev/issue/28614 for details; we largely copied these semantics from Go 1.15 already, but we omitted a bit of code that was necessary for trailing double-stars to be caught as an error. This change is driven by the embed proposal, where recursively embedding entire directory trees is a relatively common use case that we want to support somehow, and users might be misled by `**` silently working in a way that doesn't match the user's expectation. We want path.Match, tool/file.Glob, and embed patterns to behave consistently, so for that reason, start consistently treating `**` as an error. Note that this is a breaking change for any users whose pattern includes `**` as two consecutive wildcards, and this is on purpose for the reasons outlined above. Users who run into this problem may not have been aware that it behaved like a single wildcard, and in the majority of cases they should be able to use `*` instead. For #1919. Fixes #3315. Signed-off-by: Daniel Martí <[email protected]> Change-Id: Ifeb81f4818a863cd0e4c5c13bc0fc2ac54a0f22f Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1198636 Unity-Result: CUE porcuepine <[email protected]> TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Roger Peppe <[email protected]>
1 parent 25bb3d4 commit 987a85e

File tree

4 files changed

+43
-14
lines changed

4 files changed

+43
-14
lines changed

Diff for: pkg/path/match.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
// ErrBadPattern indicates a pattern was malformed.
2828
var ErrBadPattern = errors.New("syntax error in pattern")
2929

30+
var errStarStarDisallowed = errors.New("'**' is not supported in patterns as of yet")
31+
3032
// Match reports whether name matches the shell file name pattern.
3133
// The pattern syntax is:
3234
//
@@ -51,13 +53,19 @@ var ErrBadPattern = errors.New("syntax error in pattern")
5153
//
5254
// On Windows, escaping is disabled. Instead, '\\' is treated as
5355
// path separator.
56+
//
57+
// A pattern may not contain '**', as a wildcard matching separator characters
58+
// is not supported at this time.
5459
func Match(pattern, name string, o OS) (matched bool, err error) {
5560
os := getOS(o)
5661
Pattern:
5762
for len(pattern) > 0 {
5863
var star bool
5964
var chunk string
60-
star, chunk, pattern = scanChunk(pattern, os)
65+
star, chunk, pattern, err = scanChunk(pattern, os)
66+
if err != nil {
67+
return false, err
68+
}
6169
if star && chunk == "" {
6270
// Trailing * matches rest of string unless it has a /.
6371
return !strings.Contains(name, string(os.Separator)), nil
@@ -92,17 +100,29 @@ Pattern:
92100
}
93101
}
94102
}
103+
// Before returning false with no error,
104+
// check that the remainder of the pattern is syntactically valid.
105+
for len(pattern) > 0 {
106+
_, chunk, pattern, err = scanChunk(pattern, os)
107+
if err != nil {
108+
return false, err
109+
}
110+
}
95111
return false, nil
96112
}
97113
return len(name) == 0, nil
98114
}
99115

100116
// scanChunk gets the next segment of pattern, which is a non-star string
101117
// possibly preceded by a star.
102-
func scanChunk(pattern string, os os) (star bool, chunk, rest string) {
103-
for len(pattern) > 0 && pattern[0] == '*' {
118+
func scanChunk(pattern string, os os) (star bool, chunk, rest string, _ error) {
119+
if len(pattern) > 0 && pattern[0] == '*' {
104120
pattern = pattern[1:]
105121
star = true
122+
if len(pattern) > 0 && pattern[0] == '*' {
123+
// ** is disallowed to allow for future functionality.
124+
return false, "", "", errStarStarDisallowed
125+
}
106126
}
107127
inrange := false
108128
var i int
@@ -126,7 +146,7 @@ Scan:
126146
}
127147
}
128148
}
129-
return star, pattern[0:i], pattern[i:]
149+
return star, pattern[0:i], pattern[i:], nil
130150
}
131151

132152
// matchChunk checks whether chunk matches the beginning of s.

Diff for: pkg/path/match_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,9 @@ var matchTests = []MatchTest{
8686
{"a[", "x", false, ErrBadPattern},
8787
{"a/b[", "x", false, ErrBadPattern},
8888
{"*x", "xxx", true, nil},
89-
// TODO(mvdan): this should fail; right now "**" happens to behave like "*".
90-
{"**", "ab/c", false, nil},
91-
{"**/c", "ab/c", true, nil},
92-
{"a/b/**", "", false, nil},
89+
{"**", "ab/c", false, errStarStarDisallowed},
90+
{"**/c", "ab/c", false, errStarStarDisallowed},
91+
{"a/b/**", "", false, errStarStarDisallowed},
9392
{"\\**", "*ab", true, nil},
9493
{"[x**y]", "*", true, nil},
9594
}

Diff for: pkg/tool/file/file.go

+12
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ package file
1717
import (
1818
"os"
1919
"path/filepath"
20+
"runtime"
2021

2122
"cuelang.org/go/cue"
2223
"cuelang.org/go/cue/errors"
2324
"cuelang.org/go/internal/task"
25+
pkgpath "cuelang.org/go/pkg/path"
2426
)
2527

2628
func init() {
@@ -110,6 +112,16 @@ func (c *cmdGlob) Run(ctx *task.Context) (res interface{}, err error) {
110112
if ctx.Err != nil {
111113
return nil, ctx.Err
112114
}
115+
// Validate that the glob pattern is valid per [pkgpath.Match].
116+
// Note that we use the current OS to match the semantics of [filepath.Glob],
117+
// and since the APIs in this package are meant to support native paths.
118+
os := pkgpath.Unix
119+
if runtime.GOOS == "windows" {
120+
os = pkgpath.Windows
121+
}
122+
if _, err := pkgpath.Match(glob, "", os); err != nil {
123+
return nil, err
124+
}
113125
m, err := filepath.Glob(glob)
114126
for i, s := range m {
115127
m[i] = filepath.ToSlash(s)

Diff for: pkg/tool/file/file_test.go

+4-6
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,12 @@ func TestGlob(t *testing.T) {
125125
qt.Assert(t, qt.DeepEquals(got, any(map[string]any{"files": []string{"testdata/input.foo"}})))
126126

127127
// globstar or recursive globbing is not supported.
128-
// TODO(mvdan): this should fail; right now "**" happens to behave like "*".
129128
v = parse(t, "tool/file.Glob", `{
130129
glob: "testdata/**/glob.leaf"
131130
}`)
132131
got, err = (*cmdGlob).Run(nil, &task.Context{Obj: v})
133-
qt.Assert(t, qt.IsNil(err))
134-
qt.Assert(t, qt.DeepEquals(got, any(map[string]any{"files": []string{"testdata/glob1/glob.leaf"}})))
132+
qt.Assert(t, qt.IsNotNil(err))
133+
qt.Assert(t, qt.IsNil(got))
135134
}
136135

137136
func TestGlobEscapeStar(t *testing.T) {
@@ -151,9 +150,8 @@ func TestGlobEscapeStar(t *testing.T) {
151150
}`)
152151
got, err := (*cmdGlob).Run(nil, &task.Context{Obj: v})
153152
if runtime.GOOS == "windows" {
154-
// TODO(mvdan): this should fail; right now "**" happens to behave like "*".
155-
qt.Assert(t, qt.IsNil(err))
156-
qt.Assert(t, qt.DeepEquals(got, any(map[string]any{"files": []string(nil)})))
153+
qt.Assert(t, qt.IsNotNil(err))
154+
qt.Assert(t, qt.Equals(got, nil))
157155
} else {
158156
qt.Assert(t, qt.IsNil(err))
159157
qt.Assert(t, qt.DeepEquals(got, any(map[string]any{"files": []string{leafFile}})))

0 commit comments

Comments
 (0)