// 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" "math" "regexp" "strconv" "strings" "unicode" "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 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) 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 } // 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. 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("rpm", parser{}) }