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:] }