feat: quote Windows supervisor arguments
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidCommandLineArgument = errors.New("Windows command-line argument contains NUL or invalid UTF-8")
|
||||||
|
|
||||||
|
// BuildCommandLine constructs the lpCommandLine string for an explicit
|
||||||
|
// lpApplicationName. It implements the CommandLineToArgvW-compatible escaping
|
||||||
|
// convention: quotes are escaped by a preceding run of backslashes and trailing
|
||||||
|
// backslashes inside a quoted argument are doubled.
|
||||||
|
func BuildCommandLine(arguments []string) (string, error) {
|
||||||
|
if len(arguments) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
quoted := make([]string, len(arguments))
|
||||||
|
for index, argument := range arguments {
|
||||||
|
if strings.IndexByte(argument, 0) >= 0 || !utf8.ValidString(argument) {
|
||||||
|
return "", ErrInvalidCommandLineArgument
|
||||||
|
}
|
||||||
|
quoted[index] = quoteArgument(argument)
|
||||||
|
}
|
||||||
|
return strings.Join(quoted, " "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteArgument(argument string) string {
|
||||||
|
if argument != "" && !strings.ContainsAny(argument, " \t\n\v\"") {
|
||||||
|
return argument
|
||||||
|
}
|
||||||
|
var output strings.Builder
|
||||||
|
output.Grow(len(argument) + 2)
|
||||||
|
output.WriteByte('"')
|
||||||
|
backslashes := 0
|
||||||
|
for _, character := range argument {
|
||||||
|
switch character {
|
||||||
|
case '\\':
|
||||||
|
backslashes++
|
||||||
|
case '"':
|
||||||
|
output.WriteString(strings.Repeat("\\", backslashes*2+1))
|
||||||
|
output.WriteRune(character)
|
||||||
|
backslashes = 0
|
||||||
|
default:
|
||||||
|
output.WriteString(strings.Repeat("\\", backslashes))
|
||||||
|
output.WriteRune(character)
|
||||||
|
backslashes = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.WriteString(strings.Repeat("\\", backslashes*2))
|
||||||
|
output.WriteByte('"')
|
||||||
|
return output.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildCommandLine_HP_CMD_01(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
arguments []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"empty vector", nil, ""},
|
||||||
|
{"ordinary", []string{"-File", `C:\ProgramData\RVBox\work\run.ps1`}, `-File C:\ProgramData\RVBox\work\run.ps1`},
|
||||||
|
{"empty argument", []string{"-Command", ""}, `-Command ""`},
|
||||||
|
{"spaces", []string{"-File", `C:\path with spaces\run.ps1`}, `-File "C:\path with spaces\run.ps1"`},
|
||||||
|
{"embedded quote", []string{`say "hello"`}, `"say \"hello\""`},
|
||||||
|
{"trailing slash", []string{`C:\path with spaces\`}, `"C:\path with spaces\\"`},
|
||||||
|
{"unicode", []string{"-File", `C:\工作\脚本.ps1`}, `-File C:\工作\脚本.ps1`},
|
||||||
|
}
|
||||||
|
for _, test := range cases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got, err := BuildCommandLine(test.arguments)
|
||||||
|
if err != nil || got != test.want {
|
||||||
|
t.Fatalf("BuildCommandLine(%q) = (%q, %v), want %q", test.arguments, got, err, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCommandLineRejectsNUL_BH_CMD_01(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if _, err := BuildCommandLine([]string{"safe", "bad\x00argument"}); !errors.Is(err, ErrInvalidCommandLineArgument) {
|
||||||
|
t.Fatalf("NUL argument error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := BuildCommandLine([]string{string([]byte{0xff})}); !errors.Is(err, ErrInvalidCommandLineArgument) {
|
||||||
|
t.Fatalf("invalid UTF-8 argument error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,8 +61,15 @@ tests = [
|
|||||||
"internal/domain/lifecycle_test.go:TestLifecycleTransitionMatrix_HP_CMD_01",
|
"internal/domain/lifecycle_test.go:TestLifecycleTransitionMatrix_HP_CMD_01",
|
||||||
"internal/domain/lifecycle_test.go:TestTerminalPredicateExhaustive_HP_CMD_01",
|
"internal/domain/lifecycle_test.go:TestTerminalPredicateExhaustive_HP_CMD_01",
|
||||||
"internal/domain/revision_test.go:TestRevisionComparisonAndAdvance_HP_CMD_01",
|
"internal/domain/revision_test.go:TestRevisionComparisonAndAdvance_HP_CMD_01",
|
||||||
|
"internal/client/supervisor/windows/commandline_test.go:TestBuildCommandLine_HP_CMD_01",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[requirements]]
|
||||||
|
id = "BH-CMD-01"
|
||||||
|
layer = "unit"
|
||||||
|
status = "implemented"
|
||||||
|
tests = ["internal/client/supervisor/windows/commandline_test.go:TestBuildCommandLineRejectsNUL_BH_CMD_01"]
|
||||||
|
|
||||||
[[requirements]]
|
[[requirements]]
|
||||||
id = "HP-LAUNCH-01"
|
id = "HP-LAUNCH-01"
|
||||||
layer = "unit"
|
layer = "unit"
|
||||||
|
|||||||
Reference in New Issue
Block a user