Skip to content

Commit

Permalink
⭐ epoch support in version
Browse files Browse the repository at this point in the history
Add support for epochs in MQL's `semver` type.

```mql
> semver("1:1.2.3") > semver("2009.1.2")
true
```

This works for both deb/rpm-style epochs ("NN:version") and python-style
epochs ([PEP440](https://peps.python.org/pep-0440/)).

Additionally you can access the epoch:

```mql
> semver("5:1.2.3").epoch
5

> semver("3!2.1").epoch
3
```

You can compare versions with and without epochs as well:

```mql
> semver("1:1.0") > semver("2009.12.03")
true
```

Note: Technically epochs aren't semver, so the name `semver` is hitting
its limit here. We could introduce the `version` type as a replacement
or as a temporary holdover (because we also don't want to clutter the
global namespace too much).

Internal note: This required a new wrapper around the version, which can
now be extended with more metadata. Currently we have the epoch stored.

Signed-off-by: Dominik Richter <[email protected]>
  • Loading branch information
arlimus committed Jan 21, 2025
1 parent e165637 commit bb28b92
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 52 deletions.
1 change: 1 addition & 0 deletions llx/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ func init() {
string(">" + types.String): {f: semverGTsemver, Label: ">"},
string("<=" + types.String): {f: semverLTEsemver, Label: "<="},
string(">=" + types.String): {f: semverGTEsemver, Label: ">="},
"epoch": {f: semverEpoch},
},
types.ArrayLike: {
"[]": {f: arrayGetIndexV2},
Expand Down
117 changes: 87 additions & 30 deletions llx/builtin_semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,104 @@
package llx

import (
"regexp"
"strconv"

"github.com/Masterminds/semver"
"go.mondoo.com/cnquery/v11/types"
)

func semverLT(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
// Version type, an abstract representation of a software version.
// It is designed to parse and compare version strings.
// It is built on semver and adds support for epochs (deb, rpm, python).
type Version struct {
*semver.Version
src string
epoch int
}

var reEpoch = regexp.MustCompile("^[0-9]+[:!]")

func NewVersion(s string) Version {
epoch := 0

x, err := semver.NewVersion(s)
if err != nil {
return BoolData(left.(string) < right.(string))
x, epoch = parseEpoch(s)
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) < right.(string))

return Version{
Version: x,
src: s,
epoch: epoch,
}
return BoolData(leftv.LessThan(rightv))
}

func semverGT(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
func parseEpoch(v string) (*semver.Version, int) {
prefix := reEpoch.FindString(v)
if prefix == "" {
return nil, 0
}

remainder := v[len(prefix):]
epochStr := v[:len(prefix)-1]
res, err := semver.NewVersion(remainder)
if err != nil {
return BoolData(left.(string) > right.(string))
return nil, 0
}
rightv, err := semver.NewVersion(right.(string))

// invalid epoch means we discard the entire version string
epoch, err := strconv.Atoi(epochStr)
if err != nil {
return BoolData(left.(string) > right.(string))
return nil, 0
}
return BoolData(leftv.GreaterThan(rightv))

return res, epoch
}

func semverLTE(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
if err != nil {
return BoolData(left.(string) <= right.(string))
// Compare compares this version to another one. It returns -1, 0, or 1 if
// the version smaller, equal, or larger than the other version.
//
// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is
// lower than the version without a prerelease.
func (v Version) Compare(o Version) int {
if v.epoch != o.epoch {
return v.epoch - o.epoch
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) <= right.(string))
if v.Version == nil || o.Version == nil {
if v.src < o.src {
return -1
} else if v.src > o.src {
return 1
}
return 0
}
return BoolData(!leftv.GreaterThan(rightv))

return v.Version.Compare(o.Version)
}

func semverLT(left interface{}, right interface{}) *RawData {
l := NewVersion(left.(string))
r := NewVersion(right.(string))
return BoolData(l.Compare(r) < 0)
}

func semverGT(left interface{}, right interface{}) *RawData {
l := NewVersion(left.(string))
r := NewVersion(right.(string))
return BoolData(l.Compare(r) > 0)
}

func semverLTE(left interface{}, right interface{}) *RawData {
l := NewVersion(left.(string))
r := NewVersion(right.(string))
return BoolData(l.Compare(r) <= 0)
}

func semverGTE(left interface{}, right interface{}) *RawData {
leftv, err := semver.NewVersion(left.(string))
if err != nil {
return BoolData(left.(string) >= right.(string))
}
rightv, err := semver.NewVersion(right.(string))
if err != nil {
return BoolData(left.(string) >= right.(string))
}
return BoolData(!leftv.LessThan(rightv))
l := NewVersion(left.(string))
r := NewVersion(right.(string))
return BoolData(l.Compare(r) >= 0)
}

func semverCmpSemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
Expand Down Expand Up @@ -83,3 +131,12 @@ func semverLTEsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64)
func semverGTEsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverGTE)
}

func semverEpoch(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
if bind.Value == nil {
return &RawData{Type: types.Int, Error: bind.Error}, 0, nil
}

v := NewVersion(bind.Value.(string))
return IntData(v.epoch), 0, nil
}
3 changes: 3 additions & 0 deletions mqlc/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ func init() {
"keys": {typ: stringArrayType, signature: FunctionSignature{}},
"values": {typ: dictArrayType, signature: FunctionSignature{}},
},
types.Semver: {
"epoch": {typ: intType, signature: FunctionSignature{}},
},
types.ArrayLike: {
"[]": {typ: childType, signature: FunctionSignature{Required: 1, Args: []types.Type{types.Int}}},
"first": {typ: childType, signature: FunctionSignature{}},
Expand Down
2 changes: 1 addition & 1 deletion mqlc/typemaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ func compileTypeConversion(llxID string, typ types.Type) fieldCompiler {
},
})

return types.String, nil
return typ, nil
}
}
153 changes: 132 additions & 21 deletions providers/core/resources/mql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,27 +864,138 @@ func TestTime(t *testing.T) {
}

