go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/core/resources/versions/rpm/version.go (about) 1 // Copyright 2017 clair authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package rpm implements a versionfmt.Parser for version numbers used in rpm 16 // based software packages. 17 package rpm 18 19 import ( 20 "errors" 21 "math" 22 "regexp" 23 "strconv" 24 "strings" 25 "unicode" 26 ) 27 28 const ( 29 // MinVersion is a special package version which is always sorted first. 30 MinVersion = "#MINV#" 31 32 // MaxVersion is a special package version which is always sorted last. 33 MaxVersion = "#MAXV#" 34 ) 35 36 // ParserName is the name by which the rpm parser is registered. 37 const ParserName = "rpm" 38 39 var ( 40 // alphanumPattern is a regular expression to match all sequences of numeric 41 // characters or alphanumeric characters. 42 alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)") 43 allowedSymbols = []rune{'.', '-', '+', '~', ':', '_'} 44 ) 45 46 type version struct { 47 epoch int 48 version string 49 release string 50 } 51 52 var ( 53 minVersion = version{version: MinVersion} 54 maxVersion = version{version: MaxVersion} 55 ) 56 57 // newVersion parses a string into a version type which can be compared. 58 func newVersion(str string) (version, error) { 59 var v version 60 61 // Trim leading and trailing space 62 str = strings.TrimSpace(str) 63 64 if len(str) == 0 { 65 return version{}, errors.New("Version string is empty") 66 } 67 68 // Max/Min versions 69 if str == maxVersion.String() { 70 return maxVersion, nil 71 } 72 if str == minVersion.String() { 73 return minVersion, nil 74 } 75 76 // Find epoch 77 sepepoch := strings.Index(str, ":") 78 if sepepoch > -1 { 79 intepoch, err := strconv.Atoi(str[:sepepoch]) 80 if err == nil { 81 v.epoch = intepoch 82 } else { 83 return version{}, errors.New("epoch in version is not a number") 84 } 85 if intepoch < 0 { 86 return version{}, errors.New("epoch in version is negative") 87 } 88 } else { 89 v.epoch = 0 90 } 91 92 // Find version / release 93 seprevision := strings.Index(str, "-") 94 if seprevision > -1 { 95 v.version = str[sepepoch+1 : seprevision] 96 v.release = str[seprevision+1:] 97 } else { 98 v.version = str[sepepoch+1:] 99 v.release = "" 100 } 101 // Verify format 102 if len(v.version) == 0 { 103 return version{}, errors.New("No version") 104 } 105 106 for i := 0; i < len(v.version); i = i + 1 { 107 r := rune(v.version[i]) 108 if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) { 109 return version{}, errors.New("invalid character in version") 110 } 111 } 112 113 for i := 0; i < len(v.release); i = i + 1 { 114 r := rune(v.release[i]) 115 if !unicode.IsDigit(r) && !unicode.IsLetter(r) && !validSymbol(r) { 116 return version{}, errors.New("invalid character in revision") 117 } 118 } 119 120 return v, nil 121 } 122 123 type Parser struct{} 124 125 func (p Parser) Valid(str string) bool { 126 _, err := newVersion(str) 127 return err == nil 128 } 129 130 func (p Parser) InRange(versionA, rangeB string) (bool, error) { 131 cmp, err := p.Compare(versionA, rangeB) 132 if err != nil { 133 return false, err 134 } 135 return cmp < 0, nil 136 } 137 138 func (p Parser) GetFixedIn(fixedIn string) (string, error) { 139 // In the old version format parser design, the string to determine fixed in 140 // version is the fixed in version. 141 return fixedIn, nil 142 } 143 144 func (p Parser) Compare(a, b string) (int, error) { 145 v1, err := newVersion(a) 146 if err != nil { 147 return 0, err 148 } 149 150 v2, err := newVersion(b) 151 if err != nil { 152 return 0, err 153 } 154 155 // Quick check 156 if v1 == v2 { 157 return 0, nil 158 } 159 160 // Max/Min comparison 161 if v1 == minVersion || v2 == maxVersion { 162 return -1, nil 163 } 164 if v2 == minVersion || v1 == maxVersion { 165 return 1, nil 166 } 167 168 // Compare epochs 169 if v1.epoch > v2.epoch { 170 return 1, nil 171 } 172 if v1.epoch < v2.epoch { 173 return -1, nil 174 } 175 176 // Compare version 177 rc := rpmvercmp(v1.version, v2.version) 178 if rc != 0 { 179 return rc, nil 180 } 181 182 // Compare revision 183 return rpmvercmp(v1.release, v2.release), nil 184 } 185 186 // rpmcmpver compares two version or release strings. 187 // 188 // Lifted from github.com/cavaliercoder/go-rpm. 189 // For the original C implementation, see: 190 // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16 191 func rpmvercmp(strA, strB string) int { 192 // shortcut for equality 193 if strA == strB { 194 return 0 195 } 196 197 // get alpha/numeric segements 198 segsa := alphanumPattern.FindAllString(strA, -1) 199 segsb := alphanumPattern.FindAllString(strB, -1) 200 segs := int(math.Min(float64(len(segsa)), float64(len(segsb)))) 201 202 // compare each segment 203 for i := 0; i < segs; i++ { 204 a := segsa[i] 205 b := segsb[i] 206 207 // compare tildes 208 if []rune(a)[0] == '~' && []rune(b)[0] == '~' { 209 continue 210 } 211 if []rune(a)[0] == '~' && []rune(b)[0] != '~' { 212 return -1 213 } 214 if []rune(a)[0] != '~' && []rune(b)[0] == '~' { 215 return 1 216 } 217 218 if unicode.IsNumber([]rune(a)[0]) { 219 // numbers are always greater than alphas 220 if !unicode.IsNumber([]rune(b)[0]) { 221 // a is numeric, b is alpha 222 return 1 223 } 224 225 // trim leading zeros 226 a = strings.TrimLeft(a, "0") 227 b = strings.TrimLeft(b, "0") 228 229 // longest string wins without further comparison 230 if len(a) > len(b) { 231 return 1 232 } else if len(b) > len(a) { 233 return -1 234 } 235 } else if unicode.IsNumber([]rune(b)[0]) { 236 // a is alpha, b is numeric 237 return -1 238 } 239 240 // string compare 241 if a < b { 242 return -1 243 } else if a > b { 244 return 1 245 } 246 } 247 248 // segments were all the same but separators must have been different 249 if len(segsa) == len(segsb) { 250 return 0 251 } 252 253 // If there is a tilde in a segment past the min number of segments, find it. 254 if len(segsa) > segs && []rune(segsa[segs])[0] == '~' { 255 return -1 256 } else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' { 257 return 1 258 } 259 260 // whoever has the most segments wins 261 if len(segsa) > len(segsb) { 262 return 1 263 } 264 265 return -1 266 } 267 268 // String returns the string representation of a Version. 269 func (v version) String() (s string) { 270 if v.epoch != 0 { 271 s = strconv.Itoa(v.epoch) + ":" 272 } 273 s += v.version 274 if v.release != "" { 275 s += "-" + v.release 276 } 277 return 278 } 279 280 func validSymbol(r rune) bool { 281 return containsRune(allowedSymbols, r) 282 } 283 284 func containsRune(s []rune, e rune) bool { 285 for _, a := range s { 286 if a == e { 287 return true 288 } 289 } 290 return false 291 }