File: rpm.rb

package info (click to toggle)
ruby-semver-dialects 3.7.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 172 kB
  • sloc: ruby: 1,632; makefile: 4
file content (158 lines) | stat: -rw-r--r-- 5,519 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
# frozen_string_literal: true

require 'strscan'

module SemverDialects
  module Rpm
    module TokenPairComparison # rubocop:todo Style/Documentation
      # Token can be either alphabets, integers or tilde.
      # Caret is currently not supported. More details here https://gitlab.com/gitlab-org/gitlab/-/issues/428941#note_1882343489
      # Precedence: numeric token > string token > no token > tilda (~)
      def compare_token_pair(a, b) # rubocop:todo Naming/MethodParameterName
        return 1 if a != '~' && b == '~'
        return -1 if 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
    end

    # This implementation references `go-rpm-version` https://github.com/knqyf263/go-rpm-version
    # Which is based on the official `rpmvercmp`
    # https://github.com/rpm-software-management/rpm/blob/master/rpmio/rpmvercmp.c implementation
    # rpm versioning schema can be found here https://github.com/rpm-software-management/rpm/blob/master/docs/manual/dependencies.md#versioning
    # Details on how the caret and tilde symbols are handled can be found here https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/#_handling_non_sorting_versions_with_tilde_dot_and_caret
    class Version < BaseVersion
      include TokenPairComparison

      attr_reader :tokens, :addition, :epoch

      def initialize(tokens, epoch: nil, release_tag: nil) # rubocop:todo Lint/MissingSuper
        @tokens = tokens
        @addition = release_tag
        @epoch = epoch
      end

      def <=>(other)
        # Compare epoch first
        epoch_cmp = compare_epochs(epoch, other.epoch)
        return epoch_cmp unless epoch_cmp.zero?

        # Then compare version
        cmp = compare_tokens(tokens, other.tokens)
        return cmp unless cmp.zero?

        # And finally compare release tags
        compare_additions(addition, other.addition)
      end

      # Note that to_s does not accurately recreate the version string.
      # More details here https://gitlab.com/gitlab-org/gitlab/-/issues/428941#note_1882343489
      def to_s
        main = if !epoch.nil?
                 "#{epoch}:" + tokens.join('.')
               else
                 tokens.join('.')
               end
        main += "-#{addition.tokens.join('.')}" unless addition.nil?

        # Remove . around ~
        main.gsub(/\.~\./, '~')
      end

      private

      def compare_epochs(a, b) # rubocop:todo Naming/MethodParameterName
        (a || 0) <=> (b || 0)
      end
    end

    class ReleaseTag < BaseVersion # rubocop:todo Style/Documentation
      include TokenPairComparison

      def initialize(tokens) # rubocop:todo Lint/MissingSuper
        @tokens = tokens
      end
    end

    class VersionParser # rubocop:todo Style/Documentation
      DASH = /-/
      ALPHABET = /([a-zA-Z]+)/
      TILDE = /~/
      DIGIT = /([0-9]+)/
      COLON = /:/
      NON_ALPHANUMERIC_DASH_TILDE_AND_WHITESPACE = /[^a-zA-Z0-9~\s]+/
      WHITE_SPACE = /\s/

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

      def initialize(input)
        @input = input
        @scanner = StringScanner.new(input)
      end

      # parse splits the input string into epoch, version and release tag Eg: <epoch>:<version>-<release_tag>
      # The version and release tag are split at the first `-` character if present
      # With the segment before the first `-` being version while the other being release tag
      # Subsequent `-` are disregarded
      def parse
        epoch = nil
        if (s = scanner.scan(/\d+:/))
          epoch = s[..-2].to_i
        end

        # parse tokens until we reach the release tag, if any
        tokens = parse_tokens(false)

        # parse release tag
        release_tag = nil
        release_tag = ReleaseTag.new(parse_tokens(true)) if scanner.rest?

        raise IncompleteScanError, scanner.rest if scanner.rest?

        Version.new(tokens, epoch: epoch, release_tag: release_tag)
      end

      private

      attr_reader :scanner, :input

      def parse_tokens(stop_at_release_tag)
        tokens = []

        until scanner.eos?
          case
          when (s = scanner.scan(DASH))
            return tokens unless stop_at_release_tag
            # If release tag has been encountered, ignore subsequent dashes
          when (s = scanner.scan(ALPHABET))
            tokens << s
          when (s = scanner.scan(TILDE))
            tokens << s
          when (s = scanner.scan(DIGIT))
            tokens << s.to_i
          when (s = scanner.scan(WHITE_SPACE))
            # Whitespace is not permitted
            # https://github.com/rpm-software-management/rpm/blob/4d1b7401415003720ea9bef7bda248f7de4fa025/docs/manual/dependencies.md#versioning
            raise SemverDialects::InvalidVersionError, input
          when (s = scanner.scan(NON_ALPHANUMERIC_DASH_TILDE_AND_WHITESPACE))
            # Non-ascii characters are considered equal
            # so they are ignored when parsing versions
            # https://github.com/rpm-software-management/rpm/blob/rpm-4.19.1.1-release/tests/rpmvercmp.at#L143
          else
            raise SemverDialects::IncompleteScanError, scanner.rest
          end
        end
        tokens
      end
    end
  end
end