Skip to content

Commit 07d4de9

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
os/exec: fix edge cases in Windows PATH resolution
- Ignore empty entries in PATH, like PowerShell does. - If we resolve a path using an explicit relative entry in PATH, treat it the same as we do for the implicit "." search path, by allowing a later (absolute) PATH entry that resolves to the same executable to return the absolute version of its path. - If the requested path does not end with an extension matching PATHEXT, return ErrNotFound (indicating that we potentially searched for multiple alternatives and did not find one) instead of ErrNotExist (which would imply that we know the exact intended path but couldn't find it). Fixes #61493. Change-Id: I5b539d8616e3403825749d8eccf46725fa808a17 Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-race,gotip-windows-amd64-race,gotip-windows-amd64-longtest Reviewed-on: https://go-review.googlesource.com/c/go/+/528037 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Bryan Mills <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent d66fc90 commit 07d4de9

File tree

2 files changed

+79
-20
lines changed

2 files changed

+79
-20
lines changed

src/os/exec/lp_windows.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,18 @@ func findExecutable(file string, exts []string) (string, error) {
4343
if chkStat(file) == nil {
4444
return file, nil
4545
}
46+
// Keep checking exts below, so that programs with weird names
47+
// like "foo.bat.exe" will resolve instead of failing.
4648
}
4749
for _, e := range exts {
4850
if f := file + e; chkStat(f) == nil {
4951
return f, nil
5052
}
5153
}
52-
return "", fs.ErrNotExist
54+
if hasExt(file) {
55+
return "", fs.ErrNotExist
56+
}
57+
return "", ErrNotFound
5358
}
5459

