| // Copyright 2009 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Fork, exec, wait, etc. |
| |
| package windows |
| |
| import ( |
| errorspkg "errors" |
| "unsafe" |
| ) |
| |
| // EscapeArg rewrites command line argument s as prescribed |
| // in http://msdn.microsoft.com/en-us/library/ms880421. |
| // This function returns "" (2 double quotes) if s is empty. |
| // Alternatively, these transformations are done: |
| // - every back slash (\) is doubled, but only if immediately |
| // followed by double quote ("); |
| // - every double quote (") is escaped by back slash (\); |
| // - finally, s is wrapped with double quotes (arg -> "arg"), |
| // but only if there is space or tab inside s. |
| func EscapeArg(s string) string { |
| if len(s) == 0 { |
| return `""` |
| } |
| n := len(s) |
| hasSpace := false |
| for i := 0; i < len(s); i++ { |
| switch s[i] { |
| case '"', '\\': |
| n++ |
| case ' ', '\t': |
| hasSpace = true |
| } |
| } |
| if hasSpace { |
| n += 2 // Reserve space for quotes. |
| } |
| if n == len(s) { |
| return s |
| } |
| |
| qs := make([]byte, n) |
| j := 0 |
| if hasSpace { |
| qs[j] = '"' |
| j++ |
| } |
| slashes := 0 |
| for i := 0; i < len(s); i++ { |
| switch s[i] { |
| default: |
| slashes = 0 |
| qs[j] = s[i] |
| case '\\': |
| slashes++ |
| qs[j] = s[i] |
| case '"': |
| for ; slashes > 0; slashes-- { |
| qs[j] = '\\' |
| j++ |
| } |
| qs[j] = '\\' |
| j++ |
| qs[j] = s[i] |
| } |
| j++ |
| } |
| if hasSpace { |
| for ; slashes > 0; slashes-- { |
| qs[j] = '\\' |
| j++ |
| } |
| qs[j] = '"' |
| j++ |
| } |
| 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 { |
| if len(args) == 0 { |
| return "" |
| } |
| |
| // Per https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw: |
| // “This function accepts command lines that contain a program name; the |
| // program name can be enclosed in quotation marks or not.” |
| // |
| // Unfortunately, it provides no means of escaping interior quotation marks |
| // within that program name, and we have no way to report them here. |
| prog := args[0] |
| mustQuote := len(prog) == 0 |
| for i := 0; i < len(prog); i++ { |
| c := prog[i] |
| if c <= ' ' || (c == '"' && i == 0) { |
| // Force quotes for not only the ASCII space and tab as described in the |
| // MSDN article, but also ASCII control characters. |
| // The documentation for CommandLineToArgvW doesn't say what happens when |
| // the first argument is not a valid program name, but it empirically |
| // seems to drop unquoted control characters. |
| mustQuote = true |
| break |
| } |
| } |
| var commandLine []byte |
| if mustQuote { |
| commandLine = make([]byte, 0, len(prog)+2) |
| commandLine = append(commandLine, '"') |
| for i := 0; i < len(prog); i++ { |
| c := prog[i] |
| if c == '"' { |
| // This quote would interfere with our surrounding quotes. |
| // We have no way to report an error, so just strip out |
| // the offending character instead. |
| continue |
| } |
| commandLine = append(commandLine, c) |
| } |
| commandLine = append(commandLine, '"') |
| } else { |
| if len(args) == 1 { |
| // args[0] is a valid command line representing itself. |
| // No need to allocate a new slice or string for it. |
| return prog |
| } |
| commandLine = []byte(prog) |
| } |
| |
| for _, arg := range args[1:] { |
| commandLine = append(commandLine, ' ') |
| // TODO(bcmills): since we're already appending to a slice, it would be nice |
| // to avoid the intermediate allocations of EscapeArg. |
| // Perhaps we can factor out an appendEscapedArg function. |
| commandLine = append(commandLine, EscapeArg(arg)...) |
| } |
| return string(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. |
| // DecomposeCommandLine returns an error if commandLine contains NUL. |
| func DecomposeCommandLine(commandLine string) ([]string, error) { |
| if len(commandLine) == 0 { |
| return []string{}, nil |
| } |
| utf16CommandLine, err := UTF16FromString(commandLine) |
| if err != nil { |
| return nil, errorspkg.New("string with NUL passed to DecomposeCommandLine") |
| } |
| var argc int32 |
| argv, err := commandLineToArgv(&utf16CommandLine[0], &argc) |
| if err != nil { |
| return nil, err |
| } |
| defer LocalFree(Handle(unsafe.Pointer(argv))) |
| |
| var args []string |
| for _, p := range unsafe.Slice(argv, argc) { |
| args = append(args, UTF16PtrToString(p)) |
| } |
| return args, nil |
| } |
| |
| // CommandLineToArgv parses a Unicode command line string and sets |
| // argc to the number of parsed arguments. |
| // |
| // The returned memory should be freed using a single call to LocalFree. |
| // |
| // Note that although the return type of CommandLineToArgv indicates 8192 |
| // entries of up to 8192 characters each, the actual count of parsed arguments |
| // may exceed 8192, and the documentation for CommandLineToArgvW does not mention |
| // any bound on the lengths of the individual argument strings. |
| // (See https://go.dev/issue/63236.) |
| func CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) { |
| argp, err := commandLineToArgv(cmd, argc) |
| argv = (*[8192]*[8192]uint16)(unsafe.Pointer(argp)) |
| return argv, err |
| } |
| |
| func CloseOnExec(fd Handle) { |
| SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0) |
| } |
| |
| // FullPath retrieves the full path of the specified file. |
| func FullPath(name string) (path string, err error) { |
| p, err := UTF16PtrFromString(name) |
| if err != nil { |
| return "", err |
| } |
| n := uint32(100) |
| for { |
| buf := make([]uint16, n) |
| n, err = GetFullPathName(p, uint32(len(buf)), &buf[0], nil) |
| if err != nil { |
| return "", err |
| } |
| if n <= uint32(len(buf)) { |
| return UTF16ToString(buf[:n]), nil |
| } |
| } |
| } |
| |
| // NewProcThreadAttributeList allocates a new ProcThreadAttributeListContainer, with the requested maximum number of attributes. |
| func NewProcThreadAttributeList(maxAttrCount uint32) (*ProcThreadAttributeListContainer, error) { |
| var size uintptr |
| err := initializeProcThreadAttributeList(nil, maxAttrCount, 0, &size) |
| if err != ERROR_INSUFFICIENT_BUFFER { |
| if err == nil { |
| return nil, errorspkg.New("unable to query buffer size from InitializeProcThreadAttributeList") |
| } |
| return nil, err |
| } |
| alloc, err := LocalAlloc(LMEM_FIXED, uint32(size)) |
| if err != nil { |
| return nil, err |
| } |
| // size is guaranteed to be ≥1 by InitializeProcThreadAttributeList. |
| al := &ProcThreadAttributeListContainer{data: (*ProcThreadAttributeList)(unsafe.Pointer(alloc))} |
| err = initializeProcThreadAttributeList(al.data, maxAttrCount, 0, &size) |
| if err != nil { |
| return nil, err |
| } |
| return al, err |
| } |
| |
| // Update modifies the ProcThreadAttributeList using UpdateProcThreadAttribute. |
| func (al *ProcThreadAttributeListContainer) Update(attribute uintptr, value unsafe.Pointer, size uintptr) error { |
| al.pointers = append(al.pointers, value) |
| return updateProcThreadAttribute(al.data, 0, attribute, value, size, nil, nil) |
| } |
| |
| // Delete frees ProcThreadAttributeList's resources. |
| func (al *ProcThreadAttributeListContainer) Delete() { |
| deleteProcThreadAttributeList(al.data) |
| LocalFree(Handle(unsafe.Pointer(al.data))) |
| al.data = nil |
| al.pointers = nil |
| } |
| |
| // List returns the actual ProcThreadAttributeList to be passed to StartupInfoEx. |
| func (al *ProcThreadAttributeListContainer) List() *ProcThreadAttributeList { |
| return al.data |
| } |