-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsvcopts_escape_test.go
More file actions
154 lines (142 loc) · 5.93 KB
/
Copy pathsvcopts_escape_test.go
File metadata and controls
154 lines (142 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//go:build !windows
package main
import (
"errors"
"strings"
"testing"
"text/template"
"github.com/kardianos/service"
)
// fakeSystemd stands in for kardianos's systemd backend so applyServiceOpts
// takes its systemd branch on any host. Only String matters here; New is never
// reached because the test renders the unit itself.
type fakeSystemd struct{}
func (fakeSystemd) String() string { return "linux-systemd" }
func (fakeSystemd) Detect() bool { return true }
func (fakeSystemd) Interactive() bool { return true }
func (fakeSystemd) New(service.Interface, *service.Config) (service.Service, error) {
return nil, errors.New("fakeSystemd cannot build a service")
}
// kardianosFuncs mirrors the func map kardianos feeds the systemd template
// (service_linux.go): cmd double-quotes an argument and escapes only the
// double quote; cmdEscape rewrites spaces in the executable path.
var kardianosFuncs = template.FuncMap{
"cmd": func(s string) string { return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` },
"cmdEscape": func(s string) string { return strings.ReplaceAll(s, " ", `\x20`) },
}
// renderUnit renders the self-install unit for the flags an operator passed to
// `pingularity install`, exactly as controlCmd + svcConfig hand them to
// kardianos, and returns its ExecStart line.
func renderUnit(t *testing.T, flags []string) string {
t.Helper()
sc := svcConfig(append([]string{"run"}, flags...))
tmpl, err := template.New("").Funcs(kardianosFuncs).Parse(systemdScript)
if err != nil {
t.Fatalf("systemdScript failed to parse: %v", err)
}
data := struct {
Description string
Path string
Name string
Dependencies []string
Arguments []string
WorkingDirectory string
ReloadSignal string
Restart string
}{
Description: sc.Description, Path: "/usr/local/bin/pingularity", Name: sc.Name,
Dependencies: sc.Dependencies, Arguments: sc.Arguments,
ReloadSignal: "HUP", Restart: "always",
}
var b strings.Builder
if err := tmpl.Execute(&b, data); err != nil {
t.Fatalf("systemdScript failed to render: %v", err)
}
for _, line := range strings.Split(b.String(), "\n") {
if strings.HasPrefix(line, "ExecStart=") {
return line
}
}
t.Fatalf("no ExecStart line in rendered unit:\n%s", b.String())
return ""
}
// systemd reads ExecStart with its own rules, double quotes or not: %x is a
// specifier (%t the runtime directory, %h the home), $VAR and ${VAR} are
// environment substitutions, and a backslash starts a C escape. kardianos's
// cmd escapes only the double quote, so a -metrics-token with a % in it reached
// the daemon as a different token and a trailing backslash left a unit systemd
// refuses to load. Each install argument must reach the unit escaped per
// systemd.unit(5)/systemd.service(5): %% for %, $$ for $, a doubled backslash -
// while the template's own $PINGULARITY_OPTS stays expandable.
func TestSystemdUnitKeepsFlagValuesVerbatim(t *testing.T) {
orig := service.AvailableSystems()
service.ChooseSystem(fakeSystemd{})
t.Cleanup(func() { service.ChooseSystem(orig...) })
if service.Platform() != "linux-systemd" {
t.Fatalf("platform stand-in not in effect: %q", service.Platform())
}
line := renderUnit(t, []string{"-metrics-token", "tok%tX", "-db", `/var/lib/ping${HOME}y`, "-allow-host", `a\`, "-listen", ":9000"})
for _, want := range []string{
`"tok%%tX"`, // %t is systemd's runtime directory; %% is a literal percent
`"/var/lib/ping$${HOME}y"`, // ${HOME} substitutes even mid-word; $$ is a literal dollar
`"a\\"`, // a lone trailing backslash escapes the closing quote
`":9000"`, // an ordinary value is untouched
" $PINGULARITY_OPTS", // the template's own variable must still expand
} {
if !strings.Contains(line, want) {
t.Errorf("ExecStart lacks %s:\n%s", want, line)
}
}
for _, bad := range []string{`"tok%tX"`, `ping${HOME}y"`, `"a\"`} {
if strings.Contains(line, bad) {
t.Errorf("ExecStart still carries the raw value %s, which systemd rewrites:\n%s", bad, line)
}
}
}
// launchd's plist and the Windows SCM expand nothing, so on any other platform
// the arguments must reach the service definition exactly as typed.
func TestNonSystemdPlatformsKeepArgumentsAsTyped(t *testing.T) {
if service.Platform() == "linux-systemd" {
t.Skip("this host is systemd; the escaping is right here")
}
sc := svcConfig([]string{"run", "-metrics-token", "tok%tX", "-db", `C:\data\p.db`})
if got, want := strings.Join(sc.Arguments, " "), `run -metrics-token tok%tX -db C:\data\p.db`; got != want {
t.Errorf("arguments changed on %s: got %q, want %q", service.Platform(), got, want)
}
}
// systemdArgs applies systemd's three escapes and nothing else: %% for a
// percent, $$ for a dollar, a doubled backslash - in that order, so the
// backslash doubling never touches what the other two produce - and leaves the
// double quote to kardianos's cmd.
func TestSystemdArgs(t *testing.T) {
cases := []struct{ in, want string }{
{"run", "run"},
{":9000", ":9000"},
{"tok%tX", "tok%%tX"},
{"100%", "100%%"},
{"$HOME", "$$HOME"},
{"a${X}b", "a$${X}b"},
{"$$", "$$$$"},
{`a\`, `a\\`},
{`C:\new\tab`, `C:\\new\\tab`}, // \n and \t would otherwise become a newline and a tab
{`\%`, `\\%%`}, // backslash first: the %% it precedes is not re-escaped
{`say "hi"`, `say "hi"`}, // the quote is cmd's job
{"", ""},
}
for _, c := range cases {
if got := systemdArgs([]string{c.in})[0]; got != c.want {
t.Errorf("systemdArgs(%q) = %q, want %q", c.in, got, c.want)
}
}
if got := systemdArgs(nil); got != nil {
t.Errorf("systemdArgs(nil) = %v, want nil (start/stop/status carry no arguments)", got)
}
in := []string{"run", "-db", "%h"}
out := systemdArgs(in)
if strings.Join(in, " ") != "run -db %h" {
t.Errorf("systemdArgs rewrote its input in place: %q", in)
}
if strings.Join(out, " ") != "run -db %%h" {
t.Errorf("systemdArgs(%q) = %q", in, out)
}
}