-
Notifications
You must be signed in to change notification settings - Fork 513
/
rpm_version.go
218 lines (184 loc) · 5.23 KB
/
rpm_version.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package version
import (
"fmt"
"math"
"reflect"
"regexp"
"strconv"
"strings"
"unicode"
)
type rpmVersion struct {
epoch *int
version string
release string
}
func newRpmVersion(raw string) (rpmVersion, error) {
epoch, remainingVersion, err := splitEpochFromVersion(raw)
if err != nil {
return rpmVersion{}, err
}
fields := strings.SplitN(remainingVersion, "-", 2)
version := fields[0]
var release string
if len(fields) > 1 {
// there is a release
release = fields[1]
}
return rpmVersion{
epoch: epoch,
version: version,
release: release,
}, nil
}
func splitEpochFromVersion(rawVersion string) (*int, string, error) {
fields := strings.SplitN(rawVersion, ":", 2)
// When the epoch is not included, should be considered to be 0 during
// comparisons (see https://github.com/rpm-software-management/rpm/issues/450).
// But, often the inclusion of the epoch in vuln databases or source RPM
// filenames is not consistent so, represent a missing epoch as nil. This allows
// the comparison logic itself to determine if it should use a zero or another
// value which supports more flexible comparison options because the version
// creation is not lossy
if len(fields) == 1 {
return nil, rawVersion, nil
}
// there is an epoch
epochStr := strings.TrimLeft(fields[0], " ")
epoch, err := strconv.Atoi(epochStr)
if err != nil {
return nil, "", fmt.Errorf("unable to parse epoch (%s): %w", epochStr, err)
}
return &epoch, fields[1], nil
}
func (v *rpmVersion) Compare(other *Version) (int, error) {
if other.Format != RpmFormat {
return -1, fmt.Errorf("unable to compare rpm to given format: %s", other.Format)
}
if other.rich.rpmVer == nil {
return -1, fmt.Errorf("given empty rpmVersion object")
}
return other.rich.rpmVer.compare(*v), nil
}
// Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2.
// This a pragmatic adaptation of comparison for the messy data
// encountered in vuln scanning. If epochs are NOT present and explicit
// (e.g. >= 0) in both versions then they are ignored for the comparison.
// For a rpm spec-compliant comparison, see strictCompare() instead
func (v rpmVersion) compare(v2 rpmVersion) int {
if reflect.DeepEqual(v, v2) {
return 0
}
// Only compare epochs if both are present and explicit. This is technically
// against what RedHat says to do with missing epoch (which is to assume a 0 epoch).
// However, since we may be dealing with upstream data sources where there is an epoch
// for a package but the value was stripped, the best we can do is to compare only the
// version values without the epoch values.
if epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) {
epochResult := compareEpochs(*v.epoch, *v2.epoch)
if epochResult != 0 {
return epochResult
}
}
ret := compareRpmVersions(v.version, v2.version)
if ret != 0 {
return ret
}
return compareRpmVersions(v.release, v2.release)
}
func epochIsPresent(epoch *int) bool {
return epoch != nil
}
// Epoch comparison, standard int comparison for sorting
func compareEpochs(e1 int, e2 int) int {
switch {
case e1 > e2:
return 1
case e1 < e2:
return -1
default:
return 0
}
}
func (v rpmVersion) String() string {
version := ""
if v.epoch != nil {
version += fmt.Sprintf("%d:", *v.epoch)
}
version += v.version
if v.release != "" {
version += fmt.Sprintf("-%s", v.release)
}
return version
}
// compareRpmVersions compares two version or release strings without the epoch.
// Source: https://github.com/cavaliercoder/go-rpm/blob/master/version.go
//
// For the original C implementation, see:
// https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16
var alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)")
//nolint:funlen,gocognit
func compareRpmVersions(a, b string) int {
// shortcut for equality
if a == b {
return 0
}
// get alpha/numeric segments
segsa := alphanumPattern.FindAllString(a, -1)
segsb := alphanumPattern.FindAllString(b, -1)
segs := int(math.Min(float64(len(segsa)), float64(len(segsb))))
// compare each segment
for i := 0; i < segs; i++ {
a := segsa[i]
b := segsb[i]
// compare tildes
if []rune(a)[0] == '~' || []rune(b)[0] == '~' {
if []rune(a)[0] != '~' {
return 1
}
if []rune(b)[0] != '~' {
return -1
}
}
if unicode.IsNumber([]rune(a)[0]) {
// numbers are always greater than alphas
if !unicode.IsNumber([]rune(b)[0]) {
// a is numeric, b is alpha
return 1
}
// trim leading zeros
a = strings.TrimLeft(a, "0")
b = strings.TrimLeft(b, "0")
// longest string wins without further comparison
if len(a) > len(b) {
return 1
} else if len(b) > len(a) {
return -1
}
} else if unicode.IsNumber([]rune(b)[0]) {
// a is alpha, b is numeric
return -1
}
// string compare
if a < b {
return -1
} else if a > b {
return 1
}
}
// segments were all the same but separators must have been different
if len(segsa) == len(segsb) {
return 0
}
// If there is a tilde in a segment past the min number of segments, find it.
if len(segsa) > segs && []rune(segsa[segs])[0] == '~' {
return -1
} else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' {
return 1
}
// whoever has the most segments wins
if len(segsa) > len(segsb) {
return 1
}
return -1
}