github.com/google/osv-scalibr@v0.4.1/semantic/version-alpine.go (about) 1 // Copyright 2025 Google LLC 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 semantic 16 17 import ( 18 "math/big" 19 "regexp" 20 "strings" 21 ) 22 23 var ( 24 alpineNumberComponentsFinder = regexp.MustCompile(`^((\d+)\.?)*`) 25 alpineIsFirstCharLowercaseLetter = regexp.MustCompile(`^[a-z]`) 26 alpineSuffixesFinder = regexp.MustCompile(`_(alpha|beta|pre|rc|cvs|svn|git|hg|p)(\d*)`) 27 alpineHashFinder = regexp.MustCompile(`^~([0-9a-f]+)`) 28 alpineBuildComponentFinder = regexp.MustCompile(`^-r(\d*)`) 29 ) 30 31 type alpineNumberComponent struct { 32 original string 33 value *big.Int 34 index int 35 } 36 37 func (anc alpineNumberComponent) Cmp(b alpineNumberComponent) int { 38 // ignore trailing zeros for the first digits in each version 39 if anc.index != 0 && b.index != 0 { 40 if anc.original[0] == '0' || b.original[0] == '0' { 41 return strings.Compare(anc.original, b.original) 42 } 43 } 44 45 return anc.value.Cmp(b.value) 46 } 47 48 type alpineNumberComponents []alpineNumberComponent 49 50 func (components *alpineNumberComponents) Fetch(n int) alpineNumberComponent { 51 if len(*components) <= n { 52 return alpineNumberComponent{original: "0", value: new(big.Int)} 53 } 54 55 return (*components)[n] 56 } 57 58 type alpineSuffix struct { 59 // the weight of this suffix for sorting, and implicitly what actual string it is: 60 // *alpha*, *beta*, *pre*, *rc*, <no suffix>, *cvs*, *svn*, *git*, *hg*, *p* 61 weight int 62 // the number value of this suffix component 63 number *big.Int 64 } 65 66 // weights the given suffix string based on the sort order of official supported suffixes. 67 // 68 // this is expected to be _just_ the suffix "string" i.e. it should not start with a "_" 69 // or have any trailing numbers. 70 func weightAlpineSuffixString(suffixStr string) int { 71 // "p" is omitted since it's the highest suffix, so it will be the final return 72 supported := []string{"alpha", "beta", "pre", "rc", "", "cvs", "svn", "git", "hg"} 73 74 for i, s := range supported { 75 if suffixStr == s { 76 return i 77 } 78 } 79 80 // if we didn't match a support suffix already, then we're "p" which 81 // has the highest weight as our parser only captures valid suffixes 82 return len(supported) 83 } 84 85 // alpineVersion represents a version of an Alpine package. 86 // 87 // Currently, the APK version specification is as follows: 88 // *number{.number}...{letter}{\_suffix{number}}...{~hash}{-r#}* 89 // 90 // Each *number* component is a sequence of digits (0-9). 91 // 92 // The *letter* portion can follow only after end of all the numeric 93 // version components. The *letter* is a single lower case letter (a-z). 94 // This can follow one or more *\_suffix{number}* components. The list 95 // of valid suffixes (and their sorting order) is: 96 // *alpha*, *beta*, *pre*, *rc*, <no suffix>, *cvs*, *svn*, *git*, *hg*, *p* 97 // 98 // This can be follows with an optional *{~hash}* to indicate a commit 99 // hash from where it was built. This can be any length string of 100 // lower case hexadecimal digits (0-9a-f). 101 // 102 // Finally, an optional package build component *-r{number}* can follow. 103 // 104 // Also see https://github.com/alpinelinux/apk-tools/blob/master/doc/apk-package.5.scd#package-info-metadata 105 type alpineVersion struct { 106 // the original string that was parsed 107 original string 108 // whether the version was found to be invalid while parsing 109 invalid bool 110 // the remainder of the string after parsing has been completed 111 remainder string 112 // slice of number components which can be compared in a semver-like manner 113 components alpineNumberComponents 114 // optional single lower-case letter 115 letter string 116 // slice of one or more suffixes, prefixed with "_" and optionally followed by a number. 117 // 118 // supported suffixes and their sort order are: 119 // *alpha*, *beta*, *pre*, *rc*, <no suffix>, *cvs*, *svn*, *git*, *hg*, *p* 120 suffixes []alpineSuffix 121 // optional commit hash made up of any number of lower case hexadecimal digits (0-9a-f) 122 hash string 123 // prefixed with "-r{number}" 124 buildComponent *big.Int 125 } 126 127 func (v alpineVersion) compareComponents(w alpineVersion) int { 128 numberOfComponents := max(len(v.components), len(w.components)) 129 130 for i := range numberOfComponents { 131 diff := v.components.Fetch(i).Cmp(w.components.Fetch(i)) 132 133 if diff != 0 { 134 return diff 135 } 136 } 137 138 return 0 139 } 140 141 func (v alpineVersion) compareLetters(w alpineVersion) int { 142 if v.letter == "" && w.letter != "" { 143 return -1 144 } 145 if v.letter != "" && w.letter == "" { 146 return +1 147 } 148 149 return strings.Compare(v.letter, w.letter) 150 } 151 152 func (v alpineVersion) fetchSuffix(n int) alpineSuffix { 153 if len(v.suffixes) <= n { 154 return alpineSuffix{number: big.NewInt(0), weight: 5} 155 } 156 157 return v.suffixes[n] 158 } 159 160 func (as alpineSuffix) Cmp(bs alpineSuffix) int { 161 if as.weight > bs.weight { 162 return +1 163 } 164 if as.weight < bs.weight { 165 return -1 166 } 167 168 return as.number.Cmp(bs.number) 169 } 170 171 func (v alpineVersion) compareSuffixes(w alpineVersion) int { 172 numberOfSuffixes := max(len(v.suffixes), len(w.suffixes)) 173 174 for i := range numberOfSuffixes { 175 diff := v.fetchSuffix(i).Cmp(w.fetchSuffix(i)) 176 177 if diff != 0 { 178 return diff 179 } 180 } 181 182 return 0 183 } 184 185 func (v alpineVersion) compareBuildComponents(w alpineVersion) int { 186 if v.buildComponent != nil && w.buildComponent != nil { 187 if diff := v.buildComponent.Cmp(w.buildComponent); diff != 0 { 188 return diff 189 } 190 } 191 192 return 0 193 } 194 195 func (v alpineVersion) compareRemainder(w alpineVersion) int { 196 if v.remainder == "" && w.remainder != "" { 197 return +1 198 } 199 200 if v.remainder != "" && w.remainder == "" { 201 return -1 202 } 203 204 return 0 205 } 206 207 func (v alpineVersion) compare(w alpineVersion) int { 208 // if both versions are invalid, then just use a string compare 209 if v.invalid && w.invalid { 210 return strings.Compare(v.original, w.original) 211 } 212 213 // note: commit hashes are ignored as we can't properly compare them 214 if diff := v.compareComponents(w); diff != 0 { 215 return diff 216 } 217 if diff := v.compareLetters(w); diff != 0 { 218 return diff 219 } 220 if diff := v.compareSuffixes(w); diff != 0 { 221 return diff 222 } 223 if diff := v.compareBuildComponents(w); diff != 0 { 224 return diff 225 } 226 if diff := v.compareRemainder(w); diff != 0 { 227 return diff 228 } 229 230 return 0 231 } 232 233 func (v alpineVersion) CompareStr(str string) (int, error) { 234 w, err := parseAlpineVersion(str) 235 236 if err != nil { 237 return 0, err 238 } 239 240 return v.compare(w), nil 241 } 242 243 // parseAlpineNumberComponents parses the given string into alpineVersion.components 244 // and then returns the remainder of the string for continued parsing. 245 // 246 // Each number component is a sequence of digits (0-9), separated with a ".", 247 // and with no limit on the value or amount of number components. 248 // 249 // This parser must be applied *before* any other parser. 250 func parseAlpineNumberComponents(v *alpineVersion, str string) (string, error) { 251 sub := alpineNumberComponentsFinder.FindString(str) 252 253 if sub == "" { 254 return str, nil 255 } 256 257 for i, d := range strings.Split(sub, ".") { 258 // while technically not allowed by the spec, currently apk does not 259 // consider it invalid to have a dot that isn't followed by a digit 260 if d == "" { 261 break 262 } 263 264 value, err := convertToBigInt(d) 265 266 if err != nil { 267 return "", err 268 } 269 270 v.components = append(v.components, alpineNumberComponent{ 271 value: value, 272 index: i, 273 original: d, 274 }) 275 } 276 277 return strings.TrimPrefix(str, sub), nil 278 } 279 280 // parseAlpineLetter parses the given string into an alpineVersion.letter 281 // and then returns the remainder of the string for continued parsing. 282 // 283 // The letter is optional, following after the numeric version components, and 284 // must be a single lower case letter (a-z). 285 // 286 // This parser must be applied *after* parseAlpineNumberComponents. 287 func parseAlpineLetter(v *alpineVersion, str string) string { 288 if alpineIsFirstCharLowercaseLetter.MatchString(str) { 289 v.letter = str[:1] 290 } 291 292 return strings.TrimPrefix(str, v.letter) 293 } 294 295 // parseAlpineSuffixes parses the given string into alpineVersion.suffixes and 296 // then returns the remainder of the string for continued parsing. 297 // 298 // Suffixes begin with an "_" and may optionally end with a number. 299 // 300 // This parser must be applied *after* parseAlpineLetter. 301 func parseAlpineSuffixes(v *alpineVersion, str string) (string, error) { 302 for _, match := range alpineSuffixesFinder.FindAllStringSubmatch(str, -1) { 303 if match[2] == "" { 304 match[2] = "0" 305 } 306 307 number, err := convertToBigInt(match[2]) 308 309 if err != nil { 310 return "", err 311 } 312 313 v.suffixes = append(v.suffixes, alpineSuffix{ 314 weight: weightAlpineSuffixString(match[1]), 315 number: number, 316 }) 317 str = strings.TrimPrefix(str, match[0]) 318 } 319 320 return str, nil 321 } 322 323 // parseAlpineHash parses the given string into alpineVersion.hash and then returns 324 // the remainder of the string for continued parsing. 325 // 326 // The hash is an optional value representing a commit hash, which is a string of 327 // that starts with a "~" and is followed by any number of lower case hexadecimal 328 // digits (0-9a-f). 329 // 330 // This parser must be applied *after* parseAlpineSuffixes. 331 func parseAlpineHash(v *alpineVersion, str string) string { 332 v.hash = alpineHashFinder.FindString(str) 333 334 return strings.TrimPrefix(str, v.hash) 335 } 336 337 // parseAlpineBuildComponent parses the given string into alpineVersion.buildComponent 338 // and then returns the remainder of the string for continued parsing. 339 // 340 // The build component is an optional value at the end of the version string which 341 // begins with "-r" followed by a number. 342 // 343 // This parser must be applied *after* parseAlpineBuildComponent 344 func parseAlpineBuildComponent(v *alpineVersion, str string) (string, error) { 345 if str == "" { 346 return str, nil 347 } 348 349 matches := alpineBuildComponentFinder.FindStringSubmatch(str) 350 351 if matches == nil { 352 // since this is the last part of parsing, anything other than an empty string 353 // must match as a build component or otherwise the version is invalid 354 v.invalid = true 355 356 return str, nil 357 } 358 359 if matches[1] == "" { 360 matches[1] = "0" 361 } 362 363 buildComponent, err := convertToBigInt(matches[1]) 364 365 if err != nil { 366 return "", err 367 } 368 369 v.buildComponent = buildComponent 370 371 return strings.TrimPrefix(str, matches[0]), nil 372 } 373 374 func parseAlpineVersion(str string) (alpineVersion, error) { 375 var err error 376 377 v := alpineVersion{original: str, buildComponent: new(big.Int)} 378 379 if str, err = parseAlpineNumberComponents(&v, str); err != nil { 380 return alpineVersion{}, err 381 } 382 383 str = parseAlpineLetter(&v, str) 384 385 if str, err = parseAlpineSuffixes(&v, str); err != nil { 386 return alpineVersion{}, err 387 } 388 389 str = parseAlpineHash(&v, str) 390 391 if str, err = parseAlpineBuildComponent(&v, str); err != nil { 392 return alpineVersion{}, err 393 } 394 395 v.remainder = str 396 397 return v, nil 398 }