github.com/google/osv-scalibr@v0.4.1/semantic/version-debian.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 "strings" 20 ) 21 22 func splitAround(s string, sep string, reverse bool) (string, string) { 23 var i int 24 25 if reverse { 26 i = strings.LastIndex(s, sep) 27 } else { 28 i = strings.Index(s, sep) 29 } 30 31 if i == -1 { 32 return s, "" 33 } 34 35 return s[:i], s[i+1:] 36 } 37 38 func splitDebianDigitPrefix(str string) (*big.Int, string, error) { 39 // find the index of the first non-digit in the string, which is the end of the prefix 40 i := strings.IndexFunc(str, func(c rune) bool { 41 return c < 48 || c > 57 42 }) 43 44 if i == 0 || str == "" { 45 return big.NewInt(0), str, nil 46 } 47 48 if i == -1 { 49 i = len(str) 50 } 51 52 digit, err := convertToBigInt(str[:i]) 53 54 if err != nil { 55 return nil, "", err 56 } 57 58 return digit, str[i:], nil 59 } 60 61 func splitDebianNonDigitPrefix(str string) (string, string) { 62 // find the index of the first digit in the string, which is the end of the prefix 63 i := strings.IndexAny(str, "0123456789") 64 65 if i == 0 || str == "" { 66 return "", str 67 } 68 69 if i == -1 { 70 i = len(str) 71 } 72 73 return str[:i], str[i:] 74 } 75 76 func weighDebianChar(char string) int { 77 // tilde and empty take precedent 78 if char == "~" { 79 return 1 80 } 81 if char == "" { 82 return 2 83 } 84 85 c := int(char[0]) 86 87 // all the letters sort earlier than all the non-letters 88 if c < 65 || (c > 90 && c < 97) || c > 122 { 89 c += 122 90 } 91 92 return c 93 } 94 95 func compareDebianVersions(a, b string) (int, error) { 96 var ap, bp string 97 var adp, bdp *big.Int 98 var err error 99 100 // based off: https://man7.org/linux/man-pages/man7/deb-version.7.html 101 for a != "" || b != "" { 102 ap, a = splitDebianNonDigitPrefix(a) 103 bp, b = splitDebianNonDigitPrefix(b) 104 105 // First the initial part of each string consisting entirely of 106 // non-digit characters is determined... 107 if ap != bp { 108 apSplit := strings.Split(ap, "") 109 bpSplit := strings.Split(bp, "") 110 111 for i := range max(len(ap), len(bp)) { 112 aw := weighDebianChar(fetch(apSplit, i, "")) 113 bw := weighDebianChar(fetch(bpSplit, i, "")) 114 115 if aw < bw { 116 return -1, nil 117 } 118 if aw > bw { 119 return +1, nil 120 } 121 } 122 } 123 124 // Then the initial part of the remainder of each string which 125 // consists entirely of digit characters is determined.... 126 adp, a, err = splitDebianDigitPrefix(a) 127 128 if err != nil { 129 return 0, err 130 } 131 132 bdp, b, err = splitDebianDigitPrefix(b) 133 134 if err != nil { 135 return 0, err 136 } 137 138 if diff := adp.Cmp(bdp); diff != 0 { 139 return diff, nil 140 } 141 } 142 143 return 0, nil 144 } 145 146 type debianVersion struct { 147 epoch *big.Int 148 upstream string 149 revision string 150 } 151 152 func (v debianVersion) compare(w debianVersion) (int, error) { 153 if diff := v.epoch.Cmp(w.epoch); diff != 0 { 154 return diff, nil 155 } 156 if diff, err := compareDebianVersions(v.upstream, w.upstream); diff != 0 || err != nil { 157 if err != nil { 158 return 0, err 159 } 160 161 return diff, nil 162 } 163 if diff, err := compareDebianVersions(v.revision, w.revision); diff != 0 || err != nil { 164 if err != nil { 165 return 0, err 166 } 167 168 return diff, nil 169 } 170 171 return 0, nil 172 } 173 174 func (v debianVersion) CompareStr(str string) (int, error) { 175 w, err := parseDebianVersion(str) 176 177 if err != nil { 178 return 0, err 179 } 180 181 return v.compare(w) 182 } 183 184 func parseDebianVersion(str string) (debianVersion, error) { 185 var upstream, revision string 186 187 str = strings.TrimSpace(str) 188 epoch := big.NewInt(0) 189 190 if strings.Contains(str, ":") { 191 var e string 192 var err error 193 e, str = splitAround(str, ":", false) 194 195 if epoch, err = convertToBigInt(e); err != nil { 196 return debianVersion{}, err 197 } 198 } 199 200 if strings.Contains(str, "-") { 201 upstream, revision = splitAround(str, "-", true) 202 } else { 203 upstream = str 204 revision = "0" 205 } 206 207 return debianVersion{epoch, upstream, revision}, nil 208 }