semver_dialects 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ module SemverDialects
6
+ module Semver2
7
+ # Represents a token that matches any major, minor, or patch number.
8
+ ANY_NUMBER = 'x'
9
+
10
+ class Version < BaseVersion
11
+ def initialize(tokens, prerelease_tag: nil)
12
+ @tokens = tokens
13
+ @addition = prerelease_tag
14
+ end
15
+
16
+ def <=>(other)
17
+ if (idx = tokens.index(ANY_NUMBER))
18
+ a = tokens[0..(idx - 1)]
19
+ b = other.tokens[0..(idx - 1)]
20
+ return compare_tokens(a, b)
21
+ end
22
+
23
+ if (idx = other.tokens.index(ANY_NUMBER))
24
+ a = tokens[0..(idx - 1)]
25
+ b = other.tokens[0..(idx - 1)]
26
+ return compare_tokens(a, b)
27
+ end
28
+
29
+ super
30
+ end
31
+
32
+ private
33
+
34
+ # Compares pre-release tags as specified in https://semver.org/#spec-item-9.
35
+ def compare_additions(a, b) # rubocop:disable Naming/MethodParameterName
36
+ # Pre-release versions have a lower precedence than the associated normal version.
37
+ return -1 if !a.nil? && b.nil? # only self is a pre-release
38
+ return 1 if a.nil? && !b.nil? # only other is a pre-release
39
+
40
+ a <=> b
41
+ end
42
+ end
43
+
44
+ class PrereleaseTag < BaseVersion
45
+ def initialize(tokens)
46
+ @tokens = tokens
47
+ end
48
+
49
+ # Returns true if the prerelease tag is empty.
50
+ # In Semver 2 1.2.3-0 is NOT equivalent to 1.2.3.
51
+ def is_zero?
52
+ tokens.empty?
53
+ end
54
+
55
+ private
56
+
57
+ # Compares pre-release identifiers as specified in https://semver.org/#spec-item-11.
58
+ def compare_token_pair(a, b) # rubocop:disable Naming/MethodParameterName
59
+ case a
60
+ when Integer
61
+ case b
62
+ when String
63
+ # Numeric identifiers always have lower precedence than non-numeric identifiers.
64
+ return -1
65
+ when nil
66
+ # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
67
+ return 1
68
+ end
69
+ when String
70
+ case b
71
+ when Integer
72
+ # Numeric identifiers always have lower precedence than non-numeric identifiers.
73
+ return 1
74
+ when nil
75
+ # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
76
+ return 1
77
+ end
78
+ when nil
79
+ case b
80
+ when Integer
81
+ # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
82
+ return -1
83
+ when String
84
+ # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
85
+ return -1
86
+ end
87
+ end
88
+ # Identifiers have both the same type (numeric or non-numeric).
89
+ # This returns nil if the identifiers can't be compared.
90
+ a <=> b
91
+ end
92
+ end
93
+
94
+ class VersionParser
95
+ def self.parse(input)
96
+ new(input).parse
97
+ end
98
+
99
+ attr_reader :input
100
+
101
+ def initialize(input)
102
+ @input = input
103
+ @scanner = StringScanner.new(input)
104
+ end
105
+
106
+ def parse
107
+ tokens = []
108
+ prerelease_tag = nil
109
+
110
+ # skip ignore leading v if any
111
+ scanner.skip('v')
112
+
113
+ until scanner.eos?
114
+ if (s = scanner.scan(/\d+/))
115
+ tokens << s.to_i
116
+ elsif (s = scanner.scan(/\.x\z/i))
117
+ tokens << ANY_NUMBER
118
+ elsif (s = scanner.scan('.'))
119
+ # continue
120
+ elsif (s = scanner.scan('-'))
121
+ prerelease_tag = parse_prerelease_tag
122
+ elsif (s = scanner.scan(/\+.*/))
123
+ # continue
124
+ else
125
+ raise IncompleteScanError, scanner.rest
126
+ end
127
+ end
128
+
129
+ Version.new(tokens, prerelease_tag: prerelease_tag)
130
+ end
131
+
132
+ private
133
+
134
+ attr_reader :scanner
135
+
136
+ def parse_prerelease_tag
137
+ tokens = []
138
+ at_build_tag = false
139
+
140
+ until scanner.eos? || at_build_tag
141
+ if (s = scanner.scan(/\d+(?![a-zA-Z-])/))
142
+ tokens << s.to_i
143
+ elsif (s = scanner.scan(/[0-9a-zA-Z-]+/))
144
+ tokens << s
145
+ elsif (s = scanner.scan('.'))
146
+ # continue
147
+ elsif (s = scanner.scan('+'))
148
+ scanner.unscan
149
+ at_build_tag = true
150
+ else
151
+ raise IncompleteScanError, scanner.rest
152
+ end
153
+ end
154
+
155
+ PrereleaseTag.new(tokens)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SemverDialects
4
- VERSION = '2.0.2'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'semver_dialects/version'
4
- require 'semver_dialects/semantic_version/version_translator'
5
- require 'semver_dialects/semantic_version/version_parser'
6
- require 'semver_dialects/semantic_version/version_range'
4
+ require 'semver_dialects/base_version'
5
+ require 'semver_dialects/maven'
6
+ require 'semver_dialects/semver2'
7
+ require 'semver_dialects/semantic_version'
8
+ require 'semver_dialects/boundary'
9
+ require 'semver_dialects/interval'
10
+ require 'semver_dialects/interval_parser'
11
+ require 'semver_dialects/interval_set'
12
+ require 'semver_dialects/interval_set_parser'
7
13
  require 'deb_version'
