Skip to content

Commit b2f7ab8

Browse files
authored
Fix GlobPrefix. (#220)
1 parent c9135b9 commit b2f7ab8

File tree

2 files changed

+103
-17
lines changed

2 files changed

+103
-17
lines changed

ext/regexp/regexp.go

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ package regexp
1616
import (
1717
"errors"
1818
"regexp"
19+
"regexp/syntax"
1920
"strings"
21+
"unicode/utf8"
2022

2123
"github.com/ncruces/go-sqlite3"
2224
)
@@ -50,16 +52,63 @@ func Register(db *sqlite3.Conn) error {
5052
// SELECT column WHERE column GLOB :glob_prefix AND column REGEXP :regexp
5153
//
5254
// [LIKE optimization]: https://sqlite.org/optoverview.html#the_like_optimization
53-
func GlobPrefix(re *regexp.Regexp) string {
54-
prefix, complete := re.LiteralPrefix()
55-
i := strings.IndexAny(prefix, "*?[")
56-
if i < 0 {
57-
if complete {
58-
return prefix
55+
func GlobPrefix(expr string) string {
56+
re, err := syntax.Parse(expr, syntax.Perl)
57+
if err != nil {
58+
return "" // no match possible
59+
}
60+
prog, err := syntax.Compile(re.Simplify())
61+
if err != nil {
62+
return "" // notest
63+
}
64+
65+
i := &prog.Inst[prog.Start]
66+
67+
var empty syntax.EmptyOp
68+
loop1:
69+
for {
70+
switch i.Op {
71+
case syntax.InstFail:
72+
return "" // notest
73+
case syntax.InstCapture, syntax.InstNop:
74+
// skip
75+
case syntax.InstEmptyWidth:
76+
empty |= syntax.EmptyOp(i.Arg)
77+
default:
78+
break loop1
5979
}
60-
i = len(prefix)
80+
i = &prog.Inst[i.Out]
81+
}
82+
if empty&syntax.EmptyBeginText == 0 {
83+
return "*" // not anchored
6184
}
62-
return prefix[:i] + "*"
85+
86+
var glob strings.Builder
87+
loop2:
88+
for {
89+
switch i.Op {
90+
case syntax.InstFail:
91+
return "" // notest
92+
case syntax.InstCapture, syntax.InstEmptyWidth, syntax.InstNop:
93+
// skip
94+
case syntax.InstRune, syntax.InstRune1:
95+
if len(i.Rune) != 1 || syntax.Flags(i.Arg)&syntax.FoldCase != 0 {
96+
break loop2
97+
}
98+
switch r := i.Rune[0]; r {
99+
case '*', '?', '[', utf8.RuneError:
100+
break loop2
101+
default:
102+
glob.WriteRune(r)
103+
}
104+
default:
105+
break loop2
106+
}
107+
i = &prog.Inst[i.Out]
108+
}
109+
110+
glob.WriteByte('*')
111+
return glob.String()
63112
}
64113

65114
func load(ctx sqlite3.Context, i int, expr string) (*regexp.Regexp, error) {

ext/regexp/regexp_test.go

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package regexp
33
import (
44
"database/sql"
55
"regexp"
6+
"strings"
67
"testing"
78

89
"github.com/ncruces/go-sqlite3/driver"
@@ -108,19 +109,55 @@ func TestGlobPrefix(t *testing.T) {
108109
re string
109110
want string
110111
}{
111-
{``, ""},
112-
{`a`, "a"},
113-
{`a*`, "*"},
114-
{`a+`, "a*"},
115-
{`ab*`, "a*"},
116-
{`ab+`, "ab*"},
117-
{`a\?b`, "a*"},
112+
{`[`, ""},
113+
{``, "*"},
114+
{`^`, "*"},
115+
{`a`, "*"},
116+
{`ab`, "*"},
117+
{`^a`, "a*"},
118+
{`^a*`, "*"},
119+
{`^a+`, "a*"},
120+
{`^ab*`, "a*"},
121+
{`^ab+`, "ab*"},
122+
{`^a\?b`, "a*"},
123+
{`^[a-z]`, "*"},
118124
}
119125
for _, tt := range tests {
120126
t.Run(tt.re, func(t *testing.T) {
121-
if got := GlobPrefix(regexp.MustCompile(tt.re)); got != tt.want {
122-
t.Errorf("GlobPrefix() = %v, want %v", got, tt.want)
127+
if got := GlobPrefix(tt.re); got != tt.want {
128+
t.Errorf("GlobPrefix(%v) = %v, want %v", tt.re, got, tt.want)
123129
}
124130
})
125131
}
126132
}
133+
134+
func FuzzGlobPrefix(f *testing.F) {
135+
f.Add(``, ``)
136+
f.Add(`[`, ``)
137+
f.Add(`^`, ``)
138+
f.Add(`a`, `a`)
139+
f.Add(`ab`, `b`)
140+
f.Add(`^a`, `a`)
141+
f.Add(`^a*`, `ab`)
142+
f.Add(`^a+`, `ab`)
143+
f.Add(`^ab*`, `ab`)
144+
f.Add(`^ab+`, `ab`)
145+
f.Add(`^a\?b`, `ab`)
146+
f.Add(`^[a-z]`, `ab`)
147+
148+
f.Fuzz(func(t *testing.T, lit, str string) {
149+
re, err := regexp.Compile(lit)
150+
if err != nil {
151+
t.SkipNow()
152+
}
153+
if re.MatchString(str) {
154+
prefix, ok := strings.CutSuffix(GlobPrefix(lit), "*")
155+
if !ok {
156+
t.Fatalf("missing * after %q for %q with %q", prefix, lit, str)
157+
}
158+
if !strings.HasPrefix(str, prefix) {
159+
t.Fatalf("missing prefix %q for %q with %q", prefix, lit, str)
160+
}
161+
}
162+
})
163+
}

0 commit comments

Comments
 (0)