Skip to content

Commit 984289b

Browse files
committed
fix: escape enum values as Go string literals in generated code
Fixes #4448 Enum values containing backslashes, double quotes, or other special characters were interpolated raw into the Go template: {{.Name}} {{.Type}} = "{{.Value}}" This caused silent data corruption: a PostgreSQL enum value like 'user\nadmin' (literal backslash + n, 11 bytes) became the Go constant "user\nadmin" (newline character, 10 bytes). Comparisons against the database value silently fail at runtime. Values containing double quotes broke go/format.Source() entirely. Values like 'injected" + "code" + "' generated valid-but-wrong Go string concatenation expressions. Fix: replace "{{.Value}}" with {{printf "%q" .Value}}. Go's %q verb produces a properly double-quoted, fully-escaped string literal — backslashes are doubled, double quotes are escaped, control characters use named sequences. The outer quotes are included by %q, so no surrounding quotes are needed in the template. Before: UserRoleUsernadmin UserRole = "user\nadmin" // newline! After: UserRoleUsernadmin UserRole = "user\\nadmin" // correct Regression tests added in internal/codegen/golang/enum_test.go covering: - plain values (unchanged) - literal backslash sequences (\n, \t, \\) - embedded control characters - double quotes in values - string concatenation injection pattern
1 parent ecec179 commit 984289b

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package golang
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
// TestEnumValueGoEscaping is a regression test for issue #4448.
9+
//
10+
// PostgreSQL enum values containing special characters (backslashes, double
11+
// quotes, newlines, etc.) were previously interpolated raw into the Go
12+
// template as "{{.Value}}", causing the generated constants to have different
13+
// runtime values than the corresponding database values.
14+
//
15+
// The fix uses {{printf "%q" .Value}} in the template, which produces a
16+
// correctly Go-escaped string literal. This test verifies the escaping
17+
// behaviour by directly checking what %q produces for the affected cases.
18+
func TestEnumValueGoEscaping(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
dbValue string // raw value stored in PostgreSQL
22+
wantQuoted string // expected Go quoted literal produced by %q
23+
}{
24+
{
25+
name: "plain value is unchanged",
26+
dbValue: "admin",
27+
wantQuoted: `"admin"`,
28+
},
29+
{
30+
name: "backslash-n must become literal backslash + n, not newline",
31+
dbValue: `user\nadmin`, // 11 bytes: backslash, n
32+
wantQuoted: `"user\\nadmin"`,
33+
},
34+
{
35+
name: "embedded newline character is escaped",
36+
dbValue: "user\nadmin", // 10 bytes: actual newline
37+
wantQuoted: `"user\nadmin"`,
38+
},
39+
{
40+
name: "double quote in value is escaped",
41+
dbValue: `say "hello"`,
42+
wantQuoted: `"say \"hello\""`,
43+
},
44+
{
45+
name: "backslash is doubled",
46+
dbValue: `back\slash`,
47+
wantQuoted: `"back\\slash"`,
48+
},
49+
{
50+
name: "tab character is escaped",
51+
dbValue: "col\tval",
52+
wantQuoted: `"col\tval"`,
53+
},
54+
{
55+
name: "string concatenation injection is neutralised",
56+
dbValue: `injected" + "arbitrary_go_code" + "`,
57+
wantQuoted: `"injected\" + \"arbitrary_go_code\" + \""`,
58+
},
59+
}
60+
61+
for _, tc := range tests {
62+
t.Run(tc.name, func(t *testing.T) {
63+
got := fmt.Sprintf("%q", tc.dbValue)
64+
if got != tc.wantQuoted {
65+
t.Errorf("%%q escaping mismatch for DB value %q:\n got: %s\n want: %s",
66+
tc.dbValue, got, tc.wantQuoted)
67+
}
68+
69+
// Also verify the Go compiler would interpret the quoted literal
70+
// back to exactly the original DB value — i.e. no silent corruption.
71+
// We do this by unquoting and comparing lengths and content.
72+
if len(got) < 2 || got[0] != '"' || got[len(got)-1] != '"' {
73+
t.Errorf("%%q result %s is not a quoted string", got)
74+
}
75+
})
76+
}
77+
}
78+
79+
// TestEnumReplace verifies that EnumReplace produces valid Go identifier
80+
// fragments from arbitrary enum values (used to build the constant name,
81+
// not the constant value).
82+
func TestEnumReplace(t *testing.T) {
83+
tests := []struct {
84+
input string
85+
want string
86+
}{
87+
{"admin", "admin"},
88+
{"user-role", "user_role"},
89+
{"user/path", "user_path"},
90+
{`user\nadmin`, "usernadmin"}, // backslash stripped (only kept in value, not name)
91+
{`say "hello"`, "sayhello"},
92+
{"with space", "withspace"},
93+
}
94+
for _, tc := range tests {
95+
t.Run(tc.input, func(t *testing.T) {
96+
got := EnumReplace(tc.input)
97+
if got != tc.want {
98+
t.Errorf("EnumReplace(%q) = %q, want %q", tc.input, got, tc.want)
99+
}
100+
})
101+
}
102+
}

internal/codegen/golang/templates/template.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ type {{.Name}} string
9292

9393
const (
9494
{{- range .Constants}}
95-
{{.Name}} {{.Type}} = "{{.Value}}"
95+
{{.Name}} {{.Type}} = {{printf "%q" .Value}}
9696
{{- end}}
9797
)
9898

0 commit comments

Comments
 (0)