github.com/quay/claircore@v1.5.28/ruby/version.go (about) 1 package ruby 2 3 import ( 4 "errors" 5 "regexp" 6 "strings" 7 ) 8 9 var ( 10 anchoredVersion = regexp.MustCompile(`^\s*([0-9]+(\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?)?\s*$`) 11 12 errInvalidVersion = errors.New("invalid gem version") 13 ) 14 15 // Version is a RubyGem version. 16 // This is based on the official [implementation]. 17 // 18 // [implementation]: https://github.com/rubygems/rubygems/blob/1a1948424aca90013b37cb8a78f5d1d5576023f1/lib/rubygems/version.rb 19 type Version struct { 20 segs []Segment 21 } 22 23 // NewVersion creates a Version out of the given string. 24 func NewVersion(version string) (Version, error) { 25 if !anchoredVersion.MatchString(version) { 26 return Version{}, errInvalidVersion 27 } 28 29 version = strings.TrimSpace(version) 30 if version == "" { 31 version = "0" 32 } 33 version = strings.ReplaceAll(version, "-", ".pre.") 34 35 return Version{ 36 segs: canonicalize(version), 37 }, nil 38 } 39 40 // Compare returns an integer comparing this version, v, to other. 41 // The result will be 0 if v == other, -1 if v < other, and +1 if v > other. 42 func (v Version) Compare(other Version) int { 43 segs := v.segs 44 otherSegs := other.segs 45 46 leftLen := len(segs) 47 rightLen := len(otherSegs) 48 limit := max(leftLen, rightLen) 49 for i := 0; i < limit; i++ { 50 left, right := Segment(numericSegment("0")), Segment(numericSegment("0")) 51 if i < leftLen { 52 left = segs[i] 53 } 54 if i < rightLen { 55 right = otherSegs[i] 56 } 57 58 if cmp := left.Compare(right); cmp != 0 { 59 return cmp 60 } 61 } 62 63 return 0 64 } 65 66 func canonicalize(v string) []Segment { 67 segs, prerelease := partitionSegments(v) 68 69 // Remove trailing zero segments. 70 i := len(segs) - 1 71 for ; i >= 0; i-- { 72 seg, ok := segs[i].(numericSegment) 73 if !ok || !seg.isZero() { 74 break 75 } 76 } 77 segs = segs[:i+1] 78 79 // Remove all zero segments preceding the first letter in a prerelease version. 80 if prerelease { 81 // Find the first letter in the version. 82 end := -1 83 for i := 0; i < len(segs); i++ { 84 if _, ok := segs[i].(stringSegment); ok { 85 end = i 86 break 87 } 88 } 89 if end != -1 { 90 // Find where the preceding zeroes start. 91 var start int 92 for i := end - 1; i >= 0; i-- { 93 seg, ok := segs[i].(numericSegment) 94 if !ok || !seg.isZero() { 95 start = i + 1 96 break 97 } 98 } 99 segs = append(segs[:start], segs[end:]...) 100 } 101 } 102 103 return segs 104 } 105 106 func partitionSegments(v string) ([]Segment, bool) { 107 var prerelease bool 108 splitVersion := strings.Split(v, ".") 109 segs := make([]Segment, 0, len(splitVersion)) 110 for _, s := range splitVersion { 111 if s == "" { 112 continue 113 } 114 115 if onlyDigits(s) { 116 segs = append(segs, numericSegment(s)) 117 continue 118 } 119 120 // Ruby considers any version with a letter to be a prerelease. 121 prerelease = true 122 segs = append(segs, stringSegment(s)) 123 } 124 125 return segs, prerelease 126 } 127 128 func onlyDigits(s string) bool { 129 // I don't know if converting to a []byte does anything 130 // special here, but [strconv.ParseUint] does it when ranging over a string, 131 // and this implementation is based on code from [strconv.ParseUint]. 132 for _, c := range []byte(s) { 133 if c < '0' || c > '9' { 134 return false 135 } 136 } 137 return true 138 } 139 140 func max(a, b int) int { 141 if b > a { 142 return b 143 } 144 return a 145 } 146 147 // Segment is a part of a RubyGem version. 148 type Segment interface { 149 Compare(other Segment) int 150 } 151 152 var ( 153 _ Segment = (*stringSegment)(nil) 154 _ Segment = (*numericSegment)(nil) 155 ) 156 157 type stringSegment string 158 159 // Compare returns an integer comparing this version, s, to other. 160 // The result will be 0 if v == other, -1 if v < other, and +1 if v > other. 161 // 162 // A stringSegment is always less than a numericSegment. 163 func (s stringSegment) Compare(other Segment) int { 164 switch seg := other.(type) { 165 case numericSegment: 166 return -1 167 case stringSegment: 168 return strings.Compare(string(s), string(seg)) 169 default: 170 panic("Programmer error") 171 } 172 } 173 174 type numericSegment string 175 176 // Compare returns an integer comparing this version, n, to other. 177 // The result will be 0 if n == other, -1 if n < other, and +1 if n > other. 178 // 179 // A numericSegment is always greater than a stringSegment. 180 func (n numericSegment) Compare(other Segment) int { 181 switch seg := other.(type) { 182 case stringSegment: 183 return +1 184 case numericSegment: 185 left, leftLen := string(n), len(n) 186 right, rightLen := string(seg), len(seg) 187 // The length of each string must match to compare them properly. 188 // Pad the shorter string with zeroes. 189 if leftLen == max(leftLen, rightLen) { 190 right = strings.Repeat("0", leftLen-rightLen) + right 191 } else { 192 left = strings.Repeat("0", rightLen-leftLen) + left 193 } 194 return strings.Compare(left, right) 195 default: 196 panic("Programmer error") 197 } 198 } 199 200 func (n numericSegment) isZero() bool { 201 // Again, I don't know if converting to a []byte does anything 202 // special here, but [strconv.ParseUint] does it when ranging over a string, 203 // and this implementation is based on code from [strconv.ParseUint]. 204 for _, c := range []byte(n) { 205 if c != '0' { 206 return false 207 } 208 } 209 return true 210 }