internal/stdlib: add ContentDir function

Add a function that returns the content directory of the stdlib
module.

Remove the part of the test that checks go.mod; it was never
executing, because there is no go.mod file in any of the repos being
tested.

For golang/go#47834

Change-Id: Idc982620f6736ec60fe9a295f0372fd272745457
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/343965
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/internal/stdlib/stdlib.go b/internal/stdlib/stdlib.go
index 96ae3c5..c1f7ffe 100644
--- a/internal/stdlib/stdlib.go
+++ b/internal/stdlib/stdlib.go
@@ -13,6 +13,7 @@
 	"bytes"
 	"fmt"
 	"io"
+	"io/fs"
 	"os"
 	"path"
 	"path/filepath"
@@ -331,6 +332,11 @@
 	// https://github.com/shurcooL/play/blob/master/256/moduleproxy/std/std.go.
 	defer derrors.Wrap(&err, "stdlib.Zip(%q)", requestedVersion)
 
+	zr, resolvedVersion, commitTime, _, err := zipInternal(requestedVersion)
+	return zr, resolvedVersion, commitTime, err
+}
+
+func zipInternal(requestedVersion string) (_ *zip.Reader, resolvedVersion string, commitTime time.Time, prefix string, err error) {
 	var repo *git.Repository
 	if UseTestData {
 		repo, err = getTestGoRepo(requestedVersion)
@@ -338,23 +344,23 @@
 		if requestedVersion == version.Latest {
 			requestedVersion, err = semanticVersion(requestedVersion)
 			if err != nil {
-				return nil, "", time.Time{}, err
+				return nil, "", time.Time{}, "", err
 			}
 		}
 		repo, err = getGoRepo(requestedVersion)
 	}
 	if err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	var buf bytes.Buffer
 	z := zip.NewWriter(&buf)
 	head, err := repo.Head()
 	if err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	commit, err := repo.CommitObject(head.Hash())
 	if err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	resolvedVersion = requestedVersion
 	if SupportedBranches[requestedVersion] {
@@ -362,33 +368,60 @@
 	}
 	root, err := repo.TreeObject(commit.TreeHash)
 	if err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	prefixPath := ModulePath + "@" + requestedVersion
 	// Add top-level files.
 	if err := addFiles(z, repo, root, prefixPath, false); err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	// Add files from the stdlib directory.
 	libdir := root
 	for _, d := range strings.Split(Directory(resolvedVersion), "/") {
 		libdir, err = subTree(repo, libdir, d)
 		if err != nil {
-			return nil, "", time.Time{}, err
+			return nil, "", time.Time{}, "", err
 		}
 	}
 	if err := addFiles(z, repo, libdir, prefixPath, true); err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	if err := z.Close(); err != nil {
-		return nil, "", time.Time{}, err
+		return nil, "", time.Time{}, "", err
 	}
 	br := bytes.NewReader(buf.Bytes())
 	zr, err := zip.NewReader(br, int64(br.Len()))
 	if err != nil {
+		return nil, "", time.Time{}, "", err
+	}
+	return zr, resolvedVersion, commit.Committer.When, prefixPath, nil
+}
+
+// ContentDir creates an fs.FS representing the entire Go standard library at the
+// given version (which must have been resolved with ZipInfo) and returns a
+// reader to it. It also returns the time of the commit for that version.
+//
+// Normally, ContentDir returns the resolved version it was passed. If the
+// resolved version is a supported branch like "master", ContentDir returns a
+// semantic version for the branch.
+//
+// ContentDir reads the standard library at the Go repository tag corresponding
+// to to the given semantic version.
+//
+// ContentDir ignores go.mod files in the standard library, treating it as if it
+// were a single module named "std" at the given version.
+func ContentDir(requestedVersion string) (_ fs.FS, resolvedVersion string, commitTime time.Time, err error) {
+	defer derrors.Wrap(&err, "stdlib.ContentDir(%q)", requestedVersion)
+
+	zr, resolvedVersion, commitTime, prefix, err := zipInternal(requestedVersion)
+	if err != nil {
 		return nil, "", time.Time{}, err
 	}
-	return zr, resolvedVersion, commit.Committer.When, nil
+	cdir, err := fs.Sub(zr, prefix)
+	if err != nil {
+		return nil, "", time.Time{}, err
+	}
+	return cdir, resolvedVersion, commitTime, nil
 }
 
 func newPseudoVersion(version string, commitTime time.Time, hash plumbing.Hash) string {
@@ -452,7 +485,7 @@
 			continue
 		}
 		if e.Name == "go.mod" {
-			// ignore; we'll synthesize our own
+			// Ignore; we don't need it.
 			continue
 		}
 		if strings.HasPrefix(e.Name, "README") && !strings.Contains(dirpath, "/") {
diff --git a/internal/stdlib/stdlib_test.go b/internal/stdlib/stdlib_test.go
index 812e29a..3555b2f 100644
--- a/internal/stdlib/stdlib_test.go
+++ b/internal/stdlib/stdlib_test.go
@@ -5,9 +5,9 @@
 package stdlib
 
 import (
-	"io/ioutil"
+	"errors"
+	"io/fs"
 	"reflect"
-	"strings"
 	"testing"
 
 	"golang.org/x/mod/semver"
@@ -117,7 +117,7 @@
 	}
 }
 
