versionfmt: init rpm versionfmt

This commit is contained in:
Jimmy Zelinskie 2016-12-28 23:21:12 -05:00
parent 033709eaea
commit 6864a8efea
2 changed files with 245 additions and 238 deletions

View File

@ -16,6 +16,8 @@ package rpm
import (
"errors"
"math"
"regexp"
"strconv"
"strings"
"unicode"
@ -23,26 +25,25 @@ import (
"github.com/coreos/clair/ext/versionfmt"
)
var (
// alphanumPattern is a regular expression to match all sequences of numeric
// characters or alphanumeric characters.
alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)")
allowedSymbols = []rune{'.', '-', '+', '~', ':', '_'}
)
type version struct {
epoch int
version string
revision string
epoch int
version string
release string
}
var (
minVersion = version{version: versionfmt.MinVersion}
maxVersion = version{version: versionfmt.MaxVersion}
versionAllowedSymbols = []rune{'.', '-', '+', '~', ':', '_'}
revisionAllowedSymbols = []rune{'.', '+', '~', '_'}
)
// newVersion function parses a string into a Version struct which can be compared
//
// The implementation is based on http://man.he.net/man5/deb-version
// on https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
//
// It uses the dpkg-1.17.25's algorithm (lib/parsehelp.c)
// newVersion parses a string into a version type which can be compared.
func newVersion(str string) (version, error) {
var v version
@ -77,34 +78,30 @@ func newVersion(str string) (version, error) {
v.epoch = 0
}
// Find version / revision
seprevision := strings.LastIndex(str, "-")
// Find version / release
seprevision := strings.Index(str, "-")
if seprevision > -1 {
v.version = str[sepepoch+1 : seprevision]
v.revision = str[seprevision+1:]
v.release = str[seprevision+1:]
} else {
v.version = str[sepepoch+1:]
v.revision = ""
v.release = ""
}
// Verify format
if len(v.version) == 0 {
return version{}, errors.New("No version")
}
if !unicode.IsDigit(rune(v.version[0])) {
return version{}, errors.New("version does not start with digit")
}
for i := 0; i < len(v.version); i = i + 1 {
r := rune(v.version[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !containsRune(versionAllowedSymbols, r) {
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) {
return version{}, errors.New("invalid character in version")
}
}
for i := 0; i < len(v.revision); i = i + 1 {
r := rune(v.revision[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !containsRune(revisionAllowedSymbols, r) {
for i := 0; i < len(v.release); i = i + 1 {
r := rune(v.release[i])
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) {
return version{}, errors.New("invalid character in revision")
}
}
@ -112,13 +109,6 @@ func newVersion(str string) (version, error) {
return v, nil
}
// newVersionUnsafe is just a wrapper around NewVersion that ignore potentiel
// parsing error. Useful for test purposes
func newVersionUnsafe(str string) version {
v, _ := newVersion(str)
return v
}
type parser struct{}
func (p parser) Valid(str string) bool {
@ -126,12 +116,6 @@ func (p parser) Valid(str string) bool {
return err == nil
}
// Compare function compares two Debian-like package version
//
// The implementation is based on http://man.he.net/man5/deb-version
// on https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
//
// It uses the dpkg-1.17.25's algorithm (lib/version.c)
func (p parser) Compare(a, b string) (int, error) {
v1, err := newVersion(a)
if err != nil {
@ -165,13 +149,102 @@ func (p parser) Compare(a, b string) (int, error) {
}
// Compare version
rc := verrevcmp(v1.version, v2.version)
rc := rpmvercmp(v1.version, v2.version)
if rc != 0 {
return signum(rc), nil
return rc, nil
}
// Compare revision
return signum(verrevcmp(v1.revision, v2.revision)), nil
return rpmvercmp(v1.release, v2.release), nil
}
// rpmcmpver compares two version or release strings.
//
// Lifted from github.com/cavaliercoder/go-rpm.
// For the original C implementation, see:
// https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16
func rpmvercmp(strA, strB string) int {
// shortcut for equality
if strA == strB {
return 0
}
// get alpha/numeric segements
segsa := alphanumPattern.FindAllString(strA, -1)
segsb := alphanumPattern.FindAllString(strB, -1)
segs := int(math.Min(float64(len(segsa)), float64(len(segsb))))
// compare each segment
for i := 0; i < segs; i++ {
a := segsa[i]
b := segsb[i]
// compare tildes
if []rune(a)[0] == '~' && []rune(b)[0] == '~' {
continue
}
if []rune(a)[0] == '~' && []rune(b)[0] != '~' {
return -1
}
if []rune(a)[0] != '~' && []rune(b)[0] == '~' {
return 1
}
if unicode.IsNumber([]rune(a)[0]) {
// numbers are always greater than alphas
if !unicode.IsNumber([]rune(b)[0]) {
// a is numeric, b is alpha
return 1
}
// trim leading zeros
a = strings.TrimLeft(a, "0")
b = strings.TrimLeft(b, "0")
// longest string wins without further comparison
if len(a) > len(b) {
return 1
} else if len(b) > len(a) {
return -1
}
} else if unicode.IsNumber([]rune(b)[0]) {
// a is alpha, b is numeric
return -1
}
// This is the last iteration.
if i == segs-1 {
// If there is a tilde in a segment past the min number of segments, find
// it before we rely on string compare.
lia := strings.LastIndex(strA, "~")
lib := strings.LastIndex(strB, "~")
if lia > lib {
return -1
} else if lia < lib {
return 1
}
}
// string compare
if a < b {
return -1
} else if a > b {
return 1
}
}
// segments were all the same but separators must have been different
if len(segsa) == len(segsb) {
return 0
}
// whoever has the most segments wins
if len(segsa) > len(segsb) {
return 1
}
return -1
}
// String returns the string representation of a Version.
@ -180,88 +253,14 @@ func (v version) String() (s string) {
s = strconv.Itoa(v.epoch) + ":"
}
s += v.version
if v.revision != "" {
s += "-" + v.revision
if v.release != "" {
s += "-" + v.release
}
return
}
func verrevcmp(t1, t2 string) int {
t1, rt1 := nextRune(t1)
t2, rt2 := nextRune(t2)
for rt1 != nil || rt2 != nil {
firstDiff := 0
for (rt1 != nil && !unicode.IsDigit(*rt1)) || (rt2 != nil && !unicode.IsDigit(*rt2)) {
ac := 0
bc := 0
if rt1 != nil {
ac = order(*rt1)
}
if rt2 != nil {
bc = order(*rt2)
}
if ac != bc {
return ac - bc
}
t1, rt1 = nextRune(t1)
t2, rt2 = nextRune(t2)
}
for rt1 != nil && *rt1 == '0' {
t1, rt1 = nextRune(t1)
}
for rt2 != nil && *rt2 == '0' {
t2, rt2 = nextRune(t2)
}
for rt1 != nil && unicode.IsDigit(*rt1) && rt2 != nil && unicode.IsDigit(*rt2) {
if firstDiff == 0 {
firstDiff = int(*rt1) - int(*rt2)
}
t1, rt1 = nextRune(t1)
t2, rt2 = nextRune(t2)
}
if rt1 != nil && unicode.IsDigit(*rt1) {
return 1
}
if rt2 != nil && unicode.IsDigit(*rt2) {
return -1
}
if firstDiff != 0 {
return firstDiff
}
}
return 0
}
// order compares runes using a modified ASCII table
// so that letters are sorted earlier than non-letters
// and so that tildes sorts before anything
func order(r rune) int {
if unicode.IsDigit(r) {
return 0
}
if unicode.IsLetter(r) {
return int(r)
}
if r == '~' {
return -1
}
return int(r) + 256
}
func nextRune(str string) (string, *rune) {
if len(str) >= 1 {
r := rune(str[0])
return str[1:], &r
}
return str, nil
func validSymbol(r rune) bool {
return containsRune(allowedSymbols, r)
}
func containsRune(s []rune, e rune) bool {
@ -273,17 +272,6 @@ func containsRune(s []rune, e rune) bool {
return false
}
func signum(a int) int {
switch {
case a < 0:
return -1
case a > 0:
return +1
}
return 0
}
func init() {
versionfmt.RegisterParser("rpm", parser{})
}

View File

@ -15,7 +15,6 @@
package rpm
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -34,31 +33,27 @@ func TestParse(t *testing.T) {
err bool
}{
// Test 0
{"0", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0-", version{epoch: 0, version: "0", revision: ""}, false},
{"0:0-0", version{epoch: 0, version: "0", revision: "0"}, false},
{"0:0.0-0.0", version{epoch: 0, version: "0.0", revision: "0.0"}, false},
{"0", version{epoch: 0, version: "0", release: ""}, false},
{"0:0", version{epoch: 0, version: "0", release: ""}, false},
{"0:0-", version{epoch: 0, version: "0", release: ""}, false},
{"0:0-0", version{epoch: 0, version: "0", release: "0"}, false},
{"0:0.0-0.0", version{epoch: 0, version: "0.0", release: "0.0"}, false},
// Test epoched
{"1:0", version{epoch: 1, version: "0", revision: ""}, false},
{"5:1", version{epoch: 5, version: "1", revision: ""}, false},
{"1:0", version{epoch: 1, version: "0", release: ""}, false},
{"5:1", version{epoch: 5, version: "1", release: ""}, false},
// Test multiple hypens
{"0:0-0-0", version{epoch: 0, version: "0-0", revision: "0"}, false},
{"0:0-0-0-0", version{epoch: 0, version: "0-0-0", revision: "0"}, false},
{"0:0-0-0", version{epoch: 0, version: "0", release: "0-0"}, false},
{"0:0-0-0-0", version{epoch: 0, version: "0", release: "0-0-0"}, false},
// Test multiple colons
{"0:0:0-0", version{epoch: 0, version: "0:0", revision: "0"}, false},
{"0:0:0:0-0", version{epoch: 0, version: "0:0:0", revision: "0"}, false},
{"0:0:0-0", version{epoch: 0, version: "0:0", release: "0"}, false},
{"0:0:0:0-0", version{epoch: 0, version: "0:0:0", release: "0"}, false},
// Test multiple hyphens and colons
{"0:0:0-0-0", version{epoch: 0, version: "0:0-0", revision: "0"}, false},
{"0:0-0:0-0", version{epoch: 0, version: "0-0:0", revision: "0"}, false},
// Test valid characters in version
{"0:09azAZ.-+~:_-0", version{epoch: 0, version: "09azAZ.-+~:_", revision: "0"}, false},
// Test valid characters in debian revision
{"0:0-azAZ09.+~_", version{epoch: 0, version: "0", revision: "azAZ09.+~_"}, false},
{"0:0:0-0-0", version{epoch: 0, version: "0:0", release: "0-0"}, false},
{"0:0-0:0-0", version{epoch: 0, version: "0", release: "0:0-0"}, false},
// Test version with leading and trailing spaces
{" 0:0-1", version{epoch: 0, version: "0", revision: "1"}, false},
{"0:0-1 ", version{epoch: 0, version: "0", revision: "1"}, false},
{" 0:0-1 ", version{epoch: 0, version: "0", revision: "1"}, false},
{" 0:0-1", version{epoch: 0, version: "0", release: "1"}, false},
{"0:0-1 ", version{epoch: 0, version: "0", release: "1"}, false},
{" 0:0-1 ", version{epoch: 0, version: "0", release: "1"}, false},
// Test empty version
{"", version{}, true},
{" ", version{}, true},
@ -71,7 +66,7 @@ func TestParse(t *testing.T) {
{"a:0-0", version{}, true},
{"A:0-0", version{}, true},
// Test version not starting with a digit
{"0:abc3-0", version{}, true},
{"0:abc3-0", version{epoch: 0, version: "abc3", release: "0"}, false},
}
for _, c := range cases {
v, err := newVersion(c.str)
@ -83,20 +78,6 @@ func TestParse(t *testing.T) {
}
assert.Equal(t, c.ver, v, "When parsing '%s'", c.str)
}
// Test invalid characters in version
versym := []rune{'!', '#', '@', '$', '%', '&', '/', '|', '\\', '<', '>', '(', ')', '[', ']', '{', '}', ';', ',', '=', '*', '^', '\''}
for _, r := range versym {
_, err := newVersion(strings.Join([]string{"0:0", string(r), "-0"}, ""))
assert.Error(t, err, "Parsing with invalid character '%s' in version should have failed", string(r))
}
// Test invalid characters in revision
versym = []rune{'!', '#', '@', '$', '%', '&', '/', '|', '\\', '<', '>', '(', ')', '[', ']', '{', '}', ':', ';', ',', '=', '*', '^', '\''}
for _, r := range versym {
_, err := newVersion(strings.Join([]string{"0:0-", string(r)}, ""))
assert.Error(t, err, "Parsing with invalid character '%s' in revision should have failed", string(r))
}
}
func TestParseAndCompare(t *testing.T) {
@ -105,79 +86,117 @@ func TestParseAndCompare(t *testing.T) {
expected int
v2 string
}{
{"7.6p2-4", GREATER, "7.6-0"},
{"1.0.3-3", GREATER, "1.0-1"},
{"1.3", GREATER, "1.2.2-2"},
{"1.3", GREATER, "1.2.2"},
// Some properties of text strings
{"0-pre", EQUAL, "0-pre"},
{"0-pre", LESS, "0-pree"},
{"1.1.6r2-2", GREATER, "1.1.6r-1"},
{"2.6b2-1", GREATER, "2.6b-2"},
{"98.1p5-1", LESS, "98.1-pre2-b6-2"},
{"0.4a6-2", GREATER, "0.4-1"},
{"1:3.0.5-2", LESS, "1:3.0.5.1"},
// epochs
{"1:0.4", GREATER, "10.3"},
{"1:1.25-4", LESS, "1:1.25-8"},
{"0:1.18.36", EQUAL, "1.18.36"},
{"1.18.36", GREATER, "1.18.35"},
{"0:1.18.36", GREATER, "1.18.35"},
// Funky, but allowed, characters in upstream version
{"9:1.18.36:5.4-20", LESS, "10:0.5.1-22"},
{"9:1.18.36:5.4-20", LESS, "9:1.18.36:5.5-1"},
{"9:1.18.36:5.4-20", LESS, " 9:1.18.37:4.3-22"},
{"1.18.36-0.17.35-18", GREATER, "1.18.36-19"},
// Junk
{"1:1.2.13-3", LESS, "1:1.2.13-3.1"},
{"2.0.7pre1-4", LESS, "2.0.7r-1"},
// if a version includes a dash, it should be the debrev dash - policy says so
{"0:0-0-0", GREATER, "0-0"},
// do we like strange versions? Yes we like strange versions…
{"0", EQUAL, "0"},
{"0", EQUAL, "00"},
// #205960
{"3.0~rc1-1", LESS, "3.0-1"},
// #573592 - debian policy 5.6.12
{"1.0", EQUAL, "1.0-0"},
{"0.2", LESS, "1.0-0"},
{"1.0", LESS, "1.0-0+b1"},
{"1.0", GREATER, "1.0-0~"},
// "steal" the testcases from (old perl) cupt
{"1.2.3", EQUAL, "1.2.3"}, // identical
{"4.4.3-2", EQUAL, "4.4.3-2"}, // identical
{"1:2ab:5", EQUAL, "1:2ab:5"}, // this is correct...
{"7:1-a:b-5", EQUAL, "7:1-a:b-5"}, // and this
{"57:1.2.3abYZ+~-4-5", EQUAL, "57:1.2.3abYZ+~-4-5"}, // and those too
{"1.2.3", EQUAL, "0:1.2.3"}, // zero epoch
{"1.2.3", EQUAL, "1.2.3-0"}, // zero revision
{"009", EQUAL, "9"}, // zeroes…
{"009ab5", EQUAL, "9ab5"}, // there as well
{"1.2.3", LESS, "1.2.3-1"}, // added non-zero revision
{"1.2.3", LESS, "1.2.4"}, // just bigger
{"1.2.4", GREATER, "1.2.3"}, // order doesn't matter
{"1.2.24", GREATER, "1.2.3"}, // bigger, eh?
{"0.10.0", GREATER, "0.8.7"}, // bigger, eh?
{"3.2", GREATER, "2.3"}, // major number rocks
{"1.3.2a", GREATER, "1.3.2"}, // letters rock
{"0.5.0~git", LESS, "0.5.0~git2"}, // numbers rock
{"2a", LESS, "21"}, // but not in all places
{"1.3.2a", LESS, "1.3.2b"}, // but there is another letter
{"1:1.2.3", GREATER, "1.2.4"}, // epoch rocks
{"1:1.2.3", LESS, "1:1.2.4"}, // bigger anyway
{"1.2a+~bCd3", LESS, "1.2a++"}, // tilde doesn't rock
{"1.2a+~bCd3", GREATER, "1.2a+~"}, // but first is longer!
{"5:2", GREATER, "304-2"}, // epoch rocks
{"5:2", LESS, "304:2"}, // so big epoch?
{"25:2", GREATER, "3:2"}, // 25 > 3, obviously
{"1:2:123", LESS, "1:12:3"}, // 12 > 2
{"1.2-5", LESS, "1.2-3-5"}, // 1.2 < 1.2-3
{"5.10.0", GREATER, "5.005"}, // preceding zeroes don't matters
{"3a9.8", LESS, "3.10.2"}, // letters are before all letter symbols
{"3a9.8", GREATER, "3~10"}, // but after the tilde
{"1.4+OOo3.0.0~", LESS, "1.4+OOo3.0.0-4"}, // another tilde check
{"2.4.7-1", LESS, "2.4.7-z"}, // revision comparing
{"1.002-1+b2", GREATER, "1.00"}, // whatever...
// Tests imported from tests/rpmvercmp.at
{"1.0", EQUAL, "1.0"},
{"1.0", LESS, "2.0"},
{"2.0", GREATER, "1.0"},
{"2.0.1", EQUAL, "2.0.1"},
{"2.0", LESS, "2.0.1"},
{"2.0.1", GREATER, "2.0"},
{"2.0.1a", EQUAL, "2.0.1a"},
{"2.0.1a", GREATER, "2.0.1"},
{"2.0.1", LESS, "2.0.1a"},
{"5.5p1", EQUAL, "5.5p1"},
{"5.5p1", LESS, "5.5p2"},
{"5.5p2", GREATER, "5.5p1"},
{"5.5p10", EQUAL, "5.5p10"},
{"5.5p1", LESS, "5.5p10"},
{"5.5p10", GREATER, "5.5p1"},
{"10xyz", LESS, "10.1xyz"},
{"10.1xyz", GREATER, "10xyz"},
{"xyz10", EQUAL, "xyz10"},
{"xyz10", LESS, "xyz10.1"},
{"xyz10.1", GREATER, "xyz10"},
{"xyz.4", EQUAL, "xyz.4"},
{"xyz.4", LESS, "8"},
{"8", GREATER, "xyz.4"},
{"xyz.4", LESS, "2"},
{"2", GREATER, "xyz.4"},
{"5.5p2", LESS, "5.6p1"},
{"5.6p1", GREATER, "5.5p2"},
{"5.6p1", LESS, "6.5p1"},
{"6.5p1", GREATER, "5.6p1"},
{"6.0.rc1", GREATER, "6.0"},
{"6.0", LESS, "6.0.rc1"},
{"10b2", GREATER, "10a1"},
{"10a2", LESS, "10b2"},
{"1.0aa", EQUAL, "1.0aa"},
{"1.0a", LESS, "1.0aa"},
{"1.0aa", GREATER, "1.0a"},
{"10.0001", EQUAL, "10.0001"},
{"10.0001", EQUAL, "10.1"},
{"10.1", EQUAL, "10.0001"},
{"10.0001", LESS, "10.0039"},
{"10.0039", GREATER, "10.0001"},
{"4.999.9", LESS, "5.0"},
{"5.0", GREATER, "4.999.9"},
{"20101121", EQUAL, "20101121"},
{"20101121", LESS, "20101122"},
{"20101122", GREATER, "20101121"},
{"2_0", EQUAL, "2_0"},
{"2.0", EQUAL, "2_0"},
{"2_0", EQUAL, "2.0"},
// RhBug:178798 case
{"a", EQUAL, "a"},
{"a+", EQUAL, "a+"},
{"a+", EQUAL, "a_"},
{"a_", EQUAL, "a+"},
{"+a", EQUAL, "+a"},
{"+a", EQUAL, "_a"},
{"_a", EQUAL, "+a"},
{"+_", EQUAL, "+_"},
{"_+", EQUAL, "+_"},
{"_+", EQUAL, "_+"},
{"+", EQUAL, "_"},
{"_", EQUAL, "+"},
// Basic testcases for tilde sorting
{"1.0~rc1", EQUAL, "1.0~rc1"},
{"1.0~rc1", LESS, "1.0"},
{"1.0", GREATER, "1.0~rc1"},
{"1.0~rc1", LESS, "1.0~rc2"},
{"1.0~rc2", GREATER, "1.0~rc1"},
{"1.0~rc1~git123", EQUAL, "1.0~rc1~git123"},
{"1.0~rc1~git123", LESS, "1.0~rc1"},
{"1.0~rc1", GREATER, "1.0~rc1~git123"},
// These are included here to document current, arguably buggy behaviors
// for reference purposes and for easy checking against unintended
// behavior changes.
//
// AT_BANNER([RPM version comparison oddities])
// RhBug:811992 case
// {"1b.fc17", EQUAL, "1b.fc17"},
// {"1b.fc17", LESS, "1.fc17"},
// {"1.fc17", GREATER, "1b.fc17"},
// {"1g.fc17", EQUAL, "1g.fc17"},
// {"1g.fc17", GREATER, "1.fc17"},
// {"1.fc17", LESS, "1g.fc17"},
// Non-ascii characters are considered equal so these are all the same, eh...
// {"1.1.α", EQUAL, "1.1.α"},
// {"1.1.α", EQUAL, "1.1.β"},
// {"1.1.β", EQUAL, "1.1.α"},
// {"1.1.αα", EQUAL, "1.1.α"},
// {"1.1.α", EQUAL, "1.1.ββ"},
// {"1.1.ββ", EQUAL, "1.1.αα"},
}
var (