internal/frontend, internal/vuln: support all valid Go IDs in search

Previously, only Go vuln IDs with 4 digits at the end were accepted
in search, but Go IDs may have any number of digits. This change
fixes this and moves the logic to check Go ID and alias regexps
to the internal/vuln package.

Change-Id: I878ac35e139d999a45fa532da43743f0b9ea85be
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/480516
Run-TryBot: Tatiana Bradley <tatianabradley@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index f6e9c48..ff6ab7e 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -10,7 +10,6 @@
 	"fmt"
 	"net/http"
 	"path"
-	"regexp"
 	"sort"
 	"strings"
 	"sync"
@@ -337,9 +336,6 @@
 	return sr
 }
 
-// A regexp that matches Go vuln IDs.
-var goVulnIDRegexp = regexp.MustCompile("^GO-[0-9]{4}-[0-9]{4}$")
-
 // searchRequestRedirectPath returns the path that a search request should be
 // redirected to, or the empty string if there is no such path.
 //
@@ -355,7 +351,7 @@
 	if urlSchemeIdx > -1 {
 		query = query[urlSchemeIdx+3:]
 	}
-	if vulnSupport && goVulnIDRegexp.MatchString(query) {
+	if vulnSupport && vuln.IsGoID(query) {
 		return fmt.Sprintf("/vuln/%s?q", query)
 	}
 	requestedPath := path.Clean(query)
@@ -405,7 +401,7 @@
 func searchVulnAlias(ctx context.Context, mode, cq string, vc *vuln.Client) (_ *searchAction, err error) {
 	defer derrors.Wrap(&err, "searchVulnAlias(%q, %q)", mode, cq)
 
-	if mode != searchModeVuln || !isVulnAlias(cq) || vc == nil {
+	if mode != searchModeVuln || !vuln.IsAlias(cq) || vc == nil {
 		return nil, nil
 	}
 	aliasEntries, err := vc.ByAlias(ctx, cq)
@@ -430,16 +426,6 @@
 	}
 }
 
-// Regexps that match aliases for Go vuln.
-var (
-	cveRegexp  = regexp.MustCompile("^CVE-[0-9]{4}-[0-9]+$")
-	ghsaRegexp = regexp.MustCompile("^GHSA-.{4}-.{4}-.{4}$")
-)
-
-func isVulnAlias(s string) bool {
-	return cveRegexp.MatchString(s) || ghsaRegexp.MatchString(s)
-}
-
 // searchMode reports whether the search performed should be in package or
 // symbol search mode.
 func searchMode(r *http.Request) string {
@@ -455,7 +441,7 @@
 	case searchModeVuln:
 		return searchModeVuln
 	default:
-		if isVulnAlias(q) {
+		if vuln.IsAlias(q) {
 			return searchModeVuln
 		}
 		if shouldDefaultToSymbolSearch(q) {
diff --git a/internal/frontend/vulns.go b/internal/frontend/vulns.go
index 66d6673..fc558aa 100644
--- a/internal/frontend/vulns.go
+++ b/internal/frontend/vulns.go
@@ -88,7 +88,7 @@
 		s.servePage(r.Context(), w, "vuln/list", vulnListPage)
 	default: // the path should be "/<ID>", e.g. "/GO-2021-0001".
 		id := path[1:]
-		if !goVulnIDRegexp.MatchString(id) {
+		if !vuln.IsGoID(id) {
 			if r.URL.Query().Has("q") {
 				return &serverError{status: derrors.ToStatus(derrors.NotFound)}
 			}
diff --git a/internal/vuln/regexp.go b/internal/vuln/regexp.go
new file mode 100644
index 0000000..418bd65
--- /dev/null
+++ b/internal/vuln/regexp.go
@@ -0,0 +1,24 @@
+// Copyright 2023 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 vuln
+
+import "regexp"
+
+var (
+	goRegexp   = regexp.MustCompile("^GO-[0-9]{4}-[0-9]{4,}$")
+	cveRegexp  = regexp.MustCompile("^CVE-[0-9]{4}-[0-9]+$")
+	ghsaRegexp = regexp.MustCompile("^GHSA-.{4}-.{4}-.{4}$")
+)
+
+// IsGoID returns whether s is a valid Go vulnerability ID.
+func IsGoID(s string) bool {
+	return goRegexp.MatchString(s)
+}
+
+// IsAlias returns whether s is a valid vulnerability alias
+// (CVE or GHSA).
+func IsAlias(s string) bool {
+	return cveRegexp.MatchString(s) || ghsaRegexp.MatchString(s)
+}
diff --git a/internal/vuln/regexp_test.go b/internal/vuln/regexp_test.go
new file mode 100644
index 0000000..ca66b53
--- /dev/null
+++ b/internal/vuln/regexp_test.go
@@ -0,0 +1,71 @@
+// Copyright 2023 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 vuln
+
+import "testing"
+
+func TestIsGoID(t *testing.T) {
+	tests := []struct {
+		id   string
+		want bool
+	}{
+		{
+			id:   "GO-1999-0001",
+			want: true,
+		},
+		{
+			id:   "GO-2023-12345678",
+			want: true,
+		},
+		{
+			id:   "GO-2023-123",
+			want: false,
+		},
+		{
+			id:   "GO-abcd-0001",
+			want: false,
+		},
+		{
+			id:   "CVE-1999-0001",
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.id, func(t *testing.T) {
+			got := IsGoID(tt.id)
+			if got != tt.want {
+				t.Errorf("IsGoID(%s) = %t, want %t", tt.id, got, tt.want)
+			}
+		})
+	}
+}
+
+func TestIsAlias(t *testing.T) {
+	tests := []struct {
+		id   string
+		want bool
+	}{
+		{
+			id:   "GO-1999-0001",
+			want: false,
+		},
+		{
+			id:   "GHSA-abcd-1234-efgh",
+			want: true,
+		},
+		{
+			id:   "CVE-1999-000123",
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.id, func(t *testing.T) {
+			got := IsAlias(tt.id)
+			if got != tt.want {
+				t.Errorf("IsAlias(%s) = %t, want %t", tt.id, got, tt.want)
+			}
+		})
+	}
+}