diff --git a/windows/exec_windows.go b/windows/exec_windows.go index 9eb1fb63..a020caee 100644 --- a/windows/exec_windows.go +++ b/windows/exec_windows.go @@ -78,6 +78,40 @@ func EscapeArg(s string) string { return string(qs[:j]) } +// ComposeCommandLine escapes and joins the given arguments suitable for use as a Windows command line, +// in CreateProcess's CommandLine argument, CreateService/ChangeServiceConfig's BinaryPathName argument, +// or any program that uses CommandLineToArgv. +func ComposeCommandLine(args []string) string { + var commandLine string + for i := range args { + if i > 0 { + commandLine += " " + } + commandLine += EscapeArg(args[i]) + } + return commandLine +} + +// DecomposeCommandLine breaks apart its argument command line into unescaped parts using CommandLineToArgv, +// as gathered from GetCommandLine, QUERY_SERVICE_CONFIG's BinaryPathName argument, or elsewhere that +// command lines are passed around. +func DecomposeCommandLine(commandLine string) ([]string, error) { + if len(commandLine) == 0 { + return []string{}, nil + } + var argc int32 + argv, err := CommandLineToArgv(StringToUTF16Ptr(commandLine), &argc) + if err != nil { + return nil, err + } + defer LocalFree(Handle(unsafe.Pointer(argv))) + var args []string + for _, v := range (*argv)[:argc] { + args = append(args, UTF16ToString((*v)[:])) + } + return args, nil +} + func CloseOnExec(fd Handle) { SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0) } diff --git a/windows/syscall_windows_test.go b/windows/syscall_windows_test.go index 0f17fb18..d6571b71 100644 --- a/windows/syscall_windows_test.go +++ b/windows/syscall_windows_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io/ioutil" + "math/rand" "os" "path/filepath" "runtime" @@ -599,3 +600,61 @@ func TestResourceExtraction(t *testing.T) { t.Errorf("did not find in manifest") } } + +func TestCommandLineRecomposition(t *testing.T) { + const ( + maxCharsPerArg = 35 + maxArgsPerTrial = 80 + doubleQuoteProb = 4 + singleQuoteProb = 1 + backSlashProb = 3 + spaceProb = 1 + trials = 1000 + ) + randString := func(l int) []rune { + s := make([]rune, l) + for i := range s { + s[i] = rand.Int31() + } + return s + } + mungeString := func(s []rune, char rune, timesInTen int) { + if timesInTen < rand.Intn(10)+1 || len(s) == 0 { + return + } + s[rand.Intn(len(s))] = char + } + argStorage := make([]string, maxArgsPerTrial+1) + for i := 0; i < trials; i++ { + args := argStorage[:rand.Intn(maxArgsPerTrial)+2] + args[0] = "valid-filename-for-arg0" + for j := 1; j < len(args); j++ { + arg := randString(rand.Intn(maxCharsPerArg + 1)) + mungeString(arg, '"', doubleQuoteProb) + mungeString(arg, '\'', singleQuoteProb) + mungeString(arg, '\\', backSlashProb) + mungeString(arg, ' ', spaceProb) + args[j] = string(arg) + } + commandLine := windows.ComposeCommandLine(args) + decomposedArgs, err := windows.DecomposeCommandLine(commandLine) + if err != nil { + t.Errorf("Unable to decompose %#q made from %v: %v", commandLine, args, err) + continue + } + if len(decomposedArgs) != len(args) { + t.Errorf("Incorrect decomposition length from %v to %#q to %v", args, commandLine, decomposedArgs) + continue + } + badMatches := make([]int, 0, len(args)) + for i := range args { + if args[i] != decomposedArgs[i] { + badMatches = append(badMatches, i) + } + } + if len(badMatches) != 0 { + t.Errorf("Incorrect decomposition at indices %v from %v to %#q to %v", badMatches, args, commandLine, decomposedArgs) + continue + } + } +}