Source file src/os/exec/lp_windows.go

     1  // Copyright 2010 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  package exec
     6  
     7  import (
     8  	"errors"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  )
    14  
    15  // ErrNotFound is the error resulting if a path search failed to find an executable file.
    16  var ErrNotFound = errors.New("executable file not found in %PATH%")
    17  
    18  func chkStat(file string) error {
    19  	d, err := os.Stat(file)
    20  	if err != nil {
    21  		return err
    22  	}
    23  	if d.IsDir() {
    24  		return fs.ErrPermission
    25  	}
    26  	return nil
    27  }
    28  
    29  func hasExt(file string) bool {
    30  	i := strings.LastIndex(file, ".")
    31  	if i < 0 {
    32  		return false
    33  	}
    34  	return strings.LastIndexAny(file, `:\/`) < i
    35  }
    36  
    37  func findExecutable(file string, exts []string) (string, error) {
    38  	if len(exts) == 0 {
    39  		return file, chkStat(file)
    40  	}
    41  	if hasExt(file) {
    42  		if chkStat(file) == nil {
    43  			return file, nil
    44  		}
    45  		// Keep checking exts below, so that programs with weird names
    46  		// like "foo.bat.exe" will resolve instead of failing.
    47  	}
    48  	for _, e := range exts {
    49  		if f := file + e; chkStat(f) == nil {
    50  			return f, nil
    51  		}
    52  	}
    53  	if hasExt(file) {
    54  		return "", fs.ErrNotExist
    55  	}
    56  	return "", ErrNotFound
    57  }
    58  
    59  func lookPath(file string) (string, error) {
    60  	if err := validateLookPath(file); err != nil {
    61  		return "", &Error{file, err}
    62  	}
    63  
    64  	return lookPathExts(file, pathExt())
    65  }
    66  
    67  // lookExtensions finds windows executable by its dir and path.
    68  // It uses LookPath to try appropriate extensions.
    69  // lookExtensions does not search PATH, instead it converts `prog` into `.\prog`.
    70  //
    71  // If the path already has an extension found in PATHEXT,
    72  // lookExtensions returns it directly without searching
    73  // for additional extensions. For example,
    74  // "C:\foo\example.com" would be returned as-is even if the
    75  // program is actually "C:\foo\example.com.exe".
    76  func lookExtensions(path, dir string) (string, error) {
    77  	if err := validateLookPath(path); err != nil {
    78  		return "", &Error{path, err}
    79  	}
    80  
    81  	if filepath.Base(path) == path {
    82  		path = "." + string(filepath.Separator) + path
    83  	}
    84  	exts := pathExt()
    85  	if ext := filepath.Ext(path); ext != "" {
    86  		for _, e := range exts {
    87  			if strings.EqualFold(ext, e) {
    88  				// Assume that path has already been resolved.
    89  				return path, nil
    90  			}
    91  		}
    92  	}
    93  	if dir == "" {
    94  		return lookPathExts(path, exts)
    95  	}
    96  	if filepath.VolumeName(path) != "" {
    97  		return lookPathExts(path, exts)
    98  	}
    99  	if len(path) > 1 && os.IsPathSeparator(path[0]) {
   100  		return lookPathExts(path, exts)
   101  	}
   102  	dirandpath := filepath.Join(dir, path)
   103  	// We assume that LookPath will only add file extension.
   104  	lp, err := lookPathExts(dirandpath, exts)
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  	ext := strings.TrimPrefix(lp, dirandpath)
   109  	return path + ext, nil
   110  }
   111  
   112  func pathExt() []string {
   113  	var exts []string
   114  	x := os.Getenv(`PATHEXT`)
   115  	if x != "" {
   116  		for e := range strings.SplitSeq(strings.ToLower(x), `;`) {
   117  			if e == "" {
   118  				continue
   119  			}
   120  			if e[0] != '.' {
   121  				e = "." + e
   122  			}
   123  			exts = append(exts, e)
   124  		}
   125  	} else {
   126  		exts = []string{".com", ".exe", ".bat", ".cmd"}
   127  	}
   128  	return exts
   129  }
   130  
   131  // lookPathExts implements LookPath for the given PATHEXT list.
   132  func lookPathExts(file string, exts []string) (string, error) {
   133  	if strings.ContainsAny(file, `:\/`) {
   134  		f, err := findExecutable(file, exts)
   135  		if err == nil {
   136  			return f, nil
   137  		}
   138  		return "", &Error{file, err}
   139  	}
   140  
   141  	// On Windows, creating the NoDefaultCurrentDirectoryInExePath
   142  	// environment variable (with any value or no value!) signals that
   143  	// path lookups should skip the current directory.
   144  	// In theory we are supposed to call NeedCurrentDirectoryForExePathW
   145  	// "as the registry location of this environment variable can change"
   146  	// but that seems exceedingly unlikely: it would break all users who
   147  	// have configured their environment this way!
   148  	// https://docs.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-needcurrentdirectoryforexepathw
   149  	// See also go.dev/issue/43947.
   150  	var (
   151  		dotf   string
   152  		dotErr error
   153  	)
   154  	if _, found := os.LookupEnv("NoDefaultCurrentDirectoryInExePath"); !found {
   155  		if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
   156  			if execerrdot.Value() == "0" {
   157  				execerrdot.IncNonDefault()
   158  				return f, nil
   159  			}
   160  			dotf, dotErr = f, &Error{file, ErrDot}
   161  		}
   162  	}
   163  
   164  	path := os.Getenv("path")
   165  	for _, dir := range filepath.SplitList(path) {
   166  		if dir == "" {
   167  			// Skip empty entries, consistent with what PowerShell does.
   168  			// (See https://go.dev/issue/61493#issuecomment-1649724826.)
   169  			continue
   170  		}
   171  
   172  		if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
   173  			if dotErr != nil {
   174  				// https://go.dev/issue/53536: if we resolved a relative path implicitly,
   175  				// and it is the same executable that would be resolved from the explicit %PATH%,
   176  				// prefer the explicit name for the executable (and, likely, no error) instead
   177  				// of the equivalent implicit name with ErrDot.
   178  				//
   179  				// Otherwise, return the ErrDot for the implicit path as soon as we find
   180  				// out that the explicit one doesn't match.
   181  				dotfi, dotfiErr := os.Lstat(dotf)
   182  				fi, fiErr := os.Lstat(f)
   183  				if dotfiErr != nil || fiErr != nil || !os.SameFile(dotfi, fi) {
   184  					return dotf, dotErr
   185  				}
   186  			}
   187  
   188  			if !filepath.IsAbs(f) {
   189  				if execerrdot.Value() != "0" {
   190  					// If this is the same relative path that we already found,
   191  					// dotErr is non-nil and we already checked it above.
   192  					// Otherwise, record this path as the one to which we must resolve,
   193  					// with or without a dotErr.
   194  					if dotErr == nil {
   195  						dotf, dotErr = f, &Error{file, ErrDot}
   196  					}
   197  					continue
   198  				}
   199  				execerrdot.IncNonDefault()
   200  			}
   201  			return f, nil
   202  		}
   203  	}
   204  
   205  	if dotErr != nil {
   206  		return dotf, dotErr
   207  	}
   208  	return "", &Error{file, ErrNotFound}
   209  }
   210  

View as plain text