github.com/tristanisham/sys@v0.0.0-20240326010300-a16cbabb7555/windows/exec_windows.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Fork, exec, wait, etc. 6 7 package windows 8 9 import ( 10 errorspkg "errors" 11 "unsafe" 12 ) 13 14 // EscapeArg rewrites command line argument s as prescribed 15 // in http://msdn.microsoft.com/en-us/library/ms880421. 16 // This function returns "" (2 double quotes) if s is empty. 17 // Alternatively, these transformations are done: 18 // - every back slash (\) is doubled, but only if immediately 19 // followed by double quote ("); 20 // - every double quote (") is escaped by back slash (\); 21 // - finally, s is wrapped with double quotes (arg -> "arg"), 22 // but only if there is space or tab inside s. 23 func EscapeArg(s string) string { 24 if len(s) == 0 { 25 return `""` 26 } 27 n := len(s) 28 hasSpace := false 29 for i := 0; i < len(s); i++ { 30 switch s[i] { 31 case '"', '\\': 32 n++ 33 case ' ', '\t': 34 hasSpace = true 35 } 36 } 37 if hasSpace { 38 n += 2 // Reserve space for quotes. 39 } 40 if n == len(s) { 41 return s 42 } 43 44 qs := make([]byte, n) 45 j := 0 46 if hasSpace { 47 qs[j] = '"' 48 j++ 49 } 50 slashes := 0 51 for i := 0; i < len(s); i++ { 52 switch s[i] { 53 default: 54 slashes = 0 55 qs[j] = s[i] 56 case '\\': 57 slashes++ 58 qs[j] = s[i] 59 case '"': 60 for ; slashes > 0; slashes-- { 61 qs[j] = '\\' 62 j++ 63 } 64 qs[j] = '\\' 65 j++ 66 qs[j] = s[i] 67 } 68 j++ 69 } 70 if hasSpace { 71 for ; slashes > 0; slashes-- { 72 qs[j] = '\\' 73 j++ 74 } 75 qs[j] = '"' 76 j++ 77 } 78 return string(qs[:j]) 79 } 80 81 // ComposeCommandLine escapes and joins the given arguments suitable for use as a Windows command line, 82 // in CreateProcess's CommandLine argument, CreateService/ChangeServiceConfig's BinaryPathName argument, 83 // or any program that uses CommandLineToArgv. 84 func ComposeCommandLine(args []string) string { 85 if len(args) == 0 { 86 return "" 87 } 88 89 // Per https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw: 90 // “This function accepts command lines that contain a program name; the 91 // program name can be enclosed in quotation marks or not.” 92 // 93 // Unfortunately, it provides no means of escaping interior quotation marks 94 // within that program name, and we have no way to report them here. 95 prog := args[0] 96 mustQuote := len(prog) == 0 97 for i := 0; i < len(prog); i++ { 98 c := prog[i] 99 if c <= ' ' || (c == '"' && i == 0) { 100 // Force quotes for not only the ASCII space and tab as described in the 101 // MSDN article, but also ASCII control characters. 102 // The documentation for CommandLineToArgvW doesn't say what happens when 103 // the first argument is not a valid program name, but it empirically 104 // seems to drop unquoted control characters. 105 mustQuote = true 106 break 107 } 108 } 109 var commandLine []byte 110 if mustQuote { 111 commandLine = make([]byte, 0, len(prog)+2) 112 commandLine = append(commandLine, '"') 113 for i := 0; i < len(prog); i++ { 114 c := prog[i] 115 if c == '"' { 116 // This quote would interfere with our surrounding quotes. 117 // We have no way to report an error, so just strip out 118 // the offending character instead. 119 continue 120 } 121 commandLine = append(commandLine, c) 122 } 123 commandLine = append(commandLine, '"') 124 } else { 125 if len(args) == 1 { 126 // args[0] is a valid command line representing itself. 127 // No need to allocate a new slice or string for it. 128 return prog 129 } 130 commandLine = []byte(prog) 131 } 132 133 for _, arg := range args[1:] { 134 commandLine = append(commandLine, ' ') 135 // TODO(bcmills): since we're already appending to a slice, it would be nice 136 // to avoid the intermediate allocations of EscapeArg. 137 // Perhaps we can factor out an appendEscapedArg function. 138 commandLine = append(commandLine, EscapeArg(arg)...) 139 } 140 return string(commandLine) 141 } 142 143 // DecomposeCommandLine breaks apart its argument command line into unescaped parts using CommandLineToArgv, 144 // as gathered from GetCommandLine, QUERY_SERVICE_CONFIG's BinaryPathName argument, or elsewhere that 145 // command lines are passed around. 146 // DecomposeCommandLine returns an error if commandLine contains NUL. 147 func DecomposeCommandLine(commandLine string) ([]string, error) { 148 if len(commandLine) == 0 { 149 return []string{}, nil 150 } 151 utf16CommandLine, err := UTF16FromString(commandLine) 152 if err != nil { 153 return nil, errorspkg.New("string with NUL passed to DecomposeCommandLine") 154 } 155 var argc int32 156 argv, err := commandLineToArgv(&utf16CommandLine[0], &argc) 157 if err != nil { 158 return nil, err 159 } 160 defer LocalFree(Handle(unsafe.Pointer(argv))) 161 162 var args []string 163 for _, p := range unsafe.Slice(argv, argc) { 164 args = append(args, UTF16PtrToString(p)) 165 } 166 return args, nil 167 } 168 169 // CommandLineToArgv parses a Unicode command line string and sets 170 // argc to the number of parsed arguments. 171 // 172 // The returned memory should be freed using a single call to LocalFree. 173 // 174 // Note that although the return type of CommandLineToArgv indicates 8192 175 // entries of up to 8192 characters each, the actual count of parsed arguments 176 // may exceed 8192, and the documentation for CommandLineToArgvW does not mention 177 // any bound on the lengths of the individual argument strings. 178 // (See https://go.dev/issue/63236.) 179 func CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) { 180 argp, err := commandLineToArgv(cmd, argc) 181 argv = (*[8192]*[8192]uint16)(unsafe.Pointer(argp)) 182 return argv, err 183 } 184 185 func CloseOnExec(fd Handle) { 186 SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0) 187 } 188 189 // FullPath retrieves the full path of the specified file. 190 func FullPath(name string) (path string, err error) { 191 p, err := UTF16PtrFromString(name) 192 if err != nil { 193 return "", err 194 } 195 n := uint32(100) 196 for { 197 buf := make([]uint16, n) 198 n, err = GetFullPathName(p, uint32(len(buf)), &buf[0], nil) 199 if err != nil { 200 return "", err 201 } 202 if n <= uint32(len(buf)) { 203 return UTF16ToString(buf[:n]), nil 204 } 205 } 206 } 207 208 // NewProcThreadAttributeList allocates a new ProcThreadAttributeListContainer, with the requested maximum number of attributes. 209 func NewProcThreadAttributeList(maxAttrCount uint32) (*ProcThreadAttributeListContainer, error) { 210 var size uintptr 211 err := initializeProcThreadAttributeList(nil, maxAttrCount, 0, &size) 212 if err != ERROR_INSUFFICIENT_BUFFER { 213 if err == nil { 214 return nil, errorspkg.New("unable to query buffer size from InitializeProcThreadAttributeList") 215 } 216 return nil, err 217 } 218 alloc, err := LocalAlloc(LMEM_FIXED, uint32(size)) 219 if err != nil { 220 return nil, err 221 } 222 // size is guaranteed to be ≥1 by InitializeProcThreadAttributeList. 223 al := &ProcThreadAttributeListContainer{data: (*ProcThreadAttributeList)(unsafe.Pointer(alloc))} 224 err = initializeProcThreadAttributeList(al.data, maxAttrCount, 0, &size) 225 if err != nil { 226 return nil, err 227 } 228 return al, err 229 } 230 231 // Update modifies the ProcThreadAttributeList using UpdateProcThreadAttribute. 232 func (al *ProcThreadAttributeListContainer) Update(attribute uintptr, value unsafe.Pointer, size uintptr) error { 233 al.pointers = append(al.pointers, value) 234 return updateProcThreadAttribute(al.data, 0, attribute, value, size, nil, nil) 235 } 236 237 // Delete frees ProcThreadAttributeList's resources. 238 func (al *ProcThreadAttributeListContainer) Delete() { 239 deleteProcThreadAttributeList(al.data) 240 LocalFree(Handle(unsafe.Pointer(al.data))) 241 al.data = nil 242 al.pointers = nil 243 } 244 245 // List returns the actual ProcThreadAttributeList to be passed to StartupInfoEx. 246 func (al *ProcThreadAttributeListContainer) List() *ProcThreadAttributeList { 247 return al.data 248 }