fb32dcfa58
Main Clair logic is changed in worker, updater, notifier for better adapting ancestry schema. Extensions are updated with the new model and feature lister and namespace detector drivers are able to specify the specific listers and detectors used to process layer's content. InRange and GetFixedIn interfaces are added to Version format for adapting ranged affected features and next available fixed in in the future. Tests for worker, updater and extensions are fixed.
290 lines
6.2 KiB
Go
290 lines
6.2 KiB
Go
// Copyright 2017 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 implements a versionfmt.Parser for version numbers used in rpm
|
|
// based software packages.
|
|
package rpm
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/coreos/clair/ext/versionfmt"
|
|
)
|
|
|
|
// ParserName is the name by which the rpm parser is registered.
|
|
const ParserName = "rpm"
|
|
|
|
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
|
|
release string
|
|
}
|
|
|
|
var (
|
|
minVersion = version{version: versionfmt.MinVersion}
|
|
maxVersion = version{version: versionfmt.MaxVersion}
|
|
)
|
|
|
|
// newVersion parses a string into a version type which can be compared.
|
|
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 / release
|
|
seprevision := strings.Index(str, "-")
|
|
if seprevision > -1 {
|
|
v.version = str[sepepoch+1 : seprevision]
|
|
v.release = str[seprevision+1:]
|
|
} else {
|
|
v.version = str[sepepoch+1:]
|
|
v.release = ""
|
|
}
|
|
// Verify format
|
|
if len(v.version) == 0 {
|
|
return version{}, errors.New("No version")
|
|
}
|
|
|
|
for i := 0; i < len(v.version); i = i + 1 {
|
|
r := rune(v.version[i])
|
|
if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) {
|
|
return version{}, errors.New("invalid character in version")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
type parser struct{}
|
|
|
|
func (p parser) Valid(str string) bool {
|
|
_, err := newVersion(str)
|
|
return err == nil
|
|
}
|
|
|
|
func (p parser) InRange(versionA, rangeB string) (bool, error) {
|
|
cmp, err := p.Compare(versionA, rangeB)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return cmp < 0, nil
|
|
}
|
|
|
|
func (p parser) GetFixedIn(fixedIn string) (string, error) {
|
|
// In the old version format parser design, the string to determine fixed in
|
|
// version is the fixed in version.
|
|
return fixedIn, nil
|
|
}
|
|
|
|
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 := rpmvercmp(v1.version, v2.version)
|
|
if rc != 0 {
|
|
return rc, nil
|
|
}
|
|
|
|
// Compare revision
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// If there is a tilde in a segment past the min number of segments, find it.
|
|
if len(segsa) > segs && []rune(segsa[segs])[0] == '~' {
|
|
return -1
|
|
} else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' {
|
|
return 1
|
|
}
|
|
|
|
// whoever has the most segments wins
|
|
if len(segsa) > len(segsb) {
|
|
return 1
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// 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.release != "" {
|
|
s += "-" + v.release
|
|
}
|
|
return
|
|
}
|
|
|
|
func validSymbol(r rune) bool {
|
|
return containsRune(allowedSymbols, r)
|
|
}
|
|
|
|
func containsRune(s []rune, e rune) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func init() {
|
|
versionfmt.RegisterParser(ParserName, parser{})
|
|
}
|