cmd/go/internal/modfetch: make source cache directories unwritable

When we run tests of dependency packages,
we run them in their source directories
(as we do for all tests).

Discourage modification of the source directories
by marking them unwritable. This will undoubtedly
break some tests that write temporary files to the
current directory and then clean up after themselves.
That's OK: those tests should be fixed.
(We have a bunch of those ourselves, to be sure.)

Change-Id: Ie0e6713c0121a359b2f7153fe6cf27eac381b931
Reviewed-on: https://go-review.googlesource.com/122401
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/vendor/cmd/go/go_test.go b/vendor/cmd/go/go_test.go
index ecd37e3..555b457 100644
--- a/vendor/cmd/go/go_test.go
+++ b/vendor/cmd/go/go_test.go
@@ -771,13 +771,25 @@
 		}
 	}
 	for _, path := range tg.temps {
-		tg.check(os.RemoveAll(path))
+		tg.check(removeAll(path))
 	}
 	if tg.tempdir != "" {
-		tg.check(os.RemoveAll(tg.tempdir))
+		tg.check(removeAll(tg.tempdir))
 	}
 }
 
+func removeAll(dir string) error {
+	// module cache has 0444 directories;
+	// make them writable in order to remove content.
+	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if info.IsDir() {
+			os.Chmod(path, 0777)
+		}
+		return nil
+	})
+	return os.RemoveAll(dir)
+}
+
 // failSSH puts an ssh executable in the PATH that always fails.
 // This is to stub out uses of ssh by go get.
 func (tg *testgoData) failSSH() {
diff --git a/vendor/cmd/go/internal/modfetch/unzip.go b/vendor/cmd/go/internal/modfetch/unzip.go
index 2e08aaa..3c69803 100644
--- a/vendor/cmd/go/internal/modfetch/unzip.go
+++ b/vendor/cmd/go/internal/modfetch/unzip.go
@@ -11,9 +11,11 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"cmd/go/internal/modfetch/codehost"
+	"cmd/go/internal/str"
 )
 
 func Unzip(dir, zipfile, prefix string, maxSize int64) error {
@@ -49,12 +51,15 @@
 	// Check total size.
 	var size int64
 	for _, zf := range z.File {
-		if !strings.HasPrefix(zf.Name, prefix) {
+		if !str.HasPathPrefix(zf.Name, prefix) {
 			return fmt.Errorf("unzip %v: unexpected file name %s", zipfile, zf.Name)
 		}
-		if strings.HasSuffix(zf.Name, "/") {
+		if zf.Name == prefix || strings.HasSuffix(zf.Name, "/") {
 			continue
 		}
+		if filepath.Clean(zf.Name) != zf.Name || strings.HasPrefix(zf.Name[len(prefix)+1:], "/") {
+			return fmt.Errorf("unzip %v: invalid file name %s", zipfile, zf.Name)
+		}
 		s := int64(zf.UncompressedSize64)
 		if s < 0 || maxSize-size < s {
 			return fmt.Errorf("unzip %v: content too large", zipfile)
@@ -63,11 +68,18 @@
 	}
 
 	// Unzip, enforcing sizes checked earlier.
+	dirs := map[string]bool{dir: true}
 	for _, zf := range z.File {
-		if strings.HasSuffix(zf.Name, "/") {
+		if zf.Name == prefix || strings.HasSuffix(zf.Name, "/") {
 			continue
 		}
-		dst := filepath.Join(dir, zf.Name[len(prefix):])
+		name := zf.Name[len(prefix):]
+		dst := filepath.Join(dir, name)
+		parent := filepath.Dir(dst)
+		for parent != dir {
+			dirs[parent] = true
+			parent = filepath.Dir(parent)
+		}
 		if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil {
 			return err
 		}
@@ -95,5 +107,17 @@
 		}
 	}
 
+	// Mark directories unwritable, best effort.
+	var dirlist []string
+	for dir := range dirs {
+		dirlist = append(dirlist, dir)
+	}
+	sort.Strings(dirlist)
+
+	// Run over list backward to chmod children before parents.
+	for i := len(dirlist) - 1; i >= 0; i-- {
+		os.Chmod(dir, 0555)
+	}
+
 	return nil
 }
diff --git a/vendor/cmd/go/mod_test.go b/vendor/cmd/go/mod_test.go
index bb303c0..7883452 100644
--- a/vendor/cmd/go/mod_test.go
+++ b/vendor/cmd/go/mod_test.go
@@ -853,6 +853,14 @@
 
 	tg.run("list", "-f={{.Dir}}", "rsc.io/quote") // downloads code to load package
 	tg.grepStdout(`mod[\\/]rsc.io[\\/]quote@v1.2.0`, "expected cached copy of code")
+	dir := strings.TrimSpace(tg.getStdout())
+	info, err := os.Stat(dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if info.Mode()&0222 != 0 {
+		t.Fatalf("%s should be unwritable", dir)
+	}
 
 	tg.run("list", "-m", "-f={{.Dir}}", "rsc.io/quote") // now module list should find it too
 	tg.grepStdout(`mod[\\/]rsc.io[\\/]quote@v1.2.0`, "expected cached copy of code")