-func TestZip(t *testing.T) {
+func TestContentDir(t *testing.T) {
 	UseTestData = true
 	defer func() { UseTestData = false }()
 	for _, resolvedVersion := range []string{
@@ -128,7 +128,7 @@
 		version.Master,
 	} {
 		t.Run(resolvedVersion, func(t *testing.T) {
-			zr, gotResolvedVersion, gotTime, err := Zip(resolvedVersion)
+			cdir, gotResolvedVersion, gotTime, err := ContentDir(resolvedVersion)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -151,38 +151,26 @@
 				wantFiles["cmd/README.vendor"] = true
 			}
 
-			wantPrefix := "std@" + resolvedVersion + "/"
-			readmeVendorFile := wantPrefix + "README.vendor"
-			for _, f := range zr.File {
-				if f.Name == readmeVendorFile {
-					t.Fatalf("got %q; want file to be removed", readmeVendorFile)
+			const readmeVendorFile = "README.vendor"
+			if _, err := fs.Stat(cdir, readmeVendorFile); !errors.Is(err, fs.ErrNotExist) {
+				t.Fatalf("fs.Stat returned %v; want %q to be removed", err, readmeVendorFile)
+			}
+			err = fs.WalkDir(cdir, ".", func(path string, d fs.DirEntry, err error) error {
+				if err != nil {
+					return err
 				}
-				if !strings.HasPrefix(f.Name, wantPrefix) {
-					t.Errorf("filename %q missing prefix %q", f.Name, wantPrefix)
-					continue
+				if d.IsDir() {
+					return nil
 				}
-				delete(wantFiles, f.Name[len(wantPrefix):])
+				delete(wantFiles, path)
+				return nil
+			})
+			if err != nil {
+				t.Fatal(err)
 			}
 			if len(wantFiles) > 0 {
 				t.Errorf("zip missing files: %v", reflect.ValueOf(wantFiles).MapKeys())
 			}
-			for _, f := range zr.File {
-				if f.Name == wantPrefix+"go.mod" {
-					r, err := f.Open()
-					if err != nil {
-						t.Fatal(err)
-					}
-					defer r.Close()
-					b, err := ioutil.ReadAll(r)
-					if err != nil {
-						t.Fatal(err)
-					}
-					if got, want := string(b), "module std\n"; got != want {
-						t.Errorf("go.mod: got %q, want %q", got, want)
-					}
-					break
-				}
-			}
 		})
 	}
 }