Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,22 @@ func FuzzMatchRegexp(f *testing.F) {
t.Fatalf("bad regexp translation of %#q: %v", pattern, err)
}
var (
got = p.Match(s)
want = re.MatchString(s)
got = p.Match(s)
gotBytes = p.MatchBytes([]byte(s))
want = re.MatchString(s)
)
if got != want {
t.Errorf(
"sep=%q pattern=%#q (regexp %#q) string=%#q: Match()=%t, regexp=%t",
string(sep), pattern, re, s, got, want,
)
}
if gotBytes != want {
t.Errorf(
"sep=%q pattern=%#q (regexp %#q) string=%#q: MatchBytes()=%t, regexp=%t",
string(sep), pattern, re, s, gotBytes, want,
)
}
})
}

Expand Down
14 changes: 14 additions & 0 deletions glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ package glob

import (
"fmt"
"unsafe"

"github.com/gobwas/glob/internal/debug"
"github.com/gobwas/glob/syntax"
)

// Glob represents a compiled glob pattern.
type Glob interface {
Match(string) bool
MatchBytes([]byte) bool
}

var _ Glob = (*Pattern)(nil)

// SyntaxError is returned by [Compile] when the given pattern can not be
// parsed. Offset points at the place in the pattern the error was detected
// at, so the tooling can do things like:
Expand Down Expand Up @@ -89,6 +98,11 @@ func (p *Pattern) Separators() []rune {
return p.sep
}

// MatchBytes reports whether b matches the pattern.
func (p *Pattern) MatchBytes(b []byte) bool {
return p.Match(unsafe.String(unsafe.SliceData(b), len(b)))
}

