cmd/govulncheck: fix version output for stdlib

When writing the version of a package path in the standard library,
use the Go tag (e.g. go1.16), not the semantic version (e.g. v1.16.0).

Fixes #53948.

Change-Id: Iee00b5b48005da2150239e456dd007731c151e61
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/418455
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 043e78b..474ac63 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -230,11 +230,11 @@
 		fmt.Println()
 		found := v0.PkgPath
 		if v := ci.ModuleVersions[v0.ModPath]; v != "" {
-			found += "@" + v
+			found = packageVersionString(v0.PkgPath, v[1:])
 		}
 		fmt.Printf("Found in:  %v\n", found)
 		if fixed := govulncheck.LatestFixed(v0.OSV.Affected); fixed != "" {
-			fmt.Printf("Fixed in:  %s@v%s\n", v0.PkgPath, fixed)
+			fmt.Printf("Fixed in:  %s\n", packageVersionString(v0.PkgPath, fixed))
 		}
 		fmt.Printf("More info: https://pkg.go.dev/vuln/%s\n", v0.OSV.ID)
 		fmt.Println()
@@ -345,6 +345,14 @@
 	return string(bytes.TrimSpace(out))
 }
 
+func packageVersionString(packagePath, version string) string {
+	v := "v" + version
+	if importPathInStdlib(packagePath) {
+		v = semverToGoTag(v)
+	}
+	return fmt.Sprintf("%s@%s", packagePath, v)
+}
+
 func die(format string, args ...interface{}) {
 	fmt.Fprintf(os.Stderr, format+"\n", args...)
 	os.Exit(1)
diff --git a/cmd/govulncheck/stdlib.go b/cmd/govulncheck/stdlib.go
new file mode 100644
index 0000000..a7b5be8
--- /dev/null
+++ b/cmd/govulncheck/stdlib.go
@@ -0,0 +1,80 @@
+// Copyright 2022 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.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"golang.org/x/mod/semver"
+)
+
+// Support functions for standard library packages.
+// These are copied from the internal/stdlib package in the pkgsite repo.
+
+// semverToGoTag returns the Go standard library repository tag corresponding
+// to semver, a version string without the initial "v".
+// Go tags differ from standard semantic versions in a few ways,
+// such as beginning with "go" instead of "v".
+func semverToGoTag(v string) string {
+	if strings.HasPrefix(v, "v0.0.0") {
+		return "master"
+	}
+	// Special case: v1.0.0 => go1.
+	if v == "v1.0.0" {
+		return "go1"
+	}
+	if !semver.IsValid(v) {
+		return fmt.Sprintf("<!%s:invalid semver>", v)
+	}
+	goVersion := semver.Canonical(v)
+	prerelease := semver.Prerelease(goVersion)
+	versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease)
+	patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".")
+	if patch == "0" {
+		versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0")
+	}
+	goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v"))
+	if prerelease != "" {
+		// Go prereleases look like  "beta1" instead of "beta.1".
+		// "beta1" is bad for sorting (since beta10 comes before beta9), so
+		// require the dot form.
+		i := finalDigitsIndex(prerelease)
+		if i >= 1 {
+			if prerelease[i-1] != '.' {
+				return fmt.Sprintf("<!%s:final digits in a prerelease must follow a period>", v)
+			}
+			// Remove the dot.
+			prerelease = prerelease[:i-1] + prerelease[i:]
+		}
+		goVersion += strings.TrimPrefix(prerelease, "-")
+	}
+	return goVersion
+}
+
+// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s.
+// If s doesn't end in digits, it returns -1.
+func finalDigitsIndex(s string) int {
+	// Assume ASCII (since the semver package does anyway).
+	var i int
+	for i = len(s) - 1; i >= 0; i-- {
+		if s[i] < '0' || s[i] > '9' {
+			break
+		}
+	}
+	if i == len(s)-1 {
+		return -1
+	}
+	return i + 1
+}
+
+// importPathInStdlib reports whether the given import path could be part of the Go standard library,
+// by reporting whether the first component lacks a '.'.
+func importPathInStdlib(path string) bool {
+	if i := strings.IndexByte(path, '/'); i != -1 {
+		path = path[:i]
+	}
+	return !strings.Contains(path, ".")
+}
diff --git a/cmd/govulncheck/testdata/stdlib.ct b/cmd/govulncheck/testdata/stdlib.ct
index 743a4c6..2657350 100644
--- a/cmd/govulncheck/testdata/stdlib.ct
+++ b/cmd/govulncheck/testdata/stdlib.ct
@@ -13,5 +13,5 @@
 Call stacks in your code:
  golang.org/stdvuln.main calls archive/zip.OpenReader
 
-Found in:  archive/zip@v1.18.0
+Found in:  archive/zip@go1.18
 More info: https://pkg.go.dev/vuln/STD