8
14
 
9
15
  module SemverDialects
10
16
  # Captures all errors that could be possibly raised
11
17
  class Error < StandardError
12
- def initialize(msg)
13
- super(msg)
14
- end
15
18
  end
16
19
 
17
20
  class UnsupportedPackageTypeError < Error
@@ -44,70 +47,156 @@ module SemverDialects
44
47
  end
45
48
  end
46
49
 
47
- # A utiltity module that helps with version matching
48
- module VersionChecker
49
- def self.version_translate(typ, version_string)
50
- case typ
51
- when 'maven'
52
- VersionTranslator.translate_maven(version_string)
53
- when 'npm'
54
- VersionTranslator.translate_npm(version_string)
55
- when 'conan'
56
- VersionTranslator.translate_conan(version_string)
57
- when 'nuget'
58
- VersionTranslator.translate_nuget(version_string)
59
- when 'go'
60
- VersionTranslator.translate_go(version_string)
61
- when 'gem'
62
- VersionTranslator.translate_gem(version_string)
63
- when 'pypi'
64
- VersionTranslator.translate_pypi(version_string)
65
- when 'packagist'
66
- VersionTranslator.translate_packagist(version_string)
67
- else
68
- raise UnsupportedPackageTypeError, typ
69
- end
70
- end
50
+ class IncompleteScanError < InvalidVersionError
51
+ attr_reader :rest
71
52
 
72
- # Determines if a version of a given package type satisfies a constraint.
73
- #
74
- # On normal execution, this method might raise the following exceptions:
75
- #
76
- # - UnsupportedPackageTypeError if the package type is not supported
77
- # - InvalidVersionError if the version is invalid
78
- # - InvalidConstraintError if the constraint is invalid or contains invalid versions
79
- #
80
- def self.version_sat?(typ, raw_ver, raw_constraint)
81
- # os package versions are handled very differently from application package versions
82
- return os_pkg_version_sat?(typ, raw_ver, raw_constraint) if os_purl_type?(typ)
83
-
84
- # build an interval that only contains the version
85
- version = VersionCut.new(raw_ver)
86
- version_as_interval = VersionInterval.new(IntervalType::LEFT_CLOSED | IntervalType::RIGHT_CLOSED, version, version)
87
-
88
- constraint = VersionRange.new(false)
89
- version_translate(typ, raw_constraint).each do |version_interval_str|
90
- constraint << VersionParser.parse(version_interval_str)
91
- end
92
-
93
- constraint.overlaps_with?(version_as_interval)
53
+ def initialize(rest)
54
+ @rest = rest
94
55
  end
95
56
 
96
- def self.os_purl_type?(typ)
97
- ['deb', 'rpm', 'apk'].include?(typ)
57
+ def message
58
+ "scan did not consume '#{@rest}'"
98
59
  end
60
+ end
61
+
62
+ # Determines if a version of a given package type satisfies a constraint.
63
+ #
64
+ # On normal execution, this method might raise the following exceptions:
65
+ #
66
+ # - UnsupportedPackageTypeError if the package type is not supported
67
+ # - InvalidVersionError if the version is invalid
68
+ # - InvalidConstraintError if the constraint is invalid or contains invalid versions
69
+ #
70
+ def self.version_satisfies?(typ, raw_ver, raw_constraint)
71
+ # os package versions are handled very differently from application package versions
72
+ return os_pkg_version_satisfies?(typ, raw_ver, raw_constraint) if os_purl_type?(typ)
73
+
74
+ # build an interval that only contains the version
75
+ version = SemverDialects.parse_version(typ, raw_ver)
76
+ version_as_interval = Interval.from_version(version)
77
+
78
+ interval_set = IntervalSetParser.parse(typ, raw_constraint)
79
+
80
+ interval_set.overlaps_with?(version_as_interval)
81
+ end
82
+
83
+ def self.os_purl_type?(typ)
84
+ %w[deb rpm apk].include?(typ)
85
+ end
99
86
 
