internal: add some helper functions to support CVE v5

Adds various helper functions that will be used to add support for the
new CVE v5 schema.

For golang/go#49289

Change-Id: I3e9aaa95e30000c01a3f6b5738950b9dccdd84cc
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/545296
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/cveschema5/cveschema5.go b/internal/cveschema5/cveschema5.go
index d251a48..815cc5c 100644
--- a/internal/cveschema5/cveschema5.go
+++ b/internal/cveschema5/cveschema5.go
@@ -135,10 +135,17 @@
 	return record.Metadata.ID, &record.Containers, nil
 }
 
-const Regex = `CVE-\d{4}-\d{4,}`
+const RegexStr = `CVE-\d{4}-\d{4,}`
 
-var cveRegex = regexp.MustCompile(`^` + Regex + `$`)
+var (
+	Regex       = regexp.MustCompile(RegexStr)
+	RegexStrict = regexp.MustCompile(`^` + RegexStr + `$`)
+)
 
 func IsCVE(s string) bool {
-	return cveRegex.MatchString(s)
+	return RegexStrict.MatchString(s)
+}
+
+func FindCVE(s string) string {
+	return Regex.FindString(s)
 }
diff --git a/internal/cveschema5/cveschema5_test.go b/internal/cveschema5/cveschema5_test.go
index f48ff59..681ca34 100644
--- a/internal/cveschema5/cveschema5_test.go
+++ b/internal/cveschema5/cveschema5_test.go
@@ -72,3 +72,11 @@
 		t.Errorf("Read(%s) = %v\n want %v", f, got, want)
 	}
 }
+
+func TestFindCVE(t *testing.T) {
+	s := "something/CVE-1999-0004.json"
+	got, want := FindCVE(s), "CVE-1999-0004"
+	if got != want {
+		t.Errorf("FindCVE(%s) = %s, want %s", s, got, want)
+	}
+}
diff --git a/internal/gitrepo/gitrepo.go b/internal/gitrepo/gitrepo.go
index 9e9ab33..eb04d41 100644
--- a/internal/gitrepo/gitrepo.go
+++ b/internal/gitrepo/gitrepo.go
@@ -29,10 +29,15 @@
 	ctx = event.Start(ctx, "gitrepo.Clone")
 	defer event.End(ctx)
 
-	log.Infof(ctx, "Cloning repo %q at HEAD", repoURL)
+	return CloneAt(ctx, repoURL, plumbing.HEAD)
+}
+
+// Clone returns a bare repo by cloning the repo at repoURL at the given ref.
+func CloneAt(ctx context.Context, repoURL string, ref plumbing.ReferenceName) (repo *git.Repository, err error) {
+	log.Infof(ctx, "Cloning repo %q at %s", repoURL, ref)
 	return git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
 		URL:           repoURL,
-		ReferenceName: plumbing.HEAD,
+		ReferenceName: ref,
 		SingleBranch:  true,
 		Depth:         1,
 		Tags:          git.NoTags,
diff --git a/internal/report/lint.go b/internal/report/lint.go
index 6086bce..16fccbc 100644
--- a/internal/report/lint.go
+++ b/internal/report/lint.go
@@ -192,9 +192,9 @@
 	issueRegex    = regexp.MustCompile(`https://go.dev/issue/\d+`)
 	announceRegex = regexp.MustCompile(`https://groups.google.com/g/golang-(announce|dev|nuts)/c/([^/]+)`)
 
-	nistRegex     = regexp.MustCompile(`^https://nvd.nist.gov/vuln/detail/(` + cveschema5.Regex + `)$`)
+	nistRegex     = regexp.MustCompile(`^https://nvd.nist.gov/vuln/detail/(` + cveschema5.RegexStr + `)$`)
 	ghsaLinkRegex = regexp.MustCompile(`^https://github.com/.*/(` + ghsa.Regex + `)$`)
-	mitreRegex    = regexp.MustCompile(`^https://cve.mitre.org/.*(` + cveschema5.Regex + `)$`)
+	mitreRegex    = regexp.MustCompile(`^https://cve.mitre.org/.*(` + cveschema5.RegexStr + `)$`)
 )
 
 // Checks that the "links" section of a Report for a package in the
diff --git a/internal/test/txtar.go b/internal/test/txtar.go
new file mode 100644
index 0000000..6397c96
--- /dev/null
+++ b/internal/test/txtar.go
@@ -0,0 +1,44 @@
+// 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 test
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	"golang.org/x/tools/txtar"
+)
+
+func WriteTxtar(filename string, files []txtar.File, comment string) error {
+	if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
+		return err
+	}
+
+	if err := os.WriteFile(filename, txtar.Format(
+		&txtar.Archive{
+			Comment: []byte(addCopyright(comment)),
+			Files:   files,
+		},
+	), os.ModePerm); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func addCopyright(comment string) string {
+	return fmt.Sprintf("%s\n\n%s\n\n", copyright, comment)
+}
+
+var copyright = fmt.Sprintf(`Copyright %d 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.`, currentYear())
+
+func currentYear() int {
+	year, _, _ := time.Now().Date()
+	return year
+}
diff --git a/internal/test/txtar_test.go b/internal/test/txtar_test.go
new file mode 100644
index 0000000..2762a09
--- /dev/null
+++ b/internal/test/txtar_test.go
@@ -0,0 +1,48 @@
+// 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 test
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/tools/txtar"
+)
+
+func TestWriteTxtar(t *testing.T) {
+	tmp := t.TempDir()
+
+	filename := filepath.Join(tmp, "example", "file.txtar")
+	files := []txtar.File{
+		{
+			Name: "a.txt",
+			Data: []byte("abcdefg\n"),
+		},
+		{
+			Name: "b.txt",
+			Data: []byte("hijklmnop\n"),
+		},
+	}
+	comment := "Context about this archive"
+
+	if err := WriteTxtar(filename, files, comment); err != nil {
+		t.Fatal(err)
+	}
+
+	got, err := txtar.ParseFile(filename)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := &txtar.Archive{
+		Comment: []byte(addCopyright(comment)),
+		Files:   files,
+	}
+
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("mismatch (-want, +got):\n%s", diff)
+	}
+}