rpm/compare.go (100 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates.
//
// 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"
"unicode"
)
// LabelCompare returns 0 if the two packages have the same label, -1 if l1 < l2
// and 1 of l1 > l2.
func LabelCompare(l1, l2 Label) int {
// 1. Set each epoch value to 0 if it’s null/None.
if l1.Epoch == "" {
l1.Epoch = "0"
}
if l2.Epoch == "" {
l2.Epoch = "0"
}
// 2. Compare the epoch values using compare_values(). If they’re not equal,
// return that result, else move on to the next portion (version). The logic
// within compare_values() is that if one is empty/null and the other is not,
// the non-empty one is greater, and that ends the comparison. If neither of
// them is empty/not present, compare them using rpmvercmp() and follow the same
// logic; if one is “greater” (newer) than the other, that’s the end result of
// the comparison. Otherwise, move on to the next component (version).
if c := versionCompare(l1.Epoch, l2.Epoch); c != 0 {
return c
}
// 3. Compare the versions using the same logic.
if c := versionCompare(l1.Version, l2.Version); c != 0 {
return c
}
// 4. Compare the releases using the same logic.
if c := versionCompare(l1.Release, l2.Release); c != 0 {
return c
}
// 5. If all of the components are “equal”, the packages are the same.
return 0
}
func versionCompare(v1, v2 string) int {
// 1. If the strings are binary equal (a == b), they’re equal, return 0.
if v1 == v2 {
return 0
}
// 2. Loop over the strings, left-to-right.
for {
// 1. Trim anything that’s not [A-Za-z0-9] or tilde (~) from the front of both strings.
v1 = strings.TrimLeftFunc(v1, isntAlnumOrTilde)
v2 = strings.TrimLeftFunc(v2, isntAlnumOrTilde)
v1StartsWithTilde := len(v1) > 0 && v1[0] == '~'
v2StartsWithTilde := len(v2) > 0 && v2[0] == '~'
if v1StartsWithTilde && v2StartsWithTilde {
// 2. If both strings start with a tilde, discard it and move on to the next character.
v1, v2 = v1[1:], v2[1:]
continue
} else if v1StartsWithTilde {
// 3.a If string a starts with a tilde and string b does not, return -1 (string a is older);
return -1
} else if v2StartsWithTilde {
// 3.b and the inverse if string b starts with a tilde and string a does not.
return 1
}
// neither v1 nor v2 start with tilde
// they start with a letter or digit, or empty
if len(v1) == 0 || len(v2) == 0 {
// 4. End the loop if either string has reached zero length.
break
}
// 5. If the first character of a is a digit, pop the leading chunk of continuous digits from each
// string (which may be ” for b if only one a starts with digits). If a begins with a letter, do
// the same for leading letters.
var isNumeric bool
var segFunc func(rune) bool
if unicode.IsDigit(rune(v1[0])) {
isNumeric = true
segFunc = unicode.IsDigit
} else {
isNumeric = false
segFunc = unicode.IsLetter
}
var v1Seg, v2Seg string
v1Seg, v1 = takeWhile(v1, segFunc) // seg will always have len > 0
v2Seg, v2 = takeWhile(v2, segFunc)
// 6. If the segement from b had 0 length, return 1 if the segment from a was numeric, or -1 if it
// was alphabetic. The logical result of this is that if a begins with numbers and b does not, a is
// newer (return 1). If a begins with letters and b does not, then a is older (return -1). If the
// leading character(s) from a and b were both numbers or both letters, continue on.
if len(v2Seg) == 0 {
if isNumeric {
return 1
}
return -1
}
// here we know that they both start with either numbers or letters
if isNumeric {
// 7. If the leading segments were both numeric, discard any leading zeros and whichever one is
// longer wins. If a is longer than b (without leading zeroes), return 1, and vice-versa. If
// they’re of the same length, continue on.
v1Seg = strings.TrimLeftFunc(v1Seg, isZero)
v2Seg = strings.TrimLeftFunc(v2Seg, isZero)
if c := compareLen(v1Seg, v2Seg); c != 0 {
return c
}
}
// 8. Compare the leading segments with strcmp() (or <=> in Ruby). If that returns a non-zero value,
// then return that value. Else continue to the next iteration of the loop.
switch {
case v1Seg < v2Seg:
return -1
case v1Seg > v2Seg:
return 1
}
}
// 3. If the loop ended (nothing has been returned yet, either both strings are
// totally the same or they’re the same up to the end of one of them, like with
// “1.2.3” and “1.2.3b”), then the longest wins - if what’s left of a is longer
// than what’s left of b, return 1. Vice-versa for if what’s left of b is longer
// than what’s left of a. And finally, if what’s left of them is the same length,
// return 0.
return compareLen(v1, v2)
}
func compareLen(s1, s2 string) int {
diff := len(s1) - len(s2)
switch {
case diff < 0:
return -1
case diff > 0:
return 1
default:
return 0
}
}
func isntAlnumOrTilde(r rune) bool {
return !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '~')
}
func isZero(r rune) bool {
return r == '0'
}
func takeWhile(s string, f func(rune) bool) (matched, rest string) {
var i int
for i = 0; i < len(s) && f(rune(s[i])); i++ {
}
return s[:i], s[i:]
}