github.com/google/osv-scalibr@v0.4.1/semantic/version-packagist.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 "regexp" 19 "strconv" 20 "strings" 21 ) 22 23 var ( 24 packagistVersionSeperatorFinder = regexp.MustCompile(`[-_+]`) 25 packagistNonDigitToDigitFinder = regexp.MustCompile(`([^\d.])(\d)`) 26 packagistDigitToNonDigitFinder = regexp.MustCompile(`(\d)([^\d.])`) 27 ) 28 29 func canonicalizePackagistVersion(v string) string { 30 // todo: decide how to handle this - without it, we're 1:1 with the native 31 // PHP version_compare function, but composer removes it; arguably this 32 // should be done before the version is passed in (by the dev), except 33 // the ecosystem is named "Packagist" not "php version_compare", though 34 // packagist itself doesn't seem to enforce this (its composer that does 35 // the trimming...) 36 v = strings.TrimPrefix(strings.TrimPrefix(v, "v"), "V") 37 38 v = packagistVersionSeperatorFinder.ReplaceAllString(v, ".") 39 v = packagistNonDigitToDigitFinder.ReplaceAllString(v, "$1.$2") 40 v = packagistDigitToNonDigitFinder.ReplaceAllString(v, "$1.$2") 41 42 return v 43 } 44 45 func weighPackagistBuildCharacter(str string) int { 46 if strings.HasPrefix(str, "RC") { 47 return 3 48 } 49 50 specials := []string{"dev", "a", "b", "rc", "#", "p"} 51 52 for i, special := range specials { 53 if strings.HasPrefix(str, special) { 54 return i 55 } 56 } 57 58 return 0 59 } 60 61 func comparePackagistSpecialVersions(a, b string) int { 62 av := weighPackagistBuildCharacter(a) 63 bv := weighPackagistBuildCharacter(b) 64 65 if av > bv { 66 return 1 67 } else if av < bv { 68 return -1 69 } 70 71 return 0 72 } 73 74 func comparePackagistComponents(a, b []string) int { 75 minLength := min(len(a), len(b)) 76 77 var compare int 78 79 for i := range minLength { 80 ai, aErr := convertToBigInt(a[i]) 81 bi, bErr := convertToBigInt(b[i]) 82 83 switch { 84 case aErr == nil && bErr == nil: 85 compare = ai.Cmp(bi) 86 case aErr != nil && bErr != nil: 87 compare = comparePackagistSpecialVersions(a[i], b[i]) 88 case aErr == nil: 89 compare = comparePackagistSpecialVersions("#", b[i]) 90 default: 91 compare = comparePackagistSpecialVersions(a[i], "#") 92 } 93 94 if compare != 0 { 95 if compare > 0 { 96 return 1 97 } 98 99 return -1 100 } 101 } 102 103 if len(a) > len(b) { 104 next := a[len(b)] 105 106 if _, err := strconv.Atoi(next); err == nil { 107 return 1 108 } 109 110 return comparePackagistComponents(a[len(b):], []string{"#"}) 111 } 112 113 if len(a) < len(b) { 114 next := b[len(a)] 115 116 if _, err := strconv.Atoi(next); err == nil { 117 return -1 118 } 119 120 return comparePackagistComponents([]string{"#"}, b[len(a):]) 121 } 122 123 return 0 124 } 125 126 type packagistVersion struct { 127 Original string 128 Components []string 129 } 130 131 func parsePackagistVersion(str string) packagistVersion { 132 return packagistVersion{ 133 str, 134 strings.Split(canonicalizePackagistVersion(str), "."), 135 } 136 } 137 138 func (v packagistVersion) compare(w packagistVersion) int { 139 return comparePackagistComponents(v.Components, w.Components) 140 } 141 142 func (v packagistVersion) CompareStr(str string) (int, error) { 143 return v.compare(parsePackagistVersion(str)), nil 144 }