File: apk.rb

package info (click to toggle)
ruby-semver-dialects 3.4.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 156 kB
  • sloc: ruby: 1,537; makefile: 4
file content (242 lines) | stat: -rw-r--r-- 7,951 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
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# frozen_string_literal: true

require 'strscan'

module SemverDialects
  module Apk
    # This implementation references the version.c apk-tools implementation
    # https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/6052bfef57a81d82451b4cad86f78a2d01959767/src/version.c
    # apk version spec can be found here https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver
    class Version < BaseVersion
      PRE_RELEASE_ORDER = { 'alpha' => 0, 'beta' => 1, 'pre' => 2, 'rc' => 3 }.freeze
      POST_RELEASE_ORDER = { 'cvs' => 0, 'svn' => 1, 'git' => 2, 'hg' => 3, 'p' => 4 }.freeze

      attr_reader :tokens, :pre_release, :post_release, :revision

      def initialize(tokens, pre_release: [], post_release: [], revision: [])
        @tokens = tokens
        @pre_release = pre_release
        @post_release = post_release
        @revision = revision
      end

      def <=>(other)
        cmp = compare_tokens(tokens, other.tokens)
        return cmp unless cmp.zero?

        cmp = compare_pre_release(pre_release, other.pre_release)
        return cmp unless cmp.zero?

        cmp = compare_post_release(post_release, other.post_release)
        return cmp unless cmp.zero?

        compare_revisions(revision, other.revision)
      end

      # Note that to_s does not accurately recreate the version string
      # if alphabets are present in the version segment.
      # For instance 1.2.a or 1.2a would both be returned as 1.2.a with to_s
      # More details in https://gitlab.com/gitlab-org/ruby/gems/semver_dialects/-/merge_requests/97#note_1989192447
      def to_s
        @to_s ||= begin
          main = tokens.join('.')
          main += "_#{pre_release.join('')}" unless pre_release.empty?
          main += "_#{post_release.join('')}" unless post_release.empty?
          main += "-#{revision.join('')}" unless revision.empty?
          main
        end
      end

      private

      # Token can be either integer or string
      # Precedence: numeric token > string token > no token
      def compare_token_pair(a, b)
        return 1 if !a.nil? && b.nil?
        return -1 if a.nil? && !b.nil?

        return 1 if a.is_a?(Integer) && b.is_a?(String)
        return -1 if a.is_a?(String) && b.is_a?(Integer)

        # Remaining scenario are tokens of the same type ie Integer or String. Use <=> to compare
        a <=> b
      end

      # Precedence: post-release > no release > pre-release
      # https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver
      def compare_pre_release(a, b)
        return 0 if a.empty? && b.empty?
        return -1 if !a.empty? && b.empty?
        return 1 if a.empty? && !b.empty?

        compare_suffix(a, b, PRE_RELEASE_ORDER)
      end

      # Precedence: post-release > no release > pre-release
      # https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver
      def compare_post_release(a, b)
        return 0 if a.empty? && b.empty?
        return 1 if !a.empty? && b.empty?
        return -1 if a.empty? && !b.empty?

        compare_suffix(a, b, POST_RELEASE_ORDER)
      end

      # Pre-release precedence: alpha < beta < pre < rc
      # Post-release precedence: cvs < svn < git < hg < p
      # Precedence for releases with number eg alpha1:
      # release without number < release with number
      def compare_suffix(a, b, order)
        a_suffix = order[a[0]]
        b_suffix = order[b[0]]

        return 1 if a_suffix > b_suffix
        return -1 if a_suffix < b_suffix

        a_value = a[1]
        b_value = b[1]

        return 1 if !a_value.nil? && b_value.nil?
        return -1 if a_value.nil? && !b_value.nil?

        (a_value || 0) <=> (b_value || 0)
      end

      def compare_revisions(a, b)
        return 0 if a.empty? && b.empty?
        return 1 if !a.empty? && b.empty?
        return -1 if a.empty? && !b.empty?

        a_value = a[1]
        b_value = b[1]

        return 1 if !a_value.nil? && b_value.nil?
        return -1 if a_value.nil? && !b_value.nil?

        (a_value || 0) <=> (b_value || 0)
      end
    end

    class VersionParser
      DASH = /-/
      ALPHABETS = /([a-zA-Z]+)/
      DIGITS = /([0-9]+)/
      DIGIT = /[0-9]/
      DOT = '.'
      UNDERSCORE = '_'
      PRE_RELEASE_SUFFIXES = %w[alpha beta pre rc].freeze
      POST_RELEASE_SUFFIXES = %w[cvs svn git hg p].freeze
      WHITE_SPACE = /\s/

      def self.parse(input)
        new(input).parse
      end

      attr_reader :scanner, :input

      def initialize(input)
        @input = input
        @pre_release = []
        @post_release = []
        @revision = []
        @scanner = StringScanner.new(input)
      end

      # Parse splits the raw version string into:
      # version, pre_release, post_release and revision
      # Format: <version>_<release>-<revision>
      # Note that version segment can contain alphabets
      # Release is always preceded with `_`
      # Revision is always preceded with `-`
      def parse
        tokens = parse_tokens

        Version.new(tokens, pre_release: @pre_release, post_release: @post_release, revision: @revision)
      end

      private

      def parse_tokens
        tokens = []

        until scanner.eos?
          case
          when (s = scanner.scan(ALPHABETS))
            tokens << s
          when (s = scanner.scan(DIGITS))
            # TODO: add support to parse numbers with leading zero https://gitlab.com/gitlab-org/gitlab/-/issues/471509
            raise SemverDialects::UnsupportedVersionError, input if s.start_with?('0') && s.length > 1

            tokens << s.to_i
          when (s = scanner.scan(UNDERSCORE))
            parse_release
            # Continue parsing if there's remaining tokens since revision which comes after release is optional
            return tokens if scanner.eos?
          when (s = scanner.scan(DASH))
            parse_revision
            return tokens
          when (s = scanner.scan(WHITE_SPACE))
            # Raise error if there's whitespace
            raise SemverDialects::InvalidVersionError, input
          when (s = scanner.scan(DOT))
            # Skip parsing dot
          else
            raise SemverDialects::IncompleteScanError, scanner.rest
          end
        end
        tokens
      end

      # PRE_RELEASE_SUFFIXES: alpha, beta, pre, rc
      # POST_RELEASE_SUFFIXES: cvs, svn, git, hg, p
      # No other suffixes are allowed
      # Release can be either `<suffix>` or `<suffix><number>` with the number being optional
      def parse_release
        # TODO: Add support to parse version with multiple releases
        raise SemverDialects::UnsupportedVersionError, input if !@pre_release.empty? || !@post_release.empty?

        suffix_type = nil
        until scanner.eos?
          case
          when (s = scanner.scan(ALPHABETS))
            if PRE_RELEASE_SUFFIXES.include?(s)
              suffix_type = :pre
              @pre_release << s
            elsif POST_RELEASE_SUFFIXES.include?(s)
              suffix_type = :post
              @post_release << s
            else
              raise SemverDialects::InvalidVersionError, input
            end
            return unless scanner.peek(1) =~ DIGIT
          when (s = scanner.scan(DIGITS))
            if suffix_type == :pre
              @pre_release << s.to_i
              return
            elsif suffix_type == :post
              @post_release << s.to_i
              return
            end
          end
        end
      end

      # Revision can be either `r` or `r<number>` with the number being optional
      def parse_revision
        until scanner.eos?
          case
          when (s = scanner.scan(ALPHABETS))
            raise SemverDialects::InvalidVersionError, input unless s == 'r'

            @revision << s

            return unless scanner.peek(1) =~ DIGIT
          when (s = scanner.scan(DIGITS))
            @revision << s.to_i
            return
          end
        end
      end
    end
  end
end