File: rpm_compare.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (193 lines) | stat: -rw-r--r-- 6,277 bytes parent folder | download
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
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.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..-1]
        str2 = str2[1..-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 = $~.to_s
          str1 = $~.post_match
        else
          segment1 = ''
        end
        if str2 =~ /^[0-9]+/
          segment2 = $~.to_s
          str2 = $~.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 = $~.to_s
          str1 = $~.post_match
        else
          segment1 = ''
        end
        if str2 =~ /^[A-Za-z]+/
          segment2 = $~.to_s
          str2 = $~.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
    return { :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 ( not s1.nil? ) && s2.nil?
    return -1 if s1.nil? && (not s2.nil?)
    return 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_compareEVR(should, is)
    # pass on to rpm labelCompare
    should_hash = rpm_parse_evr(should)
    is_hash = rpm_parse_evr(is)

    if !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?

    rc = compare_values(should_hash[:release], is_hash[:release])

    return rc
  end
end