/
rpm_compare.rb
196 lines (174 loc) · 6.28 KB
/
rpm_compare.rb
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
# frozen_string_literal: true
require 'English'
module Puppet::Util::RpmCompare
ARCH_LIST = %w[
noarch i386 i686 ppc ppc64 armv3l armv4b armv4l armv4tl armv5tel
armv5tejl armv6l armv7l m68kmint s390 s390x ia64 x86_64 sh3 sh4
].freeze
ARCH_REGEX = Regexp.new(ARCH_LIST.map { |arch| "\\.#{arch}" }.join('|'))
# This is an attempt at implementing RPM's
# lib/rpmvercmp.c rpmvercmp(a, b) in Ruby.
#
# Some of the things in here look REALLY
# UGLY and/or arbitrary. Our goal is to
# match how RPM compares versions, quirks
# and all.
#
# I've kept a lot of C-like string processing
# in an effort to keep this as identical to RPM
# as possible.
#
# returns 1 if str1 is newer than str2,
# 0 if they are identical
# -1 if str1 is older than str2
def rpmvercmp(str1, str2)
return 0 if str1 == str2
front_strip_re = /^[^A-Za-z0-9~]+/
while str1.length > 0 or str2.length > 0
# trim anything that's in front_strip_re and != '~' off the beginning of each string
str1 = str1.gsub(front_strip_re, '')
str2 = str2.gsub(front_strip_re, '')
# "handle the tilde separator, it sorts before everything else"
if str1 =~ /^~/ && str2 =~ /^~/
# if they both have ~, strip it
str1 = str1[1..]
str2 = str2[1..]
next
elsif str1 =~ /^~/
return -1
elsif str2 =~ /^~/
return 1
end
break if str1.length == 0 or str2.length == 0
# "grab first completely alpha or completely numeric segment"
isnum = false
# if the first char of str1 is a digit, grab the chunk of continuous digits from each string
if str1 =~ /^[0-9]+/
if str1 =~ /^[0-9]+/
segment1 = $LAST_MATCH_INFO.to_s
str1 = $LAST_MATCH_INFO.post_match
else
segment1 = ''
end
if str2 =~ /^[0-9]+/
segment2 = $LAST_MATCH_INFO.to_s
str2 = $LAST_MATCH_INFO.post_match
else
segment2 = ''
end
isnum = true
# else grab the chunk of continuous alphas from each string (which may be '')
else
if str1 =~ /^[A-Za-z]+/
segment1 = $LAST_MATCH_INFO.to_s
str1 = $LAST_MATCH_INFO.post_match
else
segment1 = ''
end
if str2 =~ /^[A-Za-z]+/
segment2 = $LAST_MATCH_INFO.to_s
str2 = $LAST_MATCH_INFO.post_match
else
segment2 = ''
end
end
# if the segments we just grabbed from the strings are different types (i.e. one numeric one alpha),
# where alpha also includes ''; "numeric segments are always newer than alpha segments"
if segment2.length == 0
return 1 if isnum
return -1
end
if isnum
# "throw away any leading zeros - it's a number, right?"
segment1 = segment1.gsub(/^0+/, '')
segment2 = segment2.gsub(/^0+/, '')
# "whichever number has more digits wins"
return 1 if segment1.length > segment2.length
return -1 if segment1.length < segment2.length
end
# "strcmp will return which one is greater - even if the two segments are alpha
# or if they are numeric. don't return if they are equal because there might
# be more segments to compare"
rc = segment1 <=> segment2
return rc if rc != 0
end # end while loop
# if we haven't returned anything yet, "whichever version still has characters left over wins"
return 1 if str1.length > str2.length
return -1 if str1.length < str2.length
0
end
# parse a rpm "version" specification
# this re-implements rpm's
# rpmUtils.miscutils.stringToVersion() in ruby
def rpm_parse_evr(full_version)
epoch_index = full_version.index(':')
if epoch_index
epoch = full_version[0, epoch_index]
full_version = full_version[epoch_index + 1, full_version.length]
else
epoch = nil
end
begin
epoch = String(Integer(epoch))
rescue
# If there are non-digits in the epoch field, default to nil
epoch = nil
end
release_index = full_version.index('-')
if release_index
version = full_version[0, release_index]
release = full_version[release_index + 1, full_version.length]
arch = release.scan(ARCH_REGEX)[0]
if arch
architecture = arch.delete('.')
release.gsub!(ARCH_REGEX, '')
end
else
version = full_version
release = nil
end
{ :epoch => epoch, :version => version, :release => release, :arch => architecture }
end
# this method is a native implementation of the
# compare_values function in rpm's python bindings,
# found in python/header-py.c, as used by rpm.
def compare_values(s1, s2)
return 0 if s1.nil? && s2.nil?
return 1 if !s1.nil? && s2.nil?
return -1 if s1.nil? && !s2.nil?
rpmvercmp(s1, s2)
end
# how rpm compares two package versions:
# rpmUtils.miscutils.compareEVR(), which massages data types and then calls
# rpm.labelCompare(), found in rpm.git/python/header-py.c, which
# sets epoch to 0 if null, then compares epoch, then ver, then rel
# using compare_values() and returns the first non-0 result, else 0.
# This function combines the logic of compareEVR() and labelCompare().
#
# "version_should" can be v, v-r, or e:v-r.
# "version_is" will always be at least v-r, can be e:v-r
#
# return 1: a is newer than b
# 0: a and b are the same version
# -1: b is newer than a
def rpm_compare_evr(should, is)
# pass on to rpm labelCompare
should_hash = rpm_parse_evr(should)
is_hash = rpm_parse_evr(is)
unless should_hash[:epoch].nil?
rc = compare_values(should_hash[:epoch], is_hash[:epoch])
return rc unless rc == 0
end
rc = compare_values(should_hash[:version], is_hash[:version])
return rc unless rc == 0
# here is our special case, PUP-1244.
# if should_hash[:release] is nil (not specified by the user),
# and comparisons up to here are equal, return equal. We need to
# evaluate to whatever level of detail the user specified, so we
# don't end up upgrading or *downgrading* when not intended.
#
# This should NOT be triggered if we're trying to ensure latest.
return 0 if should_hash[:release].nil?
compare_values(should_hash[:release], is_hash[:release])
end
end