Skip to content

Commit

Permalink
add jvm version comparison
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman committed Sep 13, 2024
1 parent 9fb2194 commit 19a7170
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 14 deletions.
2 changes: 2 additions & 0 deletions grype/version/constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func GetConstraint(constStr string, format Format) (Constraint, error) {
return newKBConstraint(constStr)
case PortageFormat:
return newPortageConstraint(constStr)
case JVMFormat:
return newJvmConstraint(constStr)
case UnknownFormat:
return newFuzzyConstraint(constStr, "unknown")
}
Expand Down
28 changes: 15 additions & 13 deletions grype/version/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
GemFormat
PortageFormat
GolangFormat
JVMFormat
)

type Format int
Expand All @@ -34,6 +35,7 @@ var formatStr = []string{
"Gem",
"Portage",
"Go",
"JVM",
}

var Formats = []Format{
Expand All @@ -46,6 +48,8 @@ var Formats = []Format{
KBFormat,
GemFormat,
PortageFormat,
GolangFormat,
JVMFormat,
}

func ParseFormat(userStr string) Format {
Expand All @@ -70,35 +74,33 @@ func ParseFormat(userStr string) Format {
return GemFormat
case strings.ToLower(PortageFormat.String()), "portage":
return PortageFormat
case strings.ToLower(JVMFormat.String()), "jvm", "jre", "jdk", "openjdk", "jep223":
}
return UnknownFormat
}

func FormatFromPkgType(t pkg.Type) Format {
var format Format
switch t {
case pkg.ApkPkg:
format = ApkFormat
return ApkFormat
case pkg.DebPkg:
format = DebFormat
return DebFormat
case pkg.JavaPkg:
format = MavenFormat
return MavenFormat
case pkg.RpmPkg:
format = RpmFormat
return RpmFormat
case pkg.GemPkg:
format = GemFormat
return GemFormat
case pkg.PythonPkg:
format = PythonFormat
return PythonFormat
case pkg.KbPkg:
format = KBFormat
return KBFormat
case pkg.PortagePkg:
format = PortageFormat
return PortageFormat
case pkg.GoModulePkg:
format = GolangFormat
default:
format = UnknownFormat
return GolangFormat
}
return format
return UnknownFormat
}

func (f Format) String() string {
Expand Down
43 changes: 43 additions & 0 deletions grype/version/jvm_constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package version

import "fmt"

var _ Constraint = (*jvmConstraint)(nil)

type jvmConstraint struct {
raw string
expression constraintExpression
}

func newJvmConstraint(raw string) (jvmConstraint, error) {
constraints, err := newConstraintExpression(raw, newJvmComparator)
if err != nil {
return jvmConstraint{}, err
}
return jvmConstraint{
expression: constraints,
raw: raw,
}, nil
}

func (g jvmConstraint) String() string {
if g.raw == "" {
return "none (jvm)"
}
return fmt.Sprintf("%s (jvm)", g.raw)
}

func (g jvmConstraint) Satisfied(version *Version) (bool, error) {
if g.raw == "" {
return true, nil // the empty constraint is always satisfied
}
return g.expression.satisfied(version)
}

func newJvmComparator(unit constraintUnit) (Comparator, error) {
ver, err := newJvmVersion(unit.version)
if err != nil {
return nil, fmt.Errorf("unable to parse constraint version (%s): %w", unit.version, err)
}
return ver, nil
}
69 changes: 69 additions & 0 deletions grype/version/jvm_constraint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package version

import (
"github.com/stretchr/testify/require"
"testing"
)

func TestVersionConstraintJVM(t *testing.T) {
tests := []testCase{
// pre jep 223 versions
{version: "1.7.0_80", constraint: "< 1.8.0", satisfied: true},
{version: "1.8.0_131", constraint: "> 1.8.0", satisfied: true},
{version: "1.8.0_131", constraint: "< 1.8.0_132", satisfied: true},
{version: "1.8.0_131-b11", constraint: "< 1.8.0_132", satisfied: true},

{version: "1.7.0_80", constraint: "> 1.8.0", satisfied: false},
{version: "1.8.0_131", constraint: "< 1.8.0", satisfied: false},
{version: "1.8.0_131", constraint: "> 1.8.0_132", satisfied: false},
{version: "1.8.0_131-b11", constraint: "> 1.8.0_132", satisfied: false},

{version: "1.7.0_80", constraint: "= 1.8.0", satisfied: false},
{version: "1.8.0_131", constraint: "= 1.8.0", satisfied: false},
{version: "1.8.0_131", constraint: "= 1.8.0_132", satisfied: false},
{version: "1.8.0_131-b11", constraint: "= 1.8.0_132", satisfied: false},

{version: "1.8.0_80", constraint: "= 1.8.0_80", satisfied: true},
{version: "1.8.0_131", constraint: ">= 1.8.0_131", satisfied: true},
{version: "1.8.0_131", constraint: "= 1.8.0_131-b001", satisfied: true}, // builds should not matter
{version: "1.8.0_131-ea-b11", constraint: "= 1.8.0_131-ea", satisfied: true},

// jep 223 versions
{version: "8.0.4", constraint: "> 8.0.3", satisfied: true},
{version: "8.0.4", constraint: "< 8.0.5", satisfied: true},
{version: "9.0.0", constraint: "> 8.0.5", satisfied: true},
{version: "9.0.0", constraint: "< 9.1.0", satisfied: true},
{version: "11.0.4", constraint: "<= 11.0.4", satisfied: true},
{version: "11.0.5", constraint: "> 11.0.4", satisfied: true},

{version: "8.0.4", constraint: "< 8.0.3", satisfied: false},
{version: "8.0.4", constraint: "> 8.0.5", satisfied: false},
{version: "9.0.0", constraint: "< 8.0.5", satisfied: false},
{version: "9.0.0", constraint: "> 9.1.0", satisfied: false},
{version: "11.0.4", constraint: "> 11.0.4", satisfied: false},
{version: "11.0.5", constraint: "< 11.0.4", satisfied: false},

// mixed versions
{version: "1.8.0_131", constraint: "< 9.0.0", satisfied: true}, // 1.8.0_131 -> 8.0.131
{version: "9.0.0", constraint: "> 1.8.0_131", satisfied: true}, // 1.8.0_131 -> 8.0.131
{version: "1.8.0_131", constraint: "<= 8.0.131", satisfied: true},
{version: "1.8.0_131", constraint: "> 7.0.79", satisfied: true},
{version: "1.8.0_131", constraint: "= 8.0.131", satisfied: true},
{version: "1.8.0_131", constraint: ">= 9.0.0", satisfied: false},
{version: "9.0.1", constraint: "< 8.0.131", satisfied: false},

// pre-release versions
{version: "1.8.0_131-ea", constraint: "< 1.8.0_131", satisfied: true},
{version: "1.8.0_131", constraint: "> 1.8.0_131-ea", satisfied: true},
{version: "9.0.0-ea", constraint: "< 9.0.0", satisfied: true},
{version: "9.0.0-ea", constraint: "> 1.8.0_131", satisfied: true},
}

for _, test := range tests {
t.Run(test.version+"_constraint_"+test.constraint, func(t *testing.T) {
constraint, err := newJvmConstraint(test.constraint)
require.NoError(t, err)
test.assertVersionConstraint(t, JVMFormat, constraint)
})
}
}
102 changes: 102 additions & 0 deletions grype/version/jvm_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package version

import (
"fmt"
hashiVer "github.com/anchore/go-version"
"github.com/anchore/grype/internal"
"github.com/anchore/grype/internal/log"
"regexp"
"strings"
)

var _ Comparator = (*jvmVersion)(nil)

var preJep223VersionPattern = regexp.MustCompile(`^1\.(?P<major>\d+)(\.(?P<minor>\d+)(_(?P<patch>\d+))?(-(?P<prerelease>[^b][^-]+))?(-b(?P<build>\d+))?)?`)

type jvmVersion struct {
isPreJep223 bool
semVer *hashiVer.Version
}

func newJvmVersion(raw string) (*jvmVersion, error) {
isPreJep233 := strings.HasPrefix(raw, "1.")

if isPreJep233 {
// convert the pre-JEP 223 version to semver
raw = convertPreJep223Version(raw)
}
verObj, err := hashiVer.NewVersion(raw)
if err != nil {
return nil, fmt.Errorf("unable to create semver obj for JVM version: %w", err)
}

return &jvmVersion{
isPreJep223: isPreJep233,
semVer: verObj,
}, nil
}

func (v *jvmVersion) Compare(other *Version) (int, error) {
if other.Format != JVMFormat {
return -1, fmt.Errorf("unable to compare JVM to given format: %s", other.Format)
}

if other.rich.jvmVersion == nil {
return -1, fmt.Errorf("given empty jvmVersion object")
}

return other.rich.jvmVersion.compare(*v), nil
}

func (v jvmVersion) compare(other jvmVersion) int {
return v.semVer.Compare(other.semVer)
}

func convertPreJep223Version(version string) string {
// convert the following pre JEP 223 version strings to semvers
// 1.8.0_302-b08 --> 8.0.302+8
// 1.9.0-ea-b19 --> 9.0.0-ea+19
// NOTE: this makes an assumption that the old update field is the patch version in semver...
// this is NOT strictly in the spec, but for 1.8 this tends to be true (especially for temurin-based builds)
version = strings.TrimSpace(version)

matches := internal.MatchNamedCaptureGroups(preJep223VersionPattern, version)
if len(matches) == 0 {
log.WithFields("version", version).Trace("unable to convert pre-JEP 223 JVM version")
return version
}

// extract relevant parts from the matches
majorVersion := trim0sFromLeft(matches["major"])
minorVersion := trim0sFromLeft(matches["minor"])
patchVersion := trim0sFromLeft(matches["patch"])
preRelease := trim0sFromLeft(matches["prerelease"])
build := trim0sFromLeft(matches["build"])

if minorVersion == "" {
minorVersion = "0"
}
if patchVersion == "" {
patchVersion = "0"
}

// build the semver string
var semver strings.Builder
semver.WriteString(fmt.Sprintf("%s.%s.%s", majorVersion, minorVersion, patchVersion))

if preRelease != "" {
semver.WriteString(fmt.Sprintf("-%s", preRelease))
}
if build != "" {
semver.WriteString(fmt.Sprintf("+%s", build))
}

return semver.String()
}

func trim0sFromLeft(v string) string {
if v == "0" {
return v
}
return strings.TrimLeft(v, "0")
}
101 changes: 101 additions & 0 deletions grype/version/jvm_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package version

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)

func TestVersionJVM(t *testing.T) {
tests := []struct {
v1 string
v2 string
expected int
}{
// pre jep223 versions
{"1.8", "1.8.0", 0},
{"1.8.0", "1.8.0_0", 0},
{"1.8.0", "1.8.0", 0},
{"1.7.0", "1.8.0", -1},
{"1.8.0_131", "1.8.0_131", 0},
{"1.8.0_131", "1.8.0_132", -1},

// builds should not matter
{"1.8.0_131", "1.8.0_130", 1},
{"1.8.0_131", "1.8.0_132-b11", -1},
{"1.8.0_131-b11", "1.8.0_132-b11", -1},
{"1.8.0_131-b11", "1.8.0_131-b12", 0},
{"1.8.0_131-b11", "1.8.0_131-b10", 0},
{"1.8.0_131-b11", "1.8.0_131", 0},
{"1.8.0_131-b11", "1.8.0_131-b11", 0},

// jep223 versions (semver)
{"8.0.4", "8.0.4", 0},
{"8.0.4", "8.0.5", -1},
{"8.0.4", "8.0.3", 1},
{"8.0.4", "8.0.4+b1", 0},

// mix comparison
{"1.8.0_131", "8.0.4", 1}, // 1.8.0_131 --> 8.0.131
{"8.0.4", "1.8.0_131", -1}, // doesn't matter which side the comparison is on
{"1.8.0_131-b002", "8.0.131+b2", 0}, // builds should not matter
{"1.8.0_131-b002", "8.0.131+b1", 0}, // builds should not matter
{"1.6.0", "8.0.1", -1}, // 1.6.0 --> 6.0.0

// prerelease
{"1.8.0_13-ea-b002", "1.8.0_13-ea-b001", 0},
{"1.8.0_13-ea", "1.8.0_13-ea-b001", 0},
{"1.8.0_13-ea-b002", "8.0.13-ea+b2", 0},
{"1.8.0_13-ea-b002", "8.0.13+b2", -1},
{"1.8.0_13-b002", "8.0.13-ea+b2", 1},

// pre 1.8 (when the jep 223 was introduced)
{"1.7.0", "7.0.0", 0}, // there is no v7 of the JVM, but we want to honor this comparison since it may be someone mistakenly using the wrong version format
}

for _, test := range tests {
name := test.v1 + "_vs_" + test.v2
t.Run(name, func(t *testing.T) {
v1, err := newJvmVersion(test.v1)
require.NotNil(t, v1)
require.NoError(t, err)

v2, err := newJvmVersion(test.v2)
require.NotNil(t, v2)
require.NoError(t, err)

actual := v1.compare(*v2)
assert.Equal(t, test.expected, actual)
})
}
}

func TestVersionJVM_invalid(t *testing.T) {
tests := []struct {
name string
version string
wantErr require.ErrorAssertionFunc
}{
{
name: "outside of pre jep223 major version range",
version: "2.8.0_131-b11",
wantErr: require.Error,
},
{
name: "invalid version",
version: "1.a",
wantErr: require.Error,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
v, err := newJvmVersion(tt.version)
assert.Nil(t, v)
tt.wantErr(t, err)
})
}
}
Loading

0 comments on commit 19a7170

Please sign in to comment.