fix: contain generated files and assets and harden SQL rendering
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// Package sqllex recognizes PostgreSQL lexical boundaries without parsing SQL.
|
||||
package sqllex
|
||||
|
||||
import "strings"
|
||||
|
||||
func start(c byte) bool { return c >= 128 || c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
|
||||
func ident(c byte) bool { return start(c) || c >= '0' && c <= '9' || c >= 128 }
|
||||
|
||||
// Walk visits named parameters and standalone line comments in executable SQL. Quoted
|
||||
// strings/identifiers, dollar bodies, casts and nested block comments are opaque.
|
||||
// Returning replacement text is literal: it is never scanned again.
|
||||
func Walk(sql string, parameter func(string) string, lineComment func(string) string) string {
|
||||
var out strings.Builder
|
||||
for i := 0; i < len(sql); {
|
||||
begin := i
|
||||
switch {
|
||||
case strings.HasPrefix(sql[i:], "--"):
|
||||
i += 2
|
||||
for i < len(sql) && sql[i] != '\n' && sql[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
text := sql[begin:i]
|
||||
if lineComment != nil && strings.TrimSpace(sql[strings.LastIndexAny(sql[:begin], "\r\n")+1:begin]) == "" {
|
||||
text = lineComment(text)
|
||||
}
|
||||
out.WriteString(text)
|
||||
continue
|
||||
case strings.HasPrefix(sql[i:], "/*"):
|
||||
i += 2
|
||||
depth := 1
|
||||
for i < len(sql) && depth > 0 {
|
||||
if strings.HasPrefix(sql[i:], "/*") {
|
||||
depth++
|
||||
i += 2
|
||||
} else if strings.HasPrefix(sql[i:], "*/") {
|
||||
depth--
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
case sql[i] == '\'' || sql[i] == '"':
|
||||
q := sql[i]
|
||||
escapes := q == '\'' && i > 0 && (sql[i-1] == 'E' || sql[i-1] == 'e') && (i == 1 || !ident(sql[i-2]))
|
||||
i++
|
||||
for i < len(sql) {
|
||||
if escapes && sql[i] == '\\' {
|
||||
i++
|
||||
if i < len(sql) {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sql[i] == q {
|
||||
i++
|
||||
if i < len(sql) && sql[i] == q {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
case sql[i] == '$' && (i == 0 || !ident(sql[i-1]) && sql[i-1] != '$'):
|
||||
j := i + 1
|
||||
if j < len(sql) && start(sql[j]) {
|
||||
for j < len(sql) && ident(sql[j]) {
|
||||
j++
|
||||
}
|
||||
}
|
||||
if j < len(sql) && sql[j] == '$' {
|
||||
tag := sql[i : j+1]
|
||||
end := strings.Index(sql[j+1:], tag)
|
||||
if end < 0 {
|
||||
i = len(sql)
|
||||
} else {
|
||||
i = j + 1 + end + len(tag)
|
||||
}
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
case strings.HasPrefix(sql[i:], "::"):
|
||||
i += 2
|
||||
case sql[i] == ':' && i+1 < len(sql) && start(sql[i+1]) && (i == 0 || !ident(sql[i-1]) && sql[i-1] != ':'):
|
||||
i += 2
|
||||
for i < len(sql) && ident(sql[i]) {
|
||||
i++
|
||||
}
|
||||
text := sql[begin:i]
|
||||
if parameter != nil {
|
||||
text = parameter(text)
|
||||
}
|
||||
out.WriteString(text)
|
||||
continue
|
||||
default:
|
||||
i++
|
||||
}
|
||||
out.WriteString(sql[begin:i])
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package sqllex
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWalkSQLContexts(t *testing.T) {
|
||||
sql := `SELECT :x,:x, :x::text, ':hidden', 'it'':hidden', E'it\':hidden', ":hidden", $$:hidden$$, $名$:hidden$名$;
|
||||
-- :hidden
|
||||
/* :hidden /* :hidden */ :hidden */ SELECT :y, value::integer, x := 1;`
|
||||
var names []string
|
||||
got := Walk(sql, func(name string) string { names = append(names, name); return "$1 ${2} :inserted" }, nil)
|
||||
if strings.Join(names, ",") != ":x,:x,:x,:y" {
|
||||
t.Fatalf("parameters: %v", names)
|
||||
}
|
||||
if strings.Count(got, "$1 ${2} :inserted") != 4 || !strings.Contains(got, "value::integer") {
|
||||
t.Fatalf("result: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnterminatedSQLContextsAreOpaque(t *testing.T) {
|
||||
for _, sql := range []string{"SELECT ':x", `SELECT ":x`, "DO $tag$:x", "/* :x", "-- :x"} {
|
||||
if got := Walk(sql, func(string) string { t.Fatal("unexpected parameter"); return "" }, nil); got != sql {
|
||||
t.Fatalf("changed %q", sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user