internal/govulncheck: copy from x/vuln repo

Currently, gopls maintains an edited copy of the govulncheck command
logic in the internal/vulncheck directory, along with the necessary
glue to make it work with gopls.

This CL is the first in a sequence that will make it easier for gopls
to use that logic.

It creates a new package, internal/govulncheck, adds a script to copy
the corresponding package from the x/vuln repo, and removes the cache
in internal/vulncheck in favor of the copied one.

Although it might appear simpler to copy the .go files directly into
internal/vulncheck, that would require editing the package directives
in the files, and has the risk of overwriting files.

Change-Id: I00f726f7b142048da2407f212873420df54844b3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/405997
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/gopls/internal/govulncheck/README.md b/gopls/internal/govulncheck/README.md
new file mode 100644
index 0000000..d8339c5
--- /dev/null
+++ b/gopls/internal/govulncheck/README.md
@@ -0,0 +1,17 @@
+# internal/govulncheck package
+
+This package is a literal copy of the cmd/govulncheck/internal/govulncheck
+package in the vuln repo (https://go.googlesource.com/vuln).
+
+The `copy.sh` does the copying, after removing all .go files here. To use it:
+
+1. Clone the vuln repo to a directory next to the directory holding this repo
+   (tools). After doing that your directory structure should look something like
+   ```
+   ~/repos/x/tools/gopls/...
+   ~/repos/x/vuln/...
+   ```
+
+2. cd to this directory.
+
+3. Run `copy.sh`.
diff --git a/gopls/internal/vulncheck/cache.go b/gopls/internal/govulncheck/cache.go
similarity index 82%
rename from gopls/internal/vulncheck/cache.go
rename to gopls/internal/govulncheck/cache.go
index 39a38fb..404c356 100644
--- a/gopls/internal/vulncheck/cache.go
+++ b/gopls/internal/govulncheck/cache.go
@@ -2,7 +2,11 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package vulncheck
+//go:build go1.18
+// +build go1.18
+
+// Package govulncheck supports the govulncheck command.
+package govulncheck
 
 import (
 	"encoding/json"
@@ -17,8 +21,6 @@
 	"golang.org/x/vuln/osv"
 )
 
-// copy from x/vuln/cmd/govulncheck/cache.go
-
 // The cache uses a single JSON index file for each vulnerability database
 // which contains the map from packages to the time the last
 // vulnerability for that package was added/modified and the time that
@@ -37,19 +39,22 @@
 // $GOPATH/pkg/mod/cache/download/vulndb/{db hostname}/{import path}/vulns.json
 //   []*osv.Entry
 
-// fsCache is a thread-safe file-system cache implementing osv.Cache
+// FSCache is a thread-safe file-system cache implementing osv.Cache
 //
 // TODO: use something like cmd/go/internal/lockedfile for thread safety?
-type fsCache struct {
+type FSCache struct {
 	mu      sync.Mutex
 	rootDir string
 }
 
+// Assert that *FSCache implements client.Cache.
+var _ client.Cache = (*FSCache)(nil)
+
 // use cfg.GOMODCACHE available in cmd/go/internal?
 var defaultCacheRoot = filepath.Join(build.Default.GOPATH, "/pkg/mod/cache/download/vulndb")
 
-func defaultCache() *fsCache {
-	return &fsCache{rootDir: defaultCacheRoot}
+func DefaultCache() *FSCache {
+	return &FSCache{rootDir: defaultCacheRoot}
 }
 
 type cachedIndex struct {
@@ -57,7 +62,7 @@
 	Index     client.DBIndex
 }
 
-func (c *fsCache) ReadIndex(dbName string) (client.DBIndex, time.Time, error) {
+func (c *FSCache) ReadIndex(dbName string) (client.DBIndex, time.Time, error) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
@@ -75,7 +80,7 @@
 	return index.Index, index.Retrieved, nil
 }
 
-func (c *fsCache) WriteIndex(dbName string, index client.DBIndex, retrieved time.Time) error {
+func (c *FSCache) WriteIndex(dbName string, index client.DBIndex, retrieved time.Time) error {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
@@ -96,7 +101,7 @@
 	return nil
 }
 
-func (c *fsCache) ReadEntries(dbName string, p string) ([]*osv.Entry, error) {
+func (c *FSCache) ReadEntries(dbName string, p string) ([]*osv.Entry, error) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
@@ -114,7 +119,7 @@
 	return entries, nil
 }
 
-func (c *fsCache) WriteEntries(dbName string, p string, entries []*osv.Entry) error {
+func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) error {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
diff --git a/gopls/internal/govulncheck/cache_test.go b/gopls/internal/govulncheck/cache_test.go
new file mode 100644
index 0000000..5a25c78
--- /dev/null
+++ b/gopls/internal/govulncheck/cache_test.go
@@ -0,0 +1,165 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.18
+// +build go1.18
+
+package govulncheck
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+	"time"
+
+	"golang.org/x/sync/errgroup"
+	"golang.org/x/vuln/client"
+	"golang.org/x/vuln/osv"
+)
+
+func TestCache(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	cache := &FSCache{rootDir: tmpDir}
+	dbName := "vulndb.golang.org"
+
+	_, _, err := cache.ReadIndex(dbName)
+	if err != nil {
+		t.Fatalf("ReadIndex failed for non-existent database: %v", err)
+	}
+
+	if err = os.Mkdir(filepath.Join(tmpDir, dbName), 0777); err != nil {
+		t.Fatalf("os.Mkdir failed: %v", err)
+	}
+	_, _, err = cache.ReadIndex(dbName)
+	if err != nil {
+		t.Fatalf("ReadIndex failed for database without cached index: %v", err)
+	}
+
+	now := time.Now()
+	expectedIdx := client.DBIndex{
+		"a.vuln.example.com": time.Time{}.Add(time.Hour),
+		"b.vuln.example.com": time.Time{}.Add(time.Hour * 2),
+		"c.vuln.example.com": time.Time{}.Add(time.Hour * 3),
+	}
+	if err = cache.WriteIndex(dbName, expectedIdx, now); err != nil {
+		t.Fatalf("WriteIndex failed to write index: %v", err)
+	}
+
+	idx, retrieved, err := cache.ReadIndex(dbName)
+	if err != nil {
+		t.Fatalf("ReadIndex failed for database with cached index: %v", err)
+	}
+	if !reflect.DeepEqual(idx, expectedIdx) {
+		t.Errorf("ReadIndex returned unexpected index, got:\n%s\nwant:\n%s", idx, expectedIdx)
+	}
+	if !retrieved.Equal(now) {
+		t.Errorf("ReadIndex returned unexpected retrieved: got %s, want %s", retrieved, now)
+	}
+
+	if _, err = cache.ReadEntries(dbName, "vuln.example.com"); err != nil {
+		t.Fatalf("ReadEntires failed for non-existent package: %v", err)
+	}
+
+	expectedEntries := []*osv.Entry{
+		{ID: "001"},
+		{ID: "002"},
+		{ID: "003"},
+	}
+	if err := cache.WriteEntries(dbName, "vuln.example.com", expectedEntries); err != nil {
+		t.Fatalf("WriteEntries failed: %v", err)
+	}
+
+	entries, err := cache.ReadEntries(dbName, "vuln.example.com")
+	if err != nil {
+		t.Fatalf("ReadEntries failed for cached package: %v", err)
+	}
+	if !reflect.DeepEqual(entries, expectedEntries) {
+		t.Errorf("ReadEntries returned unexpected entries, got:\n%v\nwant:\n%v", entries, expectedEntries)
+	}
+}
+
+func TestConcurrency(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	cache := &FSCache{rootDir: tmpDir}
+	dbName := "vulndb.golang.org"
+
+	g := new(errgroup.Group)
+	for i := 0; i < 1000; i++ {
+		i := i
+		g.Go(func() error {
+			id := i % 5
+			p := fmt.Sprintf("package%d", id)
+
+			entries, err := cache.ReadEntries(dbName, p)
+			if err != nil {
+				return err
+			}
+
+			err = cache.WriteEntries(dbName, p, append(entries, &osv.Entry{ID: fmt.Sprint(id)}))
+			if err != nil {
+				return err
+			}
+			return nil
+		})
+	}
+
+	if err := g.Wait(); err != nil {
+		t.Errorf("error in parallel cache entries read/write: %v", err)
+	}
+
+	// sanity checking
+	for i := 0; i < 5; i++ {
+		id := fmt.Sprint(i)
+		p := fmt.Sprintf("package%s", id)
+
+		es, err := cache.ReadEntries(dbName, p)
+		if err != nil {
+			t.Fatalf("failed to read entries: %v", err)
+		}
+		for _, e := range es {
+			if e.ID != id {
+				t.Errorf("want %s ID for vuln entry; got %s", id, e.ID)
+			}
+		}
+	}
+
+	// do similar for cache index
+	start := time.Now()
+	for i := 0; i < 1000; i++ {
+		i := i
+		g.Go(func() error {
+			id := i % 5
+			p := fmt.Sprintf("package%v", id)
+
+			idx, _, err := cache.ReadIndex(dbName)
+			if err != nil {
+				return err
+			}
+
+			if idx == nil {
+				idx = client.DBIndex{}
+			}
+
+			// sanity checking
+			if rt, ok := idx[p]; ok && rt.Before(start) {
+				return fmt.Errorf("unexpected past time in index: %v before start %v", rt, start)
+			}
+
+			now := time.Now()
+			idx[p] = now
+			if err := cache.WriteIndex(dbName, idx, now); err != nil {
+				return err
+			}
+			return nil
+		})
+	}
+
+	if err := g.Wait(); err != nil {
+		t.Errorf("error in parallel cache index read/write: %v", err)
+	}
+}
diff --git a/gopls/internal/govulncheck/copy.sh b/gopls/internal/govulncheck/copy.sh
new file mode 100755
index 0000000..24ed45b
--- /dev/null
+++ b/gopls/internal/govulncheck/copy.sh
@@ -0,0 +1,13 @@
+#!/bin/bash -eu
+
+# Copyright 2020 The Go Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+set -o pipefail
+
+# Copy golang.org/x/vuln/cmd/govulncheck/internal/govulncheck into this directory.
+# Assume the x/vuln repo is a sibling of the tools repo.
+
+rm -f *.go
+cp ../../../../vuln/cmd/govulncheck/internal/govulncheck/*.go .
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
index 3fd9d03..459ecca 100644
--- a/gopls/internal/vulncheck/command.go
+++ b/gopls/internal/vulncheck/command.go
@@ -14,6 +14,7 @@
 	"strings"
 
 	"golang.org/x/tools/go/packages"
+	gvc "golang.org/x/tools/gopls/internal/govulncheck"
 	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/vuln/client"
 	"golang.org/x/vuln/vulncheck"
@@ -28,7 +29,7 @@
 		args.Pattern = "."
 	}
 
-	dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: defaultCache()})
+	dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: gvc.DefaultCache()})
 	if err != nil {
 		return res, err
 	}