path/filepath: fix globbing of c:\*dir\... pattern

The problem was introduced by the recent filepath.Join change.

Fixes #14949

Change-Id: I7ee52f210e12bbb1369e308e584ddb2c7766e095
Reviewed-on: https://go-review.googlesource.com/23240
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/src/path/filepath/match.go b/src/path/filepath/match.go
index d64bf84..2adb0c7 100644
--- a/src/path/filepath/match.go
+++ b/src/path/filepath/match.go
@@ -240,13 +240,10 @@
 	}
 
 	dir, file := Split(pattern)
-	switch dir {
-	case "":
-		dir = "."
-	case string(Separator):
-		// nothing
-	default:
-		dir = dir[0 : len(dir)-1] // chop off trailing separator
+	if runtime.GOOS == "windows" {
+		dir = cleanGlobPathWindows(dir)
+	} else {
+		dir = cleanGlobPath(dir)
 	}
 
 	if !hasMeta(dir) {
@@ -267,6 +264,35 @@
 	return
 }
 
+// cleanGlobPath prepares path for glob matching.
+func cleanGlobPath(path string) string {
+	switch path {
+	case "":
+		return "."
+	case string(Separator):
+		// do nothing to the path
+		return path
+	default:
+		return path[0 : len(path)-1] // chop off trailing separator
+	}
+}
+
+// cleanGlobPathWindows is windows version of cleanGlobPath.
+func cleanGlobPathWindows(path string) string {
+	vollen := volumeNameLen(path)
+	switch {
+	case path == "":
+		return "."
+	case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /, \, C:\ and C:/
+		// do nothing to the path
+		return path
+	case vollen == len(path) && len(path) == 2: // C:
+		return path + "." // convert C: into C:.
+	default:
+		return path[0 : len(path)-1] // chop off trailing separator
+	}
+}
+
 // glob searches for files matching pattern in the directory dir
 // and appends them to matches. If the directory cannot be
 // opened, it returns the existing matches. New matches are
diff --git a/src/path/filepath/match_test.go b/src/path/filepath/match_test.go
index d8bab7f..8dcfa59 100644
--- a/src/path/filepath/match_test.go
+++ b/src/path/filepath/match_test.go
@@ -5,10 +5,12 @@
 package filepath_test
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 	. "path/filepath"
 	"runtime"
+	"sort"
 	"strings"
 	"testing"
 )
@@ -209,3 +211,164 @@
 		}
 	}
 }
+
+type globTest struct {
+	pattern string
+	matches []string
+}
+
+func (test *globTest) buildWant(root string) []string {
+	want := make([]string, 0)
+	for _, m := range test.matches {
+		want = append(want, root+FromSlash(m))
+	}
+	sort.Strings(want)
+	return want
+}
+
+func (test *globTest) globAbs(root, rootPattern string) error {
+	p := FromSlash(rootPattern + `\` + test.pattern)
+	have, err := Glob(p)
+	if err != nil {
+		return err
+	}
+	sort.Strings(have)
+	want := test.buildWant(root + `\`)
+	if strings.Join(want, "_") == strings.Join(have, "_") {
+		return nil
+	}
+	return fmt.Errorf("Glob(%q) returns %q, but %q expected", p, have, want)
+}
+
+func (test *globTest) globRel(root string) error {
+	p := root + FromSlash(test.pattern)
+	have, err := Glob(p)
+	if err != nil {
+		return err
+	}
+	sort.Strings(have)
+	want := test.buildWant(root)
+	if strings.Join(want, "_") == strings.Join(have, "_") {
+		return nil
+	}
+	// try also matching version without root prefix
+	wantWithNoRoot := test.buildWant("")
+	if strings.Join(wantWithNoRoot, "_") == strings.Join(have, "_") {
+		return nil
+	}
+	return fmt.Errorf("Glob(%q) returns %q, but %q expected", p, have, want)
+}
+
+func TestWindowsGlob(t *testing.T) {
+	if runtime.GOOS != "windows" {
+		t.Skipf("skipping windows specific test")
+	}
+
+	tmpDir, err := ioutil.TempDir("", "TestWindowsGlob")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	// /tmp may itself be a symlink
+	tmpDir, err = EvalSymlinks(tmpDir)
+	if err != nil {
+		t.Fatal("eval symlink for tmp dir:", err)
+	}
+
+	if len(tmpDir) < 3 {
+		t.Fatalf("tmpDir path %q is too short", tmpDir)
+	}
+	if tmpDir[1] != ':' {
+		t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir)
+	}
+
+	dirs := []string{
+		"a",
+		"b",
+		"dir/d/bin",
+	}
+	files := []string{
+		"dir/d/bin/git.exe",
+	}
+	for _, dir := range dirs {
+		err := os.MkdirAll(Join(tmpDir, dir), 0777)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	for _, file := range files {
+		err := ioutil.WriteFile(Join(tmpDir, file), nil, 0666)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	tests := []globTest{
+		{"a", []string{"a"}},
+		{"b", []string{"b"}},
+		{"c", []string{}},
+		{"*", []string{"a", "b", "dir"}},
+		{"d*", []string{"dir"}},
+		{"*i*", []string{"dir"}},
+		{"*r", []string{"dir"}},
+		{"?ir", []string{"dir"}},
+		{"?r", []string{}},
+		{"d*/*/bin/git.exe", []string{"dir/d/bin/git.exe"}},
+	}
+
+	// test absolute paths
+	for _, test := range tests {
+		var p string
+		err = test.globAbs(tmpDir, tmpDir)
+		if err != nil {
+			t.Error(err)
+		}
+		// test C:\*Documents and Settings\...
+		p = tmpDir
+		p = strings.Replace(p, `:\`, `:\*`, 1)
+		err = test.globAbs(tmpDir, p)
+		if err != nil {
+			t.Error(err)
+		}
+		// test C:\Documents and Settings*\...
+		p = tmpDir
+		p = strings.Replace(p, `:\`, `:`, 1)
+		p = strings.Replace(p, `\`, `*\`, 1)
+		p = strings.Replace(p, `:`, `:\`, 1)
+		err = test.globAbs(tmpDir, p)
+		if err != nil {
+			t.Error(err)
+		}
+	}
+
+	// test relative paths
+	wd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = os.Chdir(tmpDir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		err := os.Chdir(wd)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+	for _, test := range tests {
+		err := test.globRel("")
+		if err != nil {
+			t.Error(err)
+		}
+		err = test.globRel(`.\`)
+		if err != nil {
+			t.Error(err)
+		}
+		err = test.globRel(tmpDir[:2]) // C:
+		if err != nil {
+			t.Error(err)
+		}
+	}
+}