1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-28 10:03:42 +02:00

[#4704] fixed '~' autowildcard wrapping when the string has escaped % character

This commit is contained in:
Gani Georgiev 2024-04-05 20:12:45 +03:00
parent ac76166cb2
commit 63bcffb223
3 changed files with 131 additions and 5 deletions

View File

@ -1,3 +1,8 @@
## (WIP) v0.22.8
- Fixed '~' auto wildcard wrapping when the param has escaped `%` character ([#4704](https://github.com/pocketbase/pocketbase/discussions/4704)).
## v0.22.7 ## v0.22.7
- Replaced the default `s3blob` driver with a trimmed vendored version to reduce the binary size with ~10MB. - Replaced the default `s3blob` driver with a trimmed vendored version to reduce the binary size with ~10MB.

View File

@ -432,16 +432,15 @@ func mergeParams(params ...dbx.Params) dbx.Params {
} }
// wrapLikeParams wraps each provided param value string with `%` // wrapLikeParams wraps each provided param value string with `%`
// if the string doesn't contains the `%` char (including its escape sequence). // if the param doesn't contain an explicit wildcard (`%`) character already.
func wrapLikeParams(params dbx.Params) dbx.Params { func wrapLikeParams(params dbx.Params) dbx.Params {
result := dbx.Params{} result := dbx.Params{}
for k, v := range params { for k, v := range params {
vStr := cast.ToString(v) vStr := cast.ToString(v)
if !strings.Contains(vStr, "%") { if !containsUnescapedChar(vStr, '%') {
for i := 0; i < len(dbx.DefaultLikeEscape); i += 2 { // note: this is done to minimize the breaking changes and to preserve the original autoescape behavior
vStr = strings.ReplaceAll(vStr, dbx.DefaultLikeEscape[i], dbx.DefaultLikeEscape[i+1]) vStr = escapeUnescapedChars(vStr, '\\', '%', '_')
}
vStr = "%" + vStr + "%" vStr = "%" + vStr + "%"
} }
result[k] = vStr result[k] = vStr
@ -450,6 +449,63 @@ func wrapLikeParams(params dbx.Params) dbx.Params {
return result return result
} }
func escapeUnescapedChars(str string, escapeChars ...rune) string {
rs := []rune(str)
total := len(rs)
result := make([]rune, 0, total)
var match bool
for i := total - 1; i >= 0; i-- {
if match {
// check if already escaped
if rs[i] != '\\' {
result = append(result, '\\')
}
match = false
} else {
for _, ec := range escapeChars {
if rs[i] == ec {
match = true
break
}
}
}
result = append(result, rs[i])
// in case the matching char is at the beginning
if i == 0 && match {
result = append(result, '\\')
}
}
// reverse
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return string(result)
}
func containsUnescapedChar(str string, ch rune) bool {
var prev rune
for _, c := range str {
if c == ch && prev != '\\' {
return true
}
if c == '\\' && prev == '\\' {
prev = rune(0) // reset escape sequence
} else {
prev = c
}
}
return false
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
var _ dbx.Expression = (*opExpr)(nil) var _ dbx.Expression = (*opExpr)(nil)

View File

@ -238,3 +238,68 @@ func TestFilterDataBuildExprWithParams(t *testing.T) {
t.Fatalf("Expected query \n%s, \ngot \n%s", expectedQuery, calledQueries[0]) t.Fatalf("Expected query \n%s, \ngot \n%s", expectedQuery, calledQueries[0])
} }
} }
func TestLikeParamsWrapping(t *testing.T) {
// create a dummy db
sqlDB, err := sql.Open("sqlite", "file::memory:?cache=shared")
if err != nil {
t.Fatal(err)
}
db := dbx.NewFromDB(sqlDB, "sqlite")
calledQueries := []string{}
db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
calledQueries = append(calledQueries, sql)
}
db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
calledQueries = append(calledQueries, sql)
}
resolver := search.NewSimpleFieldResolver(`^test\w+$`)
filter := search.FilterData(`
test1 ~ {:p1} ||
test2 ~ {:p2} ||
test3 ~ {:p3} ||
test4 ~ {:p4} ||
test5 ~ {:p5} ||
test6 ~ {:p6} ||
test7 ~ {:p7} ||
test8 ~ {:p8} ||
test9 ~ {:p9} ||
test10 ~ {:p10} ||
test11 ~ {:p11} ||
test12 ~ {:p12}
`)
replacements := []dbx.Params{
{"p1": `abc`},
{"p2": `ab%c`},
{"p3": `ab\%c`},
{"p4": `%ab\%c`},
{"p5": `ab\\%c`},
{"p6": `ab\\\%c`},
{"p7": `ab_c`},
{"p8": `ab\_c`},
{"p9": `%ab_c`},
{"p10": `ab\c`},
{"p11": `_ab\c_`},
{"p12": `ab\c%`},
}
expr, err := filter.BuildExpr(resolver, replacements...)
if err != nil {
t.Fatal(err)
}
db.Select().Where(expr).Build().Execute()
if len(calledQueries) != 1 {
t.Fatalf("Expected 1 query, got %d", len(calledQueries))
}
expectedQuery := `SELECT * WHERE ([[test1]] LIKE '%abc%' ESCAPE '\' OR [[test2]] LIKE 'ab%c' ESCAPE '\' OR [[test3]] LIKE 'ab\\%c' ESCAPE '\' OR [[test4]] LIKE '%ab\\%c' ESCAPE '\' OR [[test5]] LIKE 'ab\\\\%c' ESCAPE '\' OR [[test6]] LIKE 'ab\\\\\\%c' ESCAPE '\' OR [[test7]] LIKE '%ab\_c%' ESCAPE '\' OR [[test8]] LIKE '%ab\\\_c%' ESCAPE '\' OR [[test9]] LIKE '%ab_c' ESCAPE '\' OR [[test10]] LIKE '%ab\\c%' ESCAPE '\' OR [[test11]] LIKE '%\_ab\\c\_%' ESCAPE '\' OR [[test12]] LIKE 'ab\\c%' ESCAPE '\')`
if expectedQuery != calledQueries[0] {
t.Fatalf("Expected query \n%s, \ngot \n%s", expectedQuery, calledQueries[0])
}
}