internal/proxy: add ModuleExistsAtTaggedVersion and refactor proxy package
Add a function, ModuleExistsAtTaggedVersion that will be used to check
if "+incompatible" versions exist.
Change-Id: I446454722cc046c1ac0a0824040e6caa16397570
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/528235
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index a04bf3c..d4d46ef 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -14,10 +14,12 @@
"net/http"
"os"
urlpath "path"
+ "regexp"
"sort"
"strings"
"sync"
+ "golang.org/x/exp/slices"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/vulndb/internal/derrors"
@@ -83,6 +85,40 @@
return b, nil
}
+func (c *Client) list(path string) ([]byte, error) {
+ escaped, err := module.EscapePath(path)
+ if err != nil {
+ return nil, err
+ }
+ return c.lookup(fmt.Sprintf("%s/@v/list", escaped))
+}
+
+func (c *Client) latest(path string) ([]byte, error) {
+ escaped, err := module.EscapePath(path)
+ if err != nil {
+ return nil, err
+ }
+ return c.lookup(fmt.Sprintf("%s/@latest", escaped))
+}
+
+func (c *Client) info(path string, ver string) ([]byte, error) {
+ ep, ev, err := escapePathAndVersion(path, ver)
+ if err != nil {
+ return nil, err
+ }
+ return c.lookup(fmt.Sprintf("%s/@v/%v.info", ep, ev))
+}
+
+func (c *Client) mod(path string, ver string) ([]byte, error) {
+ ep, ev, err := escapePathAndVersion(path, ver)
+ if err != nil {
+ return nil, err
+ }
+ return c.lookup(fmt.Sprintf("%s/@v/%v.mod", ep, ev))
+}
+
+var commitHashRegex = regexp.MustCompile(`^[a-f0-9]+$`)
+
func escapePathAndVersion(path, ver string) (ePath, eVersion string, err error) {
ePath, err = module.EscapePath(path)
if err != nil {
@@ -102,11 +138,7 @@
}
func (c *Client) CanonicalModulePath(path, version string) (_ string, err error) {
- ep, ev, err := escapePathAndVersion(path, version)
- if err != nil {
- return "", err
- }
- b, err := c.lookup(fmt.Sprintf("%s/@v/%s.mod", ep, ev))
+ b, err := c.mod(path, version)
if err != nil {
return "", err
}
@@ -115,19 +147,30 @@
return "", err
}
if m.Module == nil {
- return "", fmt.Errorf("unable to retrieve module information for %s, %s", path, string(b))
+ return "", fmt.Errorf("unable to retrieve module information for %s", path)
}
return m.Module.Mod.Path, nil
}
+// ModuleExistsAtTaggedVersion returns whether the given module path exists
+// at the given version.
+// The module need not be canonical, but the version must be an unprefixed
+// canonical tagged version (e.g. 1.2.3 or 1.2.3+incompatible).
+func (c *Client) ModuleExistsAtTaggedVersion(path, version string) bool {
+ // Use this strategy to take advantage of caching.
+ // Some reports would cause this function to be called for many versions
+ // on the same module.
+ vs, err := c.versions(path)
+ if err != nil {
+ return false
+ }
+ return slices.Contains(vs, version)
+}
+
// CanonicalModuleVersion returns the canonical version string (with no leading "v" prefix)
// for the given module path and version string.
func (c *Client) CanonicalModuleVersion(path, ver string) (_ string, err error) {
- ep, ev, err := escapePathAndVersion(path, ver)
- if err != nil {
- return "", err
- }
- b, err := c.lookup(fmt.Sprintf("%s/@v/%v.info", ep, ev))
+ b, err := c.info(path, ver)
if err != nil {
return "", err
}
@@ -145,11 +188,7 @@
// Latest returns the latest version of the module, with no leading "v"
// prefix.
func (c *Client) Latest(path string) (string, error) {
- escaped, err := module.EscapePath(path)
- if err != nil {
- return "", err
- }
- b, err := c.lookup(fmt.Sprintf("%s/@latest", escaped))
+ b, err := c.latest(path)
if err != nil {
return "", err
}
@@ -167,11 +206,19 @@
// Versions returns a list of module versions (with no leading "v" prefix),
// sorted in ascending order.
func (c *Client) Versions(path string) ([]string, error) {
- escaped, err := module.EscapePath(path)
+ vs, err := c.versions(path)
if err != nil {
return nil, err
}
- b, err := c.lookup(fmt.Sprintf("%s/@v/list", escaped))
+ sort.SliceStable(vs, func(i, j int) bool {
+ return version.Before(vs[i], vs[j])
+ })
+ return vs, nil
+}
+
+// versions returns an unsorted list of module versions (with no leading "v" prefix).
+func (c *Client) versions(path string) ([]string, error) {
+ b, err := c.list(path)
if err != nil {
return nil, err
}
@@ -182,9 +229,6 @@
for _, v := range strings.Split(strings.TrimSpace(string(b)), "\n") {
vs = append(vs, version.TrimPrefix(v))
}
- sort.SliceStable(vs, func(i, j int) bool {
- return version.Before(vs[i], vs[j])
- })
return vs, nil
}
@@ -195,35 +239,18 @@
func (c *Client) FindModule(path string) (modPath string, err error) {
derrors.Wrap(&err, "FindModule(%s)", path)
- escaped, err := module.EscapePath(path)
- if err != nil {
- return "", err
- }
-
- for candidate := escaped; candidate != "."; candidate = urlpath.Dir(candidate) {
- if c.moduleExists(candidate) {
- unescaped, err := module.UnescapePath(candidate)
- if err != nil {
- return "", err
- }
- return unescaped, nil
+ for candidate := path; candidate != "."; candidate = urlpath.Dir(candidate) {
+ if c.ModuleExists(candidate) {
+ return candidate, nil
}
}
return "", errNoModuleFound
}
-// ModuleExists returns true if modPath is a recognized module.
-func (c *Client) ModuleExists(modPath string) bool {
- escaped, err := module.EscapePath(modPath)
- if err != nil {
- return false
- }
- return c.moduleExists(escaped)
-}
-
-func (c *Client) moduleExists(escaped string) bool {
- _, err := c.lookup(fmt.Sprintf("%s/@v/list", escaped))
+// ModuleExists returns true if path is a recognized module.
+func (c *Client) ModuleExists(path string) bool {
+ _, err := c.list(path)
return err == nil
}
diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go
index c4e0e6a..7414ce7 100644
--- a/internal/proxy/proxy_test.go
+++ b/internal/proxy/proxy_test.go
@@ -99,6 +99,59 @@
}
}
+func TestModuleExistsAtTaggedVersion(t *testing.T) {
+ c, err := NewTestClient(t, *realProxy)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tcs := []struct {
+ name string
+ path string
+ version string
+ want bool
+ }{
+ {
+ name: "exists",
+ path: "golang.org/x/vuln",
+ version: "0.1.0",
+ want: true,
+ },
+ {
+ name: "non-canonical module ok",
+ path: "github.com/golang/vuln",
+ version: "0.1.0",
+ want: true,
+ },
+ {
+ name: "non-canonical version not OK",
+ path: "golang.org/x/vulndb",
+ version: "0cbf4ffdb4e70fce663ec8d59198745b04e7801b",
+ want: false,
+ },
+ {
+ name: "module exists, version does not",
+ path: "golang.org/x/vulndb",
+ version: "1.0.0",
+ want: false,
+ },
+ {
+ name: "neither exist",
+ path: "golang.org/x/notamod",
+ version: "1.0.0",
+ want: false,
+ },
+ }
+
+ for _, tc := range tcs {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := c.ModuleExistsAtTaggedVersion(tc.path, tc.version); got != tc.want {
+ t.Errorf("ModuleExistsAtTaggedVersion() = %v, want %v", got, tc.want)
+ }
+ })
+ }
+}
+
func TestVersions(t *testing.T) {
c, err := NewTestClient(t, *realProxy)
if err != nil {
diff --git a/internal/proxy/testdata/proxy/TestModuleExistsAtTaggedVersion.json b/internal/proxy/testdata/proxy/TestModuleExistsAtTaggedVersion.json
new file mode 100644
index 0000000..32a6da7
--- /dev/null
+++ b/internal/proxy/testdata/proxy/TestModuleExistsAtTaggedVersion.json
@@ -0,0 +1,16 @@
+{
+ "github.com/golang/vuln/@v/list": {
+ "body": "v1.0.0\nv0.1.0\nv1.0.1\nv0.2.0\n",
+ "status_code": 200
+ },
+ "golang.org/x/notamod/@v/list": {
+ "status_code": 404
+ },
+ "golang.org/x/vuln/@v/list": {
+ "body": "v1.0.0\nv0.1.0\nv1.0.1\nv0.2.0\n",
+ "status_code": 200
+ },
+ "golang.org/x/vulndb/@v/list": {
+ "status_code": 200
+ }
+}
\ No newline at end of file