func TestSemver(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1.2.3') == semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.2.3') == semver('1.2')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('1.2') < semver('1.10.2')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.10') >= semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.10') >= '1.2'",
ResultIndex: 2, Expectation: true,
},
t.Run("regular semver", func(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1.2.3') == semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.2.3') == semver('1.2')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('1.2') < semver('1.10.2')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.10') >= semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('1.10') >= '1.2'",
ResultIndex: 2, Expectation: true,
},
})
})

t.Run("one-sided epoch", func(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1.2.3') == semver('1:1.2.3')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('2:1.2.3') == semver('1.2.3')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('3:1.2') < semver('1.10.2')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('1.10') >= semver('4:1.2.3')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('1.2') <= semver('3:1.10.2')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('4:1.10') > semver('1.2.3')",
ResultIndex: 2, Expectation: true,
},
})
})

t.Run("deb/rpm epochs", func(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1.2.3').epoch",
ResultIndex: 0, Expectation: int64(0),
},
{
Code: "semver('7:1.2.3').epoch",
ResultIndex: 0, Expectation: int64(7),
},
})
})

t.Run("python epochs", func(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1.2.3').epoch",
ResultIndex: 0, Expectation: int64(0),
},
{
Code: "semver('5!1.2.3').epoch",
ResultIndex: 0, Expectation: int64(5),
},
})
})

t.Run("different epochs", func(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('2:1.2.3') == semver('1:1.2.3')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('2:1.2.3') == semver('3:1.2.3')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('3:1.2') < semver('1:1.10.2')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('2:1.10') >= semver('4:1.2.3')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('2:1.2') <= semver('3:1.0.2')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('4:1.1') > semver('1:1.2.3')",
ResultIndex: 2, Expectation: true,
},
})
})

t.Run("semver with equal epochs", func(t *testing.T) {
x.TestSimple(t, []testutils.SimpleTest{
{
Code: "semver('1:1.2.3') == semver('1:1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('2:1.2.3') == semver('2:1.2')",
ResultIndex: 2, Expectation: false,
},
{
Code: "semver('3:1.2') < semver('3:1.10.2')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('4:1.10') >= semver('4:1.2.3')",
ResultIndex: 2, Expectation: true,
},
{
Code: "semver('5:1.10') >= '5:1.2'",
ResultIndex: 2, Expectation: true,
},
})
})
}

Expand Down

0 comments on commit bb28b92

Please sign in to comment.