100
- def self.os_pkg_version_sat?(typ, raw_ver, raw_constraint)
101
- if typ == 'deb'
102
- # we only support the less than operator, because that's the only one currently output
103
- # by the advisory exporter for operating system packages.
104
- raise SemverDialects::InvalidConstraintError, raw_constraint unless raw_constraint[0] == '<'
87
+ def self.os_pkg_version_satisfies?(typ, raw_ver, raw_constraint)
88
+ return unless typ == 'deb'
89
+ # we only support the less than operator, because that's the only one currently output
90
+ # by the advisory exporter for operating system packages.
91
+ raise SemverDialects::InvalidConstraintError, raw_constraint unless raw_constraint[0] == '<'
105
92
 
106
- v1 = DebVersion.new(raw_ver)
107
- v2 = DebVersion.new(raw_constraint[1..-1])
93
+ v1 = DebVersion.new(raw_ver)
94
+ v2 = DebVersion.new(raw_constraint[1..])
95
+
96
+ v1 < v2
97
+ end
108
98
 
109
- return v1 < v2
110
- end
99
+ # Parse a version according to the syntax type.
100
+ def self.parse_version(typ, raw_ver)
101
+ # for efficiency most popular package types come first
102
+ case typ
103
+ when 'maven'
104
+ Maven::VersionParser.parse(raw_ver)
105
+
106
+ when 'npm'
107
+ # npm follows Semver 2.0.0.
108
+ Semver2::VersionParser.parse(raw_ver)
109
+
110
+ when 'go'
111
+ # Go follows Semver 2.0.0.
112
+ #
113
+ # Go pseudo-versions are pre-releases as defined in Semver 2.0.0,
114
+ # and can be compared as such. However, a pseudo-version can't be compared
115
+ # to a pre-release or another pseudo-version of the same base version.
116
+ #
117
+ # quoting https://go.dev/ref/mod#pseudo-versions
118
+ #
119
+ # Each pseudo-version may be in one of three forms, depending on the base version. These forms ensure that a pseudo-version compares higher than its base version, but lower than the next tagged version.
120
+ #
121
+
122
+ # vX.0.0-yyyymmddhhmmss-abcdefabcdef is used when there is no known
123
+ # base version. As with all versions, the major version X must match the
124
+ # module’s major version suffix.
125
+ #
126
+ # vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef is used when the base version
127
+ # is a pre-release version like vX.Y.Z-pre.
128
+ #
129
+ # vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef is used when the base version
130
+ # is a release version like vX.Y.Z. For example, if the base version is
131
+ # v1.2.3, a pseudo-version might be v1.2.4-0.20191109021931-daa7c04131f5.
132
+ #
133
+ Semver2::VersionParser.parse(raw_ver)
134
+
135
+ when 'pypi'
136
+ # See https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers
137
+ # TODO: Implement a dedicated parser.
138
+ SemanticVersion.new(raw_ver)
139
+
140
+ when 'nuget'
141
+ # NuGet diverges from Semver 2.0.0.
142
+ #
143
+ # quoting https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#where-nugetversion-diverges-from-semantic-versioning
144
+ #
145
+ # NuGetVersion supports a 4th version segment, Revision, to be compatible
146
+ # with, or a superset of, System.Version. Therefore, excluding prerelease
147
+ # and metadata labels, a version string is Major.Minor.Patch.Revision. As
148
+ # per version normalization described above, if Revision is zero, it is
149
+ # omitted from the normalized version string.
150
+ #
151
+ # NuGetVersion only requires the major segment to be defined. All others
152
+ # are optional, and are equivalent to zero. This means that 1, 1.0,
153
+ # 1.0.0, and 1.0.0.0 are all accepted and equal.
154
+ #
155
+ # NuGetVersion uses case insensitive string comparisons for pre-release
156
+ # components. This means that 1.0.0-alpha and 1.0.0-Alpha are equal.
157
+ #
158
+ Semver2::VersionParser.parse(raw_ver.downcase)
159
+
160
+ when 'gem'
161
+ # Rubygem does not follow Semver. Its versioning scheme is not documented.
162
+ #
163
+ # quoting https://guides.rubygems.org/specification-reference/
164
+ #
165
+ # The version string can contain numbers and periods, such as 1.0.0. A
166
+ # gem is a ‘prerelease’ gem if the version has a letter in it, such as
167
+ # 1.0.0.pre.
168
+ Gem::Version.new(raw_ver)
169
+
170
+ when 'packagist'
171
+ # Packagist defines specific identifiers like alpha, beta, and stable,
172
+ # and the comparison rules for these are not compatible with Semver.
173
+ # See https://github.com/composer/semver/blob/1d09200268e7d1052ded8e5da9c73c96a63d18f5/src/VersionParser.php#L39
174
+ SemanticVersion.new(raw_ver)
175
+
176
+ when 'conan'
177
+ # Conan diverges from Semver 2.0.0.
178
+ #
179
+ # quoting https://docs.conan.io/2/tutorial/versioning/version_ranges.html#semantic-versioning
180
+ #
181
+ # Conan extends the semver specification to any number of digits, and
182
+ # also allows to include lowercase letters in it. This was done because
183
+ # during 1.X a lot of experience and feedback from users was gathered,
184
+ # and it became evident than in C++ the versioning scheme is often more
185
+ # complex, and users were demanding more flexibility, allowing versions
186
+ # like 1.2.3.a.8 if necessary.
187
+ #
188
+ # Conan versions non-digit identifiers follow the same rules as package
189
+ # names, they can only contain lowercase letters. This is to avoid
190
+ # 1.2.3-Beta to be a different version than 1.2.3-beta which can be
191
+ # problematic, even a security risk.
192
+ #
193
+ SemanticVersion.new(raw_ver)
194
+
195
+ else
196
+ raise UnsupportedPackageTypeError, typ
111
197
  end