func init() {
// The matcher tree is unexported; hand its rendering to the in-module
// tooling (cmd/globtest -v) without widening the public API.
Expand Down
159 changes: 159 additions & 0 deletions glob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,23 @@ func TestPatternMatch(t *testing.T) {
t.Errorf("too slow: %.4fms", latency.Seconds()*1000)
}
})
t.Run("glob-bytes"+suffix, func(t *testing.T) {
var (
pat = MustCompile(test.pat, test.sep...)
start = time.Now()
result = pat.MatchBytes([]byte(test.str))
latency = time.Since(start)
)
if result != test.exp {
t.Errorf(
"pattern %q matching bytes %q should be %v but got %v",
test.pat, test.str, test.exp, result,
)
}
if latency > 5*time.Millisecond {
t.Errorf("too slow: %.4fms", latency.Seconds()*1000)
}
})
if len(test.sep) == 0 && !strings.ContainsAny(test.pat, "{}") {
// NOTE: filepath.Match doesn't support `{}`.
t.Run("filepath"+suffix, func(t *testing.T) {
Expand Down Expand Up @@ -1128,3 +1145,145 @@ func TestPatternSeparatorsAliasing(t *testing.T) {
t.Errorf("Match() picked up the modification of the given slice")
}
}

func TestPatternMatchBytes(t *testing.T) {
t.Run("nil slice", func(t *testing.T) {
p := MustCompile("")
if !p.MatchBytes(nil) {
t.Errorf("empty pattern MatchBytes(nil) = false; want true")
}
p2 := MustCompile("a")
if p2.MatchBytes(nil) {
t.Errorf("non-empty pattern MatchBytes(nil) = true; want false")
}
})

t.Run("empty slice", func(t *testing.T) {
p := MustCompile("")
if !p.MatchBytes([]byte{}) {
t.Errorf("empty pattern MatchBytes([]byte{}) = false; want true")
}
p2 := MustCompile("a")
if p2.MatchBytes([]byte{}) {
t.Errorf("non-empty pattern MatchBytes([]byte{}) = true; want false")
}
})

t.Run("subslice with offset and capacity", func(t *testing.T) {
p := MustCompile("world")
buf := []byte("hello world 12345")
sub := buf[6:11]
if !p.MatchBytes(sub) {
t.Errorf("MatchBytes(%q) = false; want true", sub)
}
if p.MatchBytes(buf[0:5]) {
t.Errorf("MatchBytes(%q) = true; want false", buf[0:5])
}
})

t.Run("binary and null bytes", func(t *testing.T) {
p := MustCompile("a\x00b")
if !p.MatchBytes([]byte("a\x00b")) {
t.Errorf("MatchBytes(a\\x00b) = false; want true")
}
if p.MatchBytes([]byte("a\x00c")) {
t.Errorf("MatchBytes(a\\x00c) = true; want false")
}
})
}

func TestGlobInterface(t *testing.T) {
var g Glob = MustCompile("*.github.com")
if !g.Match("api.github.com") {
t.Errorf("Glob.Match() = false; want true")
}
if !g.MatchBytes([]byte("api.github.com")) {
t.Errorf("Glob.MatchBytes() = false; want true")
}
if g.Match("api.google.com") {
t.Errorf("Glob.Match() = true; want false")
}
if g.MatchBytes([]byte("api.google.com")) {
t.Errorf("Glob.MatchBytes() = true; want false")
}
}

func TestPatternMatchBytesZeroAllocs(t *testing.T) {
cases := []struct {
pat string
b []byte
}{
{"google.com", []byte("google.com")},
{"abc*", []byte("abcdef")},
{"*def", []byte("abcdef")},
{"ab*ef", []byte("abcdef")},
{"https://*.google.*", []byte("https://account.google.com")},
{"{cat,bat,[fr]at}", []byte("fat")},
}
for _, c := range cases {
p := MustCompile(c.pat)
_ = p.MatchBytes(c.b)
allocs := testing.AllocsPerRun(100, func() {
if !p.MatchBytes(c.b) {
t.Fatalf("pattern %q did not match", c.pat)
}
})
if allocs > 0 {
t.Errorf("pattern %q: MatchBytes allocated %f times; want 0", c.pat, allocs)
}
}
}

func BenchmarkCompareMatchAndMatchBytes(b *testing.B) {
for _, test := range []struct {
name string
glob string
str string
}{
{
name: "plain",
glob: "google.com",
str: "google.com",
},
{
name: "prefix",
glob: "abc*",
str: "abcdef",
},
{
name: "suffix",
glob: "*def",
str: "abcdef",
},
{
name: "wildcard",
glob: "https://*.google.*",
str: "https://account.google.com",
},
{
name: "alternatives",
glob: "{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}",
str: "http://yahoo.com",
},
} {
pat := MustCompile(test.glob)
str := test.str
bts := []byte(str)

b.Run(test.name+"-string", func(b *testing.B) {
for b.Loop() {
pat.Match(str)
}
})
b.Run(test.name+"-bytes", func(b *testing.B) {
for b.Loop() {
pat.MatchBytes(bts)
}
})
b.Run(test.name+"-bytes-to-string-alloc", func(b *testing.B) {
for b.Loop() {
pat.Match(string(bts))
}
})
}
}
19 changes: 18 additions & 1 deletion issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ func TestOpenIssues(t *testing.T) {
sep []rune
exp bool
}{
{32, "*.github.com", "api.github.com", nil, true},
{32, "*.github.com", "github.com", nil, false},
{33, "test*201901*.12*", "test.20190101.120000", nil, true},
{33, "test.*201901*.14*", "test.20190101.130000", nil, false},
{33, "test.*201901*.14*", "test.20190101.140000", nil, true},
Expand Down Expand Up @@ -48,10 +50,14 @@ func TestOpenIssues(t *testing.T) {
{66, `{**/daxing,daxing}/**/*dev*.yaml`, "playground/daxing/generated/dev.yaml", nil, true},
{66, `{**/daxing}/**/*dev*.yaml`, "playground/daxing/generated/dev.yaml", nil, true},
} {
got := MustCompile(c.pat, c.sep...).Match(c.str)
p := MustCompile(c.pat, c.sep...)
got := p.Match(c.str)
if got != c.exp {
t.Errorf("issue #%d: Match(%q, %q) = %t; want %t", c.issue, c.pat, c.str, got, c.exp)
}
if gotBytes := p.MatchBytes([]byte(c.str)); gotBytes != c.exp {
t.Errorf("issue #%d: MatchBytes(%q, %q) = %t; want %t", c.issue, c.pat, c.str, gotBytes, c.exp)
}
}
// Crashers: old versions panicked, new must return a syntax error.
if _, err := Compile("/{a{*.json{", '/'); err == nil {
Expand All @@ -61,3 +67,14 @@ func TestOpenIssues(t *testing.T) {
t.Errorf("issue #56: Compile succeeded; want error")
}
}

func TestIssue32(t *testing.T) {
var g Glob
g = MustCompile("*.github.com")
if !g.MatchBytes([]byte("api.github.com")) {
t.Errorf("issue #32: MatchBytes() = false; want true")
}
if g.MatchBytes([]byte("api.google.com")) {
t.Errorf("issue #32: MatchBytes() = true; want false")
}
}