Skip to content

Commit

Permalink
⭐ epoch support in version (#5106)
Browse files Browse the repository at this point in the history
* ⭐ epoch support in version

Add support for epochs in MQL's `semver` type.

```mql
> semver("1:1.2.3") < semver("2:1.0.0")
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]>

* 🧹 soft-deprecate semver in favor of version

The soft part of this deprecation is that we don't show a deprecation
warning anywhere at all. So semver is still technically included, since
we released it in v9.x, but we will deprecate it formally with the next
major release.

---------

Signed-off-by: Dominik Richter <[email protected]>
  • Loading branch information
arlimus authored Feb 11, 2025
1 parent be323a1 commit d48ae6e
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 169 deletions.
2 changes: 1 addition & 1 deletion cli/printer/mql.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ func (print *Printer) Data(typ types.Type, data interface{}, codeID string, bund
case types.Block:
return print.refMap(typ, data.(map[string]interface{}), codeID, bundle, indent)

case types.Semver:
case types.Version:
return print.Secondary(data.(string))

case types.ArrayLike:
Expand Down
59 changes: 30 additions & 29 deletions llx/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ func init() {
string("!=" + types.Float): {f: stringNotFloatV2, Label: "!="},
string("==" + types.Dict): {f: stringCmpDictV2, Label: "=="},
string("!=" + types.Dict): {f: stringNotDictV2, Label: "!="},
string("==" + types.Semver): {f: semverCmpSemver, Label: "=="},
string("!=" + types.Semver): {f: semverNotSemver, Label: "!="},
string("==" + types.Version): {f: versionCmpVersion, Label: "=="},
string("!=" + types.Version): {f: versionNotVersion, Label: "!="},
string("==" + types.ArrayLike): {f: chunkEqFalseV2, Label: "=="},
string("!=" + types.ArrayLike): {f: chunkNeqTrueV2, Label: "!="},
string("==" + types.Array(types.String)): {f: stringCmpStringarrayV2, Label: "=="},
Expand All @@ -295,10 +295,10 @@ func init() {
string("<=" + types.Dict): {f: stringLTEDictV2, Label: "<="},
string(">" + types.Dict): {f: stringGTDictV2, Label: ">"},
string(">=" + types.Dict): {f: stringGTEDictV2, Label: ">="},
string("<" + types.Semver): {f: semverLTsemver, Label: "<"},
string(">" + types.Semver): {f: semverGTsemver, Label: ">"},
string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="},
string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="},
string("<" + types.Version): {f: versionLTversion, Label: "<"},
string(">" + types.Version): {f: versionGTversion, Label: ">"},
string("<=" + types.Version): {f: versionLTEversion, Label: "<="},
string(">=" + types.Version): {f: versionGTEversion, Label: ">="},
string("&&" + types.Bool): {f: stringAndBoolV2, Label: "&&"},
string("||" + types.Bool): {f: stringOrBoolV2, Label: "||"},
string("&&" + types.Int): {f: stringAndIntV2, Label: "&&"},
Expand Down Expand Up @@ -461,8 +461,8 @@ func init() {
string("!=" + types.String): {f: dictNotStringV2, Label: "!="},
string("==" + types.Regex): {f: dictCmpRegexV2, Label: "=="},
string("!=" + types.Regex): {f: dictNotRegexV2, Label: "!="},
string("==" + types.Semver): {f: semverCmpSemver, Label: "=="},
string("!=" + types.Semver): {f: semverNotSemver, Label: "!="},
string("==" + types.Version): {f: versionCmpVersion, Label: "=="},
string("!=" + types.Version): {f: versionNotVersion, Label: "!="},
string("==" + types.ArrayLike): {f: dictCmpArrayV2, Label: "=="},
string("!=" + types.ArrayLike): {f: dictNotArrayV2, Label: "!="},
string("==" + types.Array(types.String)): {f: dictCmpStringarrayV2, Label: "=="},
Expand Down Expand Up @@ -491,10 +491,10 @@ func init() {
string("<=" + types.Dict): {f: dictLTEDictV2, Label: "<="},
string(">" + types.Dict): {f: dictGTDictV2, Label: ">"},
string(">=" + types.Dict): {f: dictGTEDictV2, Label: ">="},
string("<" + types.Semver): {f: semverLTsemver, Label: "<"},
string(">" + types.Semver): {f: semverGTsemver, Label: ">"},
string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="},
string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="},
string("<" + types.Version): {f: versionLTversion, Label: "<"},
string(">" + types.Version): {f: versionGTversion, Label: ">"},
string("<=" + types.Version): {f: versionLTEversion, Label: "<="},
string(">=" + types.Version): {f: versionGTEversion, Label: ">="},
string("&&" + types.Bool): {f: dictAndBoolV2, Label: "&&"},
string("||" + types.Bool): {f: dictOrBoolV2, Label: "||"},
string("&&" + types.Int): {f: dictAndIntV2, Label: "&&"},
Expand Down Expand Up @@ -565,23 +565,24 @@ func init() {
// We have not yet decided if and how these may be exposed to users
"notEmpty": {f: dictNotEmptyV2},
},
types.Semver: {
string("==" + types.Nil): {f: stringCmpNilV2, Label: "=="},
string("!=" + types.Nil): {f: stringNotNilV2, Label: "!="},
string("==" + types.Empty): {f: stringCmpEmptyV2, Label: "=="},
string("!=" + types.Empty): {f: stringNotEmptyV2, Label: "!="},
string("==" + types.Semver): {f: semverCmpSemver, Label: "=="},
string("!=" + types.Semver): {f: semverNotSemver, Label: "!="},
string("<" + types.Semver): {f: semverLTsemver, Label: "<"},
string(">" + types.Semver): {f: semverGTsemver, Label: ">"},
string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="},
string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="},
string("==" + types.String): {f: semverCmpSemver, Label: "=="},
string("!=" + types.String): {f: semverNotSemver, Label: "!="},
string("<" + types.String): {f: semverLTsemver, Label: "<"},
string(">" + types.String): {f: semverGTsemver, Label: ">"},
string("<=" + types.String): {f: semverLTEsemver, Label: "<="},
string(">=" + types.String): {f: semverGTEsemver, Label: ">="},
types.Version: {
string("==" + types.Nil): {f: stringCmpNilV2, Label: "=="},
string("!=" + types.Nil): {f: stringNotNilV2, Label: "!="},
string("==" + types.Empty): {f: stringCmpEmptyV2, Label: "=="},
string("!=" + types.Empty): {f: stringNotEmptyV2, Label: "!="},
string("==" + types.Version): {f: versionCmpVersion, Label: "=="},
string("!=" + types.Version): {f: versionNotVersion, Label: "!="},
string("<" + types.Version): {f: versionLTversion, Label: "<"},
string(">" + types.Version): {f: versionGTversion, Label: ">"},
string("<=" + types.Version): {f: versionLTEversion, Label: "<="},
string(">=" + types.Version): {f: versionGTEversion, Label: ">="},
string("==" + types.String): {f: versionCmpVersion, Label: "=="},
string("!=" + types.String): {f: versionNotVersion, Label: "!="},
string("<" + types.String): {f: versionLTversion, Label: "<"},
string(">" + types.String): {f: versionGTversion, Label: ">"},
string("<=" + types.String): {f: versionLTEversion, Label: "<="},
string(">=" + types.String): {f: versionGTEversion, Label: ">="},
"epoch": {f: versionEpoch},
},
types.ArrayLike: {
"[]": {f: arrayGetIndexV2},
Expand Down
23 changes: 13 additions & 10 deletions llx/builtin_global.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ func init() {
"return": returnCallV2,
"createResource": globalCreateResource,
// type-conversions
"string": stringCall,
"$regex": regexCall, // TODO: support both the regex resource and the internal typemap!
"float": floatCall,
"int": intCall,
"bool": boolCall,
"dict": dictCall,
"semver": semverCall,
"string": stringCall,
"$regex": regexCall, // TODO: support both the regex resource and the internal typemap!
"float": floatCall,
"int": intCall,
"bool": boolCall,
"dict": dictCall,
"version": versionCall,
// FIXME: DEPRECATED, remove in v13.0 vv
"semver": versionCall, // deprecated
// ^^
}
}

Expand Down Expand Up @@ -193,17 +196,17 @@ func typeofCallV2(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64,
return StringData(res.Type.Label()), 0, nil
}

func semverCall(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) {
func versionCall(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) {
if len(f.Args) != 1 {
return nil, 0, errors.New("Called `semver` with " + strconv.Itoa(len(f.Args)) + " arguments, expected one")
return nil, 0, errors.New("Called `version` with " + strconv.Itoa(len(f.Args)) + " arguments, expected one")
}

res, dref, err := e.resolveValue(f.Args[0], ref)
if err != nil || dref != 0 || res == nil {
return res, dref, err
}

return &RawData{Type: types.Semver, Value: res.Value}, 0, nil
return &RawData{Type: types.Version, Value: res.Value}, 0, nil
}

func stringCall(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) {
Expand Down
85 changes: 0 additions & 85 deletions llx/builtin_semver.go

This file was deleted.

142 changes: 142 additions & 0 deletions llx/builtin_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package llx

import (
"regexp"
"strconv"

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

// 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 {
x, epoch = parseEpoch(s)
}

return Version{
Version: x,
src: s,
epoch: epoch,
}
}

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 nil, 0
}

// invalid epoch means we discard the entire version string
epoch, err := strconv.Atoi(epochStr)
if err != nil {
return nil, 0
}

return res, epoch
}

// 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
}
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 v.Version.Compare(o.Version)
}

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

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

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

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

func versionCmpVersion(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData {
return BoolData(left.(string) == right.(string))
})
}

func versionNotVersion(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData {
return BoolData(left.(string) != right.(string))
})
}

func versionLTversion(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, versionLT)
}

func versionGTversion(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, versionGT)
}

func versionLTEversion(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, versionLTE)
}

func versionGTEversion(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) {
return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, versionGTE)
}

func versionEpoch(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
}
Loading

0 comments on commit d48ae6e

Please sign in to comment.