198
+ rescue ArgumentError
199
+ # Gem::Version.new raises an ArgumentError for invalid versions.
200
+ raise InvalidVersionError, raw_ver
112
201
  end
113
202
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semver_dialects
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Thome
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2024-03-20 00:00:00.000000000 Z
13
+ date: 2024-04-29 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: pastel
@@ -40,6 +40,20 @@ dependencies:
40
40
  - - "~>"
41
41
  - !ruby/object:Gem::Version
42
42
  version: '1.3'
43
+ - !ruby/object:Gem::Dependency
44
+ name: deb_version
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: 1.0.1
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: 1.0.1
43
57
  - !ruby/object:Gem::Dependency
44
58
  name: tty-command
45
59
  requirement: !ruby/object:Gem::Requirement
@@ -55,19 +69,19 @@ dependencies:
55
69
  - !ruby/object:Gem::Version
56
70
  version: 0.10.1
57
71
  - !ruby/object:Gem::Dependency
58
- name: deb_version
72
+ name: benchmark-ips
59
73
  requirement: !ruby/object:Gem::Requirement
60
74
  requirements:
61
75
  - - "~>"
62
76
  - !ruby/object:Gem::Version
63
- version: 1.0.1
64
- type: :runtime
77
+ version: '2.13'
78
+ type: :development
65
79
  prerelease: false
66
80
  version_requirements: !ruby/object:Gem::Requirement
67
81
  requirements:
68
82
  - - "~>"
69
83
  - !ruby/object:Gem::Version
70
- version: 1.0.1
84
+ version: '2.13'
71
85
  - !ruby/object:Gem::Dependency
72
86
  name: bundler
73
87
  requirement: !ruby/object:Gem::Requirement
@@ -110,6 +124,34 @@ dependencies:
110
124
  - - "~>"
111
125
  - !ruby/object:Gem::Version
112
126
  version: '3.0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rspec-parameterized
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '1.0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '1.0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: rubocop
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '1.63'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '1.63'
113
155
  - !ruby/object:Gem::Dependency
114
156
  name: simplecov
115
157
  requirement: !ruby/object:Gem::Requirement
@@ -135,15 +177,18 @@ extensions: []
135
177
  extra_rdoc_files: []
136
178
  files:
137
179
  - lib/semver_dialects.rb
180
+ - lib/semver_dialects/base_version.rb
181
+ - lib/semver_dialects/boundary.rb
138
182
  - lib/semver_dialects/cli.rb
139
183
  - lib/semver_dialects/command.rb
140
184
  - lib/semver_dialects/commands/check_version.rb
141
- - lib/semver_dialects/semantic_version/semantic_version.rb
142
- - lib/semver_dialects/semantic_version/version_cut.rb
143
- - lib/semver_dialects/semantic_version/version_interval.rb
144
- - lib/semver_dialects/semantic_version/version_parser.rb
145
- - lib/semver_dialects/semantic_version/version_range.rb
146
- - lib/semver_dialects/semantic_version/version_translator.rb
185
+ - lib/semver_dialects/interval.rb
186
+ - lib/semver_dialects/interval_parser.rb
187
+ - lib/semver_dialects/interval_set.rb
188
+ - lib/semver_dialects/interval_set_parser.rb
189
+ - lib/semver_dialects/maven.rb
190
+ - lib/semver_dialects/semantic_version.rb
191
+ - lib/semver_dialects/semver2.rb
147
192
  - lib/semver_dialects/version.rb
148
193
  - lib/utils.rb
149
194
  homepage: https://rubygems.org/gems/semver_dialects