Since we only ever used dpkg, this change shims everything into using dpkg.pull/298/head
parent
3897fb6706
commit
033709eaea
@ -0,0 +1,29 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package migrations
|
||||
|
||||
import "github.com/remind101/migrate"
|
||||
|
||||
func init() {
|
||||
RegisterMigration(migrate.Migration{
|
||||
ID: 6,
|
||||
Up: migrate.Queries([]string{
|
||||
`ALTER TABLE Namespace ADD COLUMN version_format varchar(128);`,
|
||||
}),
|
||||
Down: migrate.Queries([]string{
|
||||
`ALTER TABLE Namespace DROP COLUMN version_format;`,
|
||||
}),
|
||||
})
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dpkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/coreos/clair/ext/versionfmt"
|
||||
)
|
||||
|
||||
type version struct {
|
||||
epoch int
|
||||
version string
|
||||
revision 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)
|
||||
func newVersion(str string) (version, error) {
|
||||
var v version
|
||||
|
||||
// Trim leading and trailing space
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
if len(str) == 0 {
|
||||
return version{}, errors.New("Version string is empty")
|
||||
}
|
||||
|
||||
// Max/Min versions
|
||||
if str == maxVersion.String() {
|
||||
return maxVersion, nil
|
||||
}
|
||||
if str == minVersion.String() {
|
||||
return minVersion, nil
|
||||
}
|
||||
|
||||
// Find epoch
|
||||
sepepoch := strings.Index(str, ":")
|
||||
if sepepoch > -1 {
|
||||
intepoch, err := strconv.Atoi(str[:sepepoch])
|
||||
if err == nil {
|
||||
v.epoch = intepoch
|
||||
} else {
|
||||
return version{}, errors.New("epoch in version is not a number")
|
||||
}
|
||||
if intepoch < 0 {
|
||||
return version{}, errors.New("epoch in version is negative")
|
||||
}
|
||||
} else {
|
||||
v.epoch = 0
|
||||
}
|
||||
|
||||
// Find version / revision
|
||||
seprevision := strings.LastIndex(str, "-")
|
||||
if seprevision > -1 {
|
||||
v.version = str[sepepoch+1 : seprevision]
|
||||
v.revision = str[seprevision+1:]
|
||||
} else {
|
||||
v.version = str[sepepoch+1:]
|
||||
v.revision = ""
|
||||
}
|
||||
// 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) {
|
||||
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) {
|
||||
return version{}, errors.New("invalid character in revision")
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type parser struct{}
|
||||
|
||||
func (p parser) Valid(str string) bool {
|
||||
_, err := newVersion(str)
|
||||
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 {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
v2, err := newVersion(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Quick check
|
||||
if v1 == v2 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Max/Min comparison
|
||||
if v1 == minVersion || v2 == maxVersion {
|
||||
return -1, nil
|
||||
}
|
||||
if v2 == minVersion || v1 == maxVersion {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// Compare epochs
|
||||
if v1.epoch > v2.epoch {
|
||||
return 1, nil
|
||||
}
|
||||
if v1.epoch < v2.epoch {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
// Compare version
|
||||
rc := verrevcmp(v1.version, v2.version)
|
||||
if rc != 0 {
|
||||
return signum(rc), nil
|
||||
}
|
||||
|
||||
// Compare revision
|
||||
return signum(verrevcmp(v1.revision, v2.revision)), nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Version.
|
||||
func (v version) String() (s string) {
|
||||
if v.epoch != 0 {
|
||||
s = strconv.Itoa(v.epoch) + ":"
|
||||
}
|
||||
s += v.version
|
||||
if v.revision != "" {
|
||||
s += "-" + v.revision
|
||||
}
|
||||
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 containsRune(s []rune, e rune) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func signum(a int) int {
|
||||
switch {
|
||||
case a < 0:
|
||||
return -1
|
||||
case a > 0:
|
||||
return +1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionfmt.RegisterParser("dpkg", parser{})
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dpkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
LESS = -1
|
||||
EQUAL = 0
|
||||
GREATER = 1
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
cases := []struct {
|
||||
str string
|
||||
ver version
|
||||
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},
|
||||
// Test epoched
|
||||
{"1:0", version{epoch: 1, version: "0", revision: ""}, false},
|
||||
{"5:1", version{epoch: 5, version: "1", revision: ""}, 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},
|
||||
// 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},
|
||||
// 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},
|
||||
// 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},
|
||||
// Test empty version
|
||||
{"", version{}, true},
|
||||
{" ", version{}, true},
|
||||
{"0:", version{}, true},
|
||||
// Test version with embedded spaces
|
||||
{"0:0 0-1", version{}, true},
|
||||
// Test version with negative epoch
|
||||
{"-1:0-1", version{}, true},
|
||||
// Test invalid characters in epoch
|
||||
{"a:0-0", version{}, true},
|
||||
{"A:0-0", version{}, true},
|
||||
// Test version not starting with a digit
|
||||
{"0:abc3-0", version{}, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
v, err := newVersion(c.str)
|
||||
|
||||
if c.err {
|
||||
assert.Error(t, err, "When parsing '%s'", c.str)
|
||||
} else {
|
||||
assert.Nil(t, err, "When parsing '%s'", c.str)
|
||||
}
|
||||
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) {
|
||||
cases := []struct {
|
||||
v1 string
|
||||
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...
|
||||
}
|
||||
|
||||
var (
|
||||
p parser
|
||||
cmp int
|
||||
err error
|
||||
)
|
||||
for _, c := range cases {
|
||||
cmp, err = p.Compare(c.v1, c.v2)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v1, c.v2, cmp, c.expected)
|
||||
|
||||
cmp, err = p.Compare(c.v2, c.v1)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, -c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v2, c.v1, cmp, -c.expected)
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package versionfmt exposes functions to dynamically register formats used to
|
||||
// parse Feature Versions.
|
||||
package versionfmt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinVersion is a special package version which is always sorted first.
|
||||
MinVersion = "#MINV#"
|
||||
|
||||
// MaxVersion is a special package version which is always sorted last
|
||||
MaxVersion = "#MAXV#"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnknownVersionFormat is returned when a function does not have enough
|
||||
// context to determine the format of a version.
|
||||
ErrUnknownVersionFormat = errors.New("unknown version format")
|
||||
|
||||
// ErrInvalidVersion is returned when a function needs to validate a version,
|
||||
// but should return an error in the case where the version is invalid.
|
||||
ErrInvalidVersion = errors.New("invalid version")
|
||||
|
||||
nlog = capnslog.NewPackageLogger("github.com/coreos/clair", "ext/versionfmt")
|
||||
|
||||
parsersM sync.Mutex
|
||||
parsers = make(map[string]Parser)
|
||||
)
|
||||
|
||||
// Parser represents any format that can compare two version strings.
|
||||
type Parser interface {
|
||||
// Valid attempts to parse a version string and returns its success.
|
||||
Valid(string) bool
|
||||
|
||||
// Compare parses two different version strings.
|
||||
// Returns 0 when equal, -1 when a < b, 1 when b < a.
|
||||
Compare(a, b string) (int, error)
|
||||
}
|
||||
|
||||
// RegisterParser provides a way to dynamically register an implementation of a
|
||||
// Parser.
|
||||
//
|
||||
// If RegisterParser is called twice with the same name, the name is blank, or
|
||||
// if the provided Parser is nil, this function panics.
|
||||
func RegisterParser(name string, p Parser) {
|
||||
if name == "" {
|
||||
panic("Could not register a Parser with an empty name")
|
||||
}
|
||||
if p == nil {
|
||||
panic("Could not register a nil Parser")
|
||||
}
|
||||
|
||||
parsersM.Lock()
|
||||
defer parsersM.Unlock()
|
||||
|
||||
if _, alreadyExists := parsers[name]; alreadyExists {
|
||||
panic("Parser '" + name + "' is already registered")
|
||||
}
|
||||
parsers[name] = p
|
||||
}
|
||||
|
||||
// GetParser returns the registered Parser with a provided name.
|
||||
func GetParser(name string) (p Parser, exists bool) {
|
||||
parsersM.Lock()
|
||||
defer parsersM.Unlock()
|
||||
|
||||
p, exists = parsers[name]
|
||||
return
|
||||
}
|
||||
|
||||
// Valid is a helper function that will return an error if the version fails to
|
||||
// validate with a given format.
|
||||
func Valid(format, version string) error {
|
||||
versionParser, exists := GetParser(format)
|
||||
if !exists {
|
||||
return ErrUnknownVersionFormat
|
||||
}
|
||||
|
||||
if !versionParser.Valid(version) {
|
||||
return ErrInvalidVersion
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare is a helper function that will compare two versions with a given
|
||||
// format and return an error if there are any failures.
|
||||
func Compare(format, versionA, versionB string) (int, error) {
|
||||
versionParser, exists := GetParser(format)
|
||||
if !exists {
|
||||
return 0, ErrUnknownVersionFormat
|
||||
}
|
||||
|
||||
return versionParser.Compare(versionA, versionB)
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rpm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/coreos/clair/ext/versionfmt"
|
||||
)
|
||||
|
||||
type version struct {
|
||||
epoch int
|
||||
version string
|
||||
revision 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)
|
||||
func newVersion(str string) (version, error) {
|
||||
var v version
|
||||
|
||||
// Trim leading and trailing space
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
if len(str) == 0 {
|
||||
return version{}, errors.New("Version string is empty")
|
||||
}
|
||||
|
||||
// Max/Min versions
|
||||
if str == maxVersion.String() {
|
||||
return maxVersion, nil
|
||||
}
|
||||
if str == minVersion.String() {
|
||||
return minVersion, nil
|
||||
}
|
||||
|
||||
// Find epoch
|
||||
sepepoch := strings.Index(str, ":")
|
||||
if sepepoch > -1 {
|
||||
intepoch, err := strconv.Atoi(str[:sepepoch])
|
||||
if err == nil {
|
||||
v.epoch = intepoch
|
||||
} else {
|
||||
return version{}, errors.New("epoch in version is not a number")
|
||||
}
|
||||
if intepoch < 0 {
|
||||
return version{}, errors.New("epoch in version is negative")
|
||||
}
|
||||
} else {
|
||||
v.epoch = 0
|
||||
}
|
||||
|
||||
// Find version / revision
|
||||
seprevision := strings.LastIndex(str, "-")
|
||||
if seprevision > -1 {
|
||||
v.version = str[sepepoch+1 : seprevision]
|
||||
v.revision = str[seprevision+1:]
|
||||
} else {
|
||||
v.version = str[sepepoch+1:]
|
||||
v.revision = ""
|
||||
}
|
||||
// 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) {
|
||||
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) {
|
||||
return version{}, errors.New("invalid character in revision")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
_, err := newVersion(str)
|
||||
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 {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
v2, err := newVersion(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Quick check
|
||||
if v1 == v2 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Max/Min comparison
|
||||
if v1 == minVersion || v2 == maxVersion {
|
||||
return -1, nil
|
||||
}
|
||||
if v2 == minVersion || v1 == maxVersion {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// Compare epochs
|
||||
if v1.epoch > v2.epoch {
|
||||
return 1, nil
|
||||
}
|
||||
if v1.epoch < v2.epoch {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
// Compare version
|
||||
rc := verrevcmp(v1.version, v2.version)
|
||||
if rc != 0 {
|
||||
return signum(rc), nil
|
||||
}
|
||||
|
||||
// Compare revision
|
||||
return signum(verrevcmp(v1.revision, v2.revision)), nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Version.
|
||||
func (v version) String() (s string) {
|
||||
if v.epoch != 0 {
|
||||
s = strconv.Itoa(v.epoch) + ":"
|
||||
}
|
||||
s += v.version
|
||||
if v.revision != "" {
|
||||
s += "-" + v.revision
|
||||
}
|
||||
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 containsRune(s []rune, e rune) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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{})
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
// Copyright 2016 clair authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rpm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
LESS = -1
|
||||
EQUAL = 0
|
||||
GREATER = 1
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
cases := []struct {
|
||||
str string
|
||||
ver version
|
||||
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},
|
||||
// Test epoched
|
||||
{"1:0", version{epoch: 1, version: "0", revision: ""}, false},
|
||||
{"5:1", version{epoch: 5, version: "1", revision: ""}, 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},
|
||||
// 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},
|
||||
// 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},
|
||||
// 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},
|
||||
// Test empty version
|
||||
{"", version{}, true},
|
||||
{" ", version{}, true},
|
||||
{"0:", version{}, true},
|
||||
// Test version with embedded spaces
|
||||
{"0:0 0-1", version{}, true},
|
||||
// Test version with negative epoch
|
||||
{"-1:0-1", version{}, true},
|
||||
// Test invalid characters in epoch
|
||||
{"a:0-0", version{}, true},
|
||||
{"A:0-0", version{}, true},
|
||||
// Test version not starting with a digit
|
||||
{"0:abc3-0", version{}, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
v, err := newVersion(c.str)
|
||||
|
||||
if c.err {
|
||||
assert.Error(t, err, "When parsing '%s'", c.str)
|
||||
} else {
|
||||
assert.Nil(t, err, "When parsing '%s'", c.str)
|
||||
}
|
||||
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) {
|
||||
cases := []struct {
|
||||
v1 string
|
||||
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...
|
||||
}
|
||||
|
||||
var (
|
||||
p parser
|
||||
cmp int
|
||||
err error
|
||||
)
|
||||
for _, c := range cases {
|
||||
cmp, err = p.Compare(c.v1, c.v2)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v1, c.v2, cmp, c.expected)
|
||||
|
||||
cmp, err = p.Compare(c.v2, c.v1)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, -c.expected, cmp, "%s vs. %s, = %d, expected %d", c.v2, c.v1, cmp, -c.expected)
|
||||
}
|
||||
}
|
Loading…
Reference in new issue