exec_windows.go raw

   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  }
 249