internal/fetch: fs getter: check for .zip

In the module cache, some directories have .info and .mod files but no
.zip, because the go command doesn't need it. We can't display these
modules without the zip. So return NotFound for these modules, even
for Info and Mod.

Also, return derrors.NotFound any time we can't find a file.

Also, rename fsModuleGetter to fsProxyModuleGetter to make it clear
that the filesystem has to be organized like the proxy protocol.

For golang/go#47834

Change-Id: Ia7d6260abc91a6172820d9de6bf759567720a443
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/344389
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/internal/fetch/getters.go b/internal/fetch/getters.go
index 2471f4b..75618dc 100644
--- a/internal/fetch/getters.go
+++ b/internal/fetch/getters.go
@@ -156,23 +156,29 @@
 	return 0, errors.New("directoryModuleGetter.ZipSize unimplemented")
 }
 
-// An fsModuleGetter gets modules from a directory in the filesystem
+// An fsProxyModuleGetter gets modules from a directory in the filesystem
 // that is organized like the proxy, with paths that correspond to proxy
 // URLs. An example of such a directory is $(go env GOMODCACHE)/cache/download.
-type fsModuleGetter struct {
+type fsProxyModuleGetter struct {
 	dir string
 }
 
 // NewFSModuleGetter return a ModuleGetter that reads modules from a filesystem
 // directory organized like the proxy.
-func NewFSModuleGetter(dir string) ModuleGetter {
-	return &fsModuleGetter{dir: dir}
+func NewFSProxyModuleGetter(dir string) ModuleGetter {
+	return &fsProxyModuleGetter{dir: dir}
 }
 
 // Info returns basic information about the module.
-func (g *fsModuleGetter) Info(ctx context.Context, path, version string) (_ *proxy.VersionInfo, err error) {
-	defer derrors.Wrap(&err, "fsModuleGetter.Info(%q, %q)", path, version)
+func (g *fsProxyModuleGetter) Info(ctx context.Context, path, version string) (_ *proxy.VersionInfo, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.Info(%q, %q)", path, version)
 
+	// Check for a .zip file. Some directories in the download cache have .info and .mod files but no .zip.
+	f, err := g.openFile(path, version, "zip")
+	if err != nil {
+		return nil, err
+	}
+	f.Close()
 	data, err := g.readFile(path, version, "info")
 	if err != nil {
 		return nil, err
@@ -185,15 +191,21 @@
 }
 
 // Mod returns the contents of the module's go.mod file.
-func (g *fsModuleGetter) Mod(ctx context.Context, path, version string) (_ []byte, err error) {
-	defer derrors.Wrap(&err, "fsModuleGetter.Mod(%q, %q)", path, version)
+func (g *fsProxyModuleGetter) Mod(ctx context.Context, path, version string) (_ []byte, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.Mod(%q, %q)", path, version)
 
+	// Check that .zip is readable first.
+	f, err := g.openFile(path, version, "zip")
+	if err != nil {
+		return nil, err
+	}
+	f.Close()
 	return g.readFile(path, version, "mod")
 }
 
 // ContentDir returns an fs.FS for the module's contents.
-func (g *fsModuleGetter) ContentDir(ctx context.Context, path, version string) (_ fs.FS, err error) {
-	defer derrors.Wrap(&err, "fsModuleGetter.ContentDir(%q, %q)", path, version)
+func (g *fsProxyModuleGetter) ContentDir(ctx context.Context, path, version string) (_ fs.FS, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.ContentDir(%q, %q)", path, version)
 
 	data, err := g.readFile(path, version, "zip")
 	if err != nil {
@@ -207,16 +219,14 @@
 }
 
 // ZipSize returns the approximate size of the zip file in bytes.
-func (g *fsModuleGetter) ZipSize(ctx context.Context, path, version string) (int64, error) {
-	return 0, errors.New("fsModuleGetter.ZipSize unimplemented")
+func (g *fsProxyModuleGetter) ZipSize(ctx context.Context, path, version string) (int64, error) {
+	return 0, errors.New("fsProxyModuleGetter.ZipSize unimplemented")
 }
 
-func (g *fsModuleGetter) readFile(path, version, suffix string) (_ []byte, err error) {
-	epath, err := g.escapedPath(path, version, suffix)
-	if err != nil {
-		return nil, err
-	}
-	f, err := os.Open(epath)
+func (g *fsProxyModuleGetter) readFile(path, version, suffix string) (_ []byte, err error) {
+	defer derrors.Wrap(&err, "fsProxyModuleGetter.readFile(%q, %q, %q)", path, version, suffix)
+
+	f, err := g.openFile(path, version, suffix)
 	if err != nil {
 		return nil, err
 	}
@@ -224,7 +234,22 @@
 	return ioutil.ReadAll(f)
 }
 
-func (g *fsModuleGetter) escapedPath(modulePath, version, suffix string) (string, error) {
+func (g *fsProxyModuleGetter) openFile(path, version, suffix string) (_ *os.File, err error) {
+	epath, err := g.escapedPath(path, version, suffix)
+	if err != nil {
+		return nil, err
+	}
+	f, err := os.Open(epath)
+	if err != nil {
+		if errors.Is(err, fs.ErrNotExist) {
+			err = fmt.Errorf("%w: %v", derrors.NotFound, err)
+		}
+		return nil, err
+	}
+	return f, nil
+}
+
+func (g *fsProxyModuleGetter) escapedPath(modulePath, version, suffix string) (string, error) {
 	ep, err := module.EscapePath(modulePath)
 	if err != nil {
 		return "", fmt.Errorf("path: %v: %w", err, derrors.InvalidArgument)
diff --git a/internal/fetch/getters_test.go b/internal/fetch/getters_test.go
index dab3c5f..811843e 100644
--- a/internal/fetch/getters_test.go
+++ b/internal/fetch/getters_test.go
@@ -45,7 +45,7 @@
 			"dir/github.com/a!bc/@v/v2.3.4.zip",
 		},
 	} {
-		g := NewFSModuleGetter("dir").(*fsModuleGetter)
+		g := NewFSProxyModuleGetter("dir").(*fsProxyModuleGetter)
 		got, err := g.escapedPath(test.path, test.version, test.suffix)
 		if err != nil {
 			t.Fatal(err)
@@ -56,7 +56,7 @@
 	}
 }
 
-func TestFSGetter(t *testing.T) {
+func TestFSProxyGetter(t *testing.T) {
 	ctx := context.Background()
 	const (
 		modulePath = "github.com/jackc/pgio"
@@ -67,7 +67,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	g := NewFSModuleGetter("testdata/modcache")
+	g := NewFSProxyModuleGetter("testdata/modcache")
 	t.Run("info", func(t *testing.T) {
 		got, err := g.Info(ctx, modulePath, version)
 		if err != nil {
@@ -77,6 +77,10 @@
 		if !cmp.Equal(got, want) {
 			t.Errorf("got %+v, want %+v", got, want)
 		}
+
+		if _, err := g.Info(ctx, "nozip.com", version); !errors.Is(err, derrors.NotFound) {
+			t.Errorf("got %v, want NotFound", err)
+		}
 	})
 	t.Run("mod", func(t *testing.T) {
 		got, err := g.Mod(ctx, modulePath, version)
@@ -87,6 +91,10 @@
 		if !cmp.Equal(got, want) {
 			t.Errorf("got %q, want %q", got, want)
 		}
+
+		if _, err := g.Mod(ctx, "nozip.com", version); !errors.Is(err, derrors.NotFound) {
+			t.Errorf("got %v, want NotFound", err)
+		}
 	})
 	t.Run("contentdir", func(t *testing.T) {
 		fsys, err := g.ContentDir(ctx, modulePath, version)
@@ -107,5 +115,9 @@
 		if !cmp.Equal(got, want) {
 			t.Errorf("got %q, want %q", got, want)
 		}
+
+		if _, err := g.ContentDir(ctx, "nozip.com", version); !errors.Is(err, derrors.NotFound) {
+			t.Errorf("got %v, want NotFound", err)
+		}
 	})
 }
diff --git a/internal/fetch/testdata/modcache/nozip.com/@v/v1.0.0.info b/internal/fetch/testdata/modcache/nozip.com/@v/v1.0.0.info
new file mode 100644
index 0000000..2e4d188
--- /dev/null
+++ b/internal/fetch/testdata/modcache/nozip.com/@v/v1.0.0.info
@@ -0,0 +1 @@
+{"Version":"v1.0.0","Time":"2019-03-30T17:04:38Z"}
\ No newline at end of file
diff --git a/internal/fetch/testdata/modcache/nozip.com/@v/v1.0.0.mod b/internal/fetch/testdata/modcache/nozip.com/@v/v1.0.0.mod
new file mode 100644
index 0000000..c1efddd
--- /dev/null
+++ b/internal/fetch/testdata/modcache/nozip.com/@v/v1.0.0.mod
@@ -0,0 +1,3 @@
+module github.com/jackc/pgio
+
+go 1.12