5560
// LookPath searches for an executable named file in the
@@ -112,6 +117,12 @@ func LookPath(file string) (string, error) {
112117

113118
path := os.Getenv("path")
114119
for _, dir := range filepath.SplitList(path) {
120+
if dir == "" {
121+
// Skip empty entries, consistent with what PowerShell does.
122+
// (See https://go.dev/issue/61493#issuecomment-1649724826.)
123+
continue
124+
}
125+
115126
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
116127
if dotErr != nil {
117128
// https://go.dev/issue/53536: if we resolved a relative path implicitly,
@@ -130,7 +141,14 @@ func LookPath(file string) (string, error) {
130141

131142
if !filepath.IsAbs(f) {
132143
if execerrdot.Value() != "0" {
133-
return f, &Error{file, ErrDot}
144+
// If this is the same relative path that we already found,
145+
// dotErr is non-nil and we already checked it above.
146+
// Otherwise, record this path as the one to which we must resolve,
147+
// with or without a dotErr.
148+
if dotErr == nil {
149+
dotf, dotErr = f, &Error{file, ErrDot}
150+
}
151+
continue
134152
}
135153
execerrdot.IncNonDefault()
136154
}

src/os/exec/lp_windows_test.go

+59-18
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ func cmdPrintPath(args ...string) {
3434
fmt.Println(exe)
3535
}
3636

37+
// makePATH returns a PATH variable referring to the
38+
// given directories relative to a root directory.
39+
//
40+
// The empty string results in an empty entry.
41+
// Paths beginning with . are kept as relative entries.
42+
func makePATH(root string, dirs []string) string {
43+
paths := make([]string, 0, len(dirs))
44+
for _, d := range dirs {
45+
switch {
46+
case d == "":
47+
paths = append(paths, "")
48+
case d == "." || (len(d) >= 2 && d[0] == '.' && os.IsPathSeparator(d[1])):
49+
paths = append(paths, filepath.Clean(d))
50+
default:
51+
paths = append(paths, filepath.Join(root, d))
52+
}
53+
}
54+
return strings.Join(paths, string(os.PathListSeparator))
55+
}
56+
3757
// installProgs creates executable files (or symlinks to executable files) at
3858
// multiple destination paths. It uses root as prefix for all destination files.
3959
func installProgs(t *testing.T, root string, files []string) {
@@ -103,12 +123,14 @@ func installBat(t *testing.T, dstPath string) {
103123
}
104124

105125
type lookPathTest struct {
106-
name string
107-
PATHEXT string // empty to use default
108-
files []string // PATH contains all named directories
109-
searchFor string
110-
want string
111-
wantErr error
126+
name string
127+
PATHEXT string // empty to use default
128+
files []string
129+
PATH []string // if nil, use all parent directories from files
130+
searchFor string
131+
want string
132+
wantErr error
133+
skipCmdExeCheck bool // if true, do not check want against the behavior of cmd.exe
112134
}
113135

114136
var lookPathTests = []lookPathTest{
@@ -158,7 +180,7 @@ var lookPathTests = []lookPathTest{
158180
name: "no match with dir",
159181
files: []string{`p1\b.exe`, `p2\a.exe`},
160182
searchFor: `p2\b`,
161-
wantErr: fs.ErrNotExist,
183+
wantErr: exec.ErrNotFound,
162184
},
163185
{
164186
name: "extensionless file in CWD ignored",
@@ -224,6 +246,31 @@ var lookPathTests = []lookPathTest{
224246
searchFor: `a`,
225247
wantErr: exec.ErrNotFound,
226248
},
249+
{
250+
name: "ignore empty PATH entry",
251+
files: []string{`a.bat`, `p\a.bat`},
252+
PATH: []string{`p`},
253+
searchFor: `a`,
254+
want: `p\a.bat`,
255+
// If cmd.exe is too old it might not respect NoDefaultCurrentDirectoryInExePath,
256+
// so skip that check.
257+
skipCmdExeCheck: true,
258+
},
259+
{
260+
name: "return ErrDot if found by a different absolute path",
261+
files: []string{`p1\a.bat`, `p2\a.bat`},
262+
PATH: []string{`.\p1`, `p2`},
263+
searchFor: `a`,
264+
want: `p1\a.bat`,
265+
wantErr: exec.ErrDot,
266+
},
267+
{
268+
name: "suppress ErrDot if also found in absolute path",
269+
files: []string{`p1\a.bat`, `p2\a.bat`},
270+
PATH: []string{`.\p1`, `p1`, `p2`},
271+
searchFor: `a`,
272+
want: `p1\a.bat`,
273+
},
227274
}
228275

229276
func TestLookPathWindows(t *testing.T) {
@@ -257,7 +304,7 @@ func TestLookPathWindows(t *testing.T) {
257304
}
258305

259306
var pathVar string
260-
{
307+
if tt.PATH == nil {
261308
paths := make([]string, 0, len(tt.files))
262309
for _, f := range tt.files {
263310
dir := filepath.Join(root, filepath.Dir(f))
@@ -266,13 +313,15 @@ func TestLookPathWindows(t *testing.T) {
266313
}
267314
}
268315
pathVar = strings.Join(paths, string(os.PathListSeparator))
316+
} else {
317+
pathVar = makePATH(root, tt.PATH)
269318
}
270319
t.Setenv("PATH", pathVar)
271320
t.Logf("set PATH=%s", pathVar)
272321

273322
chdir(t, root)
274323

275-
if !testing.Short() {
324+
if !testing.Short() && !(tt.skipCmdExeCheck || errors.Is(tt.wantErr, exec.ErrDot)) {
276325
// Check that cmd.exe, which is our source of ground truth,
277326
// agrees that our test case is correct.
278327
cmd := testenv.Command(t, cmdExe, "/c", tt.searchFor, "printpath")
@@ -501,15 +550,7 @@ func TestCommand(t *testing.T) {
501550
root := t.TempDir()
502551
installProgs(t, root, tt.files)
503552

504-
paths := make([]string, 0, len(tt.PATH))
505-
for _, p := range tt.PATH {
506-
if p == "." {
507-
paths = append(paths, ".")
508-
} else {
509-
paths = append(paths, filepath.Join(root, p))
510-
}
511-
}
512-
pathVar := strings.Join(paths, string(os.PathListSeparator))
553+
pathVar := makePATH(root, tt.PATH)
513554
t.Setenv("PATH", pathVar)
514555
t.Logf("set PATH=%s", pathVar)
515556

0 commit comments

Comments
 (0)