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