gosumcheck: basic go.sum checker

Copied from golang.org/x/exp/sumdb/gosumcheck.

For golang/go#31761.

Change-Id: Ia2cf81372b8e2da9b879f3d737de014eb30ea34b
Reviewed-on: https://go-review.googlesource.com/c/mod/+/176639
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/go.mod b/go.mod
index ba216e7..a5fc715 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@
 
 require golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
 
-go 1.13
+go 1.12
diff --git a/gosumcheck/main.go b/gosumcheck/main.go
new file mode 100644
index 0000000..1e92f1f
--- /dev/null
+++ b/gosumcheck/main.go
@@ -0,0 +1,213 @@
+// Copyright 2019 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.
+
+// Gosumcheck checks a go.sum file against a go.sum database server.
+//
+// Usage:
+//
+//	gosumcheck [-h H] [-k key] [-u url] [-v] go.sum
+//
+// The -h flag changes the tile height (default 8).
+//
+// The -k flag changes the go.sum database server key.
+//
+// The -u flag overrides the URL of the server (usually set from the key name).
+//
+// The -v flag enables verbose output.
+// In particular, it causes gosumcheck to report
+// the URL and elapsed time for each server request.
+//
+// WARNING! WARNING! WARNING!
+//
+// Gosumcheck is meant as a proof of concept demo and should not be
+// used in production scripts or continuous integration testing.
+// It does not cache any downloaded information from run to run,
+// making it expensive and also keeping it from detecting server
+// misbehavior or successful HTTPS man-in-the-middle timeline forks.
+//
+// To discourage misuse in automated settings, gosumcheck does not
+// set any exit status to report whether any problems were found.
+//
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"strings"
+	"sync"
+	"time"
+
+	"golang.org/x/mod/sumdb"
+)
+
+func usage() {
+	fmt.Fprintf(os.Stderr, "usage: gosumcheck [-h H] [-k key] [-u url] [-v] go.sum...\n")
+	os.Exit(2)
+}
+
+var (
+	height = flag.Int("h", 8, "tile height")
+	vkey   = flag.String("k", "sum.golang.org+033de0ae+Ac4zctda0e5eza+HJyk9SxEdh+s3Ux18htTTAD8OuAn8", "key")
+	url    = flag.String("u", "", "url to server (overriding name)")
+	vflag  = flag.Bool("v", false, "enable verbose output")
+)
+
+func main() {
+	log.SetPrefix("notecheck: ")
+	log.SetFlags(0)
+
+	flag.Usage = usage
+	flag.Parse()
+	if flag.NArg() < 1 {
+		usage()
+	}
+
+	client := sumdb.NewClient(new(clientOps))
+
+	// Look in environment explicitly, so that if 'go env' is old and
+	// doesn't know about GONOSUMDB, we at least get anything
+	// set in the environment.
+	env := os.Getenv("GONOSUMDB")
+	if env == "" {
+		out, err := exec.Command("go", "env", "GONOSUMDB").CombinedOutput()
+		if err != nil {
+			log.Fatalf("go env GONOSUMDB: %v\n%s", err, out)
+		}
+		env = strings.TrimSpace(string(out))
+	}
+	client.SetGONOSUMDB(env)
+
+	for _, arg := range flag.Args() {
+		data, err := ioutil.ReadFile(arg)
+		if err != nil {
+			log.Fatal(err)
+		}
+		checkGoSum(client, arg, data)
+	}
+}
+
+func checkGoSum(client *sumdb.Client, name string, data []byte) {
+	lines := strings.Split(string(data), "\n")
+	if lines[len(lines)-1] != "" {
+		log.Printf("error: final line missing newline")
+		return
+	}
+	lines = lines[:len(lines)-1]
+
+	errs := make([]string, len(lines))
+	var wg sync.WaitGroup
+	for i, line := range lines {
+		wg.Add(1)
+		go func(i int, line string) {
+			defer wg.Done()
+			f := strings.Fields(line)
+			if len(f) != 3 {
+				errs[i] = "invalid number of fields"
+				return
+			}
+
+			dbLines, err := client.Lookup(f[0], f[1])
+			if err != nil {
+				if err == sumdb.ErrGONOSUMDB {
+					errs[i] = fmt.Sprintf("%s@%s: %v", f[0], f[1], err)
+				} else {
+					// Otherwise Lookup properly adds the prefix itself.
+					errs[i] = err.Error()
+				}
+				return
+			}
+			hashAlgPrefix := f[0] + " " + f[1] + " " + f[2][:strings.Index(f[2], ":")+1]
+			for _, dbLine := range dbLines {
+				if dbLine == line {
+					return
+				}
+				if strings.HasPrefix(dbLine, hashAlgPrefix) {
+					errs[i] = fmt.Sprintf("%s@%s hash mismatch: have %s, want %s", f[0], f[1], line, dbLine)
+					return
+				}
+			}
+			errs[i] = fmt.Sprintf("%s@%s hash algorithm mismatch: have %s, want one of:\n\t%s", f[0], f[1], line, strings.Join(dbLines, "\n\t"))
+		}(i, line)
+	}
+	wg.Wait()
+
+	for i, err := range errs {
+		if err != "" {
+			fmt.Printf("%s:%d: %s\n", name, i+1, err)
+		}
+	}
+}
+
+type clientOps struct{}
+
+func (*clientOps) ReadConfig(file string) ([]byte, error) {
+	if file == "key" {
+		return []byte(*vkey), nil
+	}
+	if strings.HasSuffix(file, "/latest") {
+		// Looking for cached latest tree head.
+		// Empty result means empty tree.
+		return []byte{}, nil
+	}
+	return nil, fmt.Errorf("unknown config %s", file)
+}
+
+func (*clientOps) WriteConfig(file string, old, new []byte) error {
+	// Ignore writes.
+	return nil
+}
+
+func (*clientOps) ReadCache(file string) ([]byte, error) {
+	return nil, fmt.Errorf("no cache")
+}
+
+func (*clientOps) WriteCache(file string, data []byte) {
+	// Ignore writes.
+}
+
+func (*clientOps) Log(msg string) {
+	log.Print(msg)
+}
+
+func (*clientOps) SecurityError(msg string) {
+	log.Fatal(msg)
+}
+
+func init() {
+	http.DefaultClient.Timeout = 1 * time.Minute
+}
+
+func (*clientOps) ReadRemote(path string) ([]byte, error) {
+	name := *vkey
+	if i := strings.Index(name, "+"); i >= 0 {
+		name = name[:i]
+	}
+	start := time.Now()
+	target := "https://" + name + path
+	if *url != "" {
+		target = *url + path
+	}
+	resp, err := http.Get(target)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf("GET %v: %v", target, resp.Status)
+	}
+	data, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
+	if err != nil {
+		return nil, err
+	}
+	if *vflag {
+		fmt.Fprintf(os.Stderr, "%.3fs %s\n", time.Since(start).Seconds(), target)
+	}
+	return data, nil
+}
diff --git a/gosumcheck/test.bash b/gosumcheck/test.bash
new file mode 100755
index 0000000..2a4b2af
--- /dev/null
+++ b/gosumcheck/test.bash
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+set -e
+go build -o gosumcheck.exe
+export GONOSUMDB=*/text # rsc.io/text but not golang.org/x/text
+./gosumcheck.exe "$@" -v test.sum
+rm -f ./gosumcheck.exe
+echo PASS
diff --git a/gosumcheck/test.sum b/gosumcheck/test.sum
new file mode 100644
index 0000000..1b252ff
--- /dev/null
+++ b/gosumcheck/test.sum
@@ -0,0 +1,6 @@
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/text v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=