vers 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +29 -1
- data/CONTRIBUTING.md +2 -2
- data/README.md +27 -24
- data/lib/vers/bazel_version.rb +114 -0
- data/lib/vers/composer_version.rb +117 -0
- data/lib/vers/conan_version.rb +87 -0
- data/lib/vers/constraint.rb +13 -3
- data/lib/vers/distribution_version.rb +482 -0
- data/lib/vers/gem_version.rb +81 -0
- data/lib/vers/interval.rb +49 -33
- data/lib/vers/maven_version.rb +16 -0
- data/lib/vers/nuget_version.rb +14 -0
- data/lib/vers/parser.rb +924 -202
- data/lib/vers/pub_version.rb +59 -0
- data/lib/vers/pypi_version.rb +256 -0
- data/lib/vers/scheme.rb +52 -0
- data/lib/vers/semver_version.rb +294 -0
- data/lib/vers/special_version.rb +336 -0
- data/lib/vers/version.rb +81 -13
- data/lib/vers/version_comparison.rb +26 -0
- data/lib/vers/version_range.rb +201 -48
- data/lib/vers.rb +39 -9
- metadata +15 -4
data/lib/vers/parser.rb
CHANGED
|
@@ -17,20 +17,33 @@ module Vers
|
|
|
17
17
|
# range.contains?("1.5.0") # => true
|
|
18
18
|
#
|
|
19
19
|
class Parser
|
|
20
|
-
|
|
21
|
-
VERS_URI_REGEX = /\Avers:([^\/]+)\/(.+)\z/
|
|
22
|
-
|
|
23
|
-
# Pre-compiled regex patterns for common npm patterns
|
|
24
|
-
NPM_CARET_REGEX = /\A\^(.+)\z/
|
|
25
|
-
NPM_TILDE_REGEX = /\A~(.+)\z/
|
|
26
|
-
NPM_HYPHEN_REGEX = /\A(.+?)\s+-\s+(.+)\z/
|
|
27
|
-
NPM_X_RANGE_MAJOR_REGEX = /\A(\d+)\.x\z/
|
|
28
|
-
NPM_X_RANGE_MINOR_REGEX = /\A(\d+)\.(\d+)\.x\z/
|
|
20
|
+
NGINX_RANGE_REGEX = /\A\d+(?:\.\d+)+-\d+(?:\.\d+)+\z/
|
|
29
21
|
OPERATOR_PREFIX_REGEX = /\A[><=!]+/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
PUB_VERSION_PREFIX_REGEX = /\A[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?/
|
|
23
|
+
SEMVER_OUTPUT_SCHEMES = %w[npm cargo nuget composer pub].freeze
|
|
24
|
+
VERS_META_ENCODINGS = {
|
|
25
|
+
"%" => "%25",
|
|
26
|
+
"|" => "%7C",
|
|
27
|
+
">" => "%3E",
|
|
28
|
+
"<" => "%3C",
|
|
29
|
+
"=" => "%3D",
|
|
30
|
+
"!" => "%21",
|
|
31
|
+
"/" => "%2F",
|
|
32
|
+
"*" => "%2A",
|
|
33
|
+
" " => "%20"
|
|
34
|
+
}.freeze
|
|
35
|
+
# Maximum accepted length for a range string at parse/parse_native
|
|
36
|
+
# entry points. Range strings concatenate multiple constraints so this
|
|
37
|
+
# is set higher than Version::MAX_LENGTH while still bounding
|
|
38
|
+
# split/regex work to a few KB.
|
|
39
|
+
MAX_INPUT_LENGTH = 2048
|
|
40
|
+
|
|
41
|
+
# Maximum number of |-separated or ||-separated constraints in a
|
|
42
|
+
# single range. The exclusion loop in parse_constraints does
|
|
43
|
+
# O(n^2 log n) work as each != splits an interval and reconstructs the
|
|
44
|
+
# range; capping n keeps the worst case under a few thousand interval
|
|
45
|
+
# operations.
|
|
46
|
+
MAX_CONSTRAINTS = 64
|
|
34
47
|
|
|
35
48
|
##
|
|
36
49
|
# Parses a vers URI string into a VersionRange
|
|
@@ -43,21 +56,39 @@ module Vers
|
|
|
43
56
|
#
|
|
44
57
|
# parser = Vers::Parser.new
|
|
45
58
|
# parser.parse("vers:npm/>=1.2.3|<2.0.0")
|
|
46
|
-
# parser.
|
|
47
|
-
# parser.
|
|
59
|
+
# parser.parse_native("~>1.0", "gem")
|
|
60
|
+
# parser.parse_native("==1.2.3", "pypi")
|
|
48
61
|
#
|
|
49
|
-
def parse(vers_string)
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
def parse(vers_string, require_canonical_order: false)
|
|
63
|
+
validate_input_length!(vers_string)
|
|
64
|
+
|
|
65
|
+
return VersionRange.unbounded if vers_string == "*"
|
|
66
|
+
unless vers_string.is_a?(String) && vers_string.start_with?("vers:")
|
|
67
|
+
raise ArgumentError, "Invalid vers URI format: #{vers_string}"
|
|
68
|
+
end
|
|
69
|
+
if vers_string.match?(/[ \t\r\n]/)
|
|
70
|
+
raise ArgumentError, "non-canonical VERS: whitespace is not permitted"
|
|
52
71
|
end
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
remainder = vers_string.delete_prefix("vers:")
|
|
74
|
+
slash = remainder.index("/")
|
|
75
|
+
unless slash && slash.positive?
|
|
76
|
+
raise ArgumentError, "Invalid vers URI format: #{vers_string}"
|
|
77
|
+
end
|
|
56
78
|
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
raw_scheme = remainder[...slash]
|
|
80
|
+
if raw_scheme != raw_scheme.downcase
|
|
81
|
+
raise ArgumentError, "non-canonical VERS: type must be lowercase"
|
|
82
|
+
end
|
|
83
|
+
scheme = Scheme.canonical(raw_scheme)
|
|
84
|
+
constraints_string = remainder[(slash + 1)..]
|
|
85
|
+
if constraints_string.empty? || constraints_string == "*"
|
|
86
|
+
return VersionRange.unbounded(scheme: scheme)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
validate_vers_constraints!(constraints_string, scheme, require_canonical_order)
|
|
59
90
|
|
|
60
|
-
parse_constraints(constraints_string, scheme)
|
|
91
|
+
parse_constraints(constraints_string, scheme, decode_versions: true)
|
|
61
92
|
end
|
|
62
93
|
|
|
63
94
|
##
|
|
@@ -75,31 +106,45 @@ module Vers
|
|
|
75
106
|
# parser.parse_native(">=1.0,<2.0", "pypi")
|
|
76
107
|
#
|
|
77
108
|
def parse_native(range_string, scheme)
|
|
78
|
-
|
|
109
|
+
validate_input_length!(range_string)
|
|
110
|
+
|
|
111
|
+
canonical_scheme = Scheme.canonical(scheme)
|
|
112
|
+
range = case canonical_scheme
|
|
79
113
|
when "npm"
|
|
80
|
-
parse_npm_range(range_string)
|
|
81
|
-
when "gem"
|
|
114
|
+
parse_npm_range(range_string, scheme: "npm")
|
|
115
|
+
when "gem"
|
|
82
116
|
parse_gem_range(range_string)
|
|
83
117
|
when "pypi"
|
|
84
118
|
parse_pypi_range(range_string)
|
|
119
|
+
when "composer"
|
|
120
|
+
parse_composer_range(range_string)
|
|
121
|
+
when "pub"
|
|
122
|
+
parse_pub_range(range_string)
|
|
123
|
+
when "conan"
|
|
124
|
+
parse_conan_range(range_string)
|
|
125
|
+
when "nginx"
|
|
126
|
+
parse_nginx_range(range_string)
|
|
127
|
+
when "openssl"
|
|
128
|
+
parse_openssl_range(range_string)
|
|
85
129
|
when "maven"
|
|
86
130
|
parse_maven_range(range_string)
|
|
87
131
|
when "cargo"
|
|
88
|
-
parse_npm_range(range_string)
|
|
132
|
+
parse_npm_range(range_string, scheme: "cargo")
|
|
89
133
|
when "nuget"
|
|
90
134
|
parse_nuget_range(range_string)
|
|
91
|
-
when "hex"
|
|
135
|
+
when "hex"
|
|
92
136
|
parse_hex_range(range_string)
|
|
93
|
-
when "go"
|
|
137
|
+
when "go"
|
|
94
138
|
parse_go_range(range_string)
|
|
95
|
-
when "deb"
|
|
139
|
+
when "deb"
|
|
96
140
|
parse_debian_range(range_string)
|
|
97
141
|
when "rpm"
|
|
98
142
|
parse_rpm_range(range_string)
|
|
99
143
|
else
|
|
100
144
|
# Fall back to generic constraint parsing
|
|
101
|
-
parse_constraints(range_string,
|
|
145
|
+
parse_constraints(range_string, canonical_scheme)
|
|
102
146
|
end
|
|
147
|
+
range.with_scheme(canonical_scheme)
|
|
103
148
|
end
|
|
104
149
|
|
|
105
150
|
##
|
|
@@ -110,61 +155,234 @@ module Vers
|
|
|
110
155
|
# @return [String] The vers URI string
|
|
111
156
|
#
|
|
112
157
|
def to_vers_string(version_range, scheme)
|
|
113
|
-
|
|
114
|
-
|
|
158
|
+
canonical_scheme = Scheme.canonical(scheme)
|
|
159
|
+
if version_range.scheme && canonical_scheme != version_range.scheme
|
|
160
|
+
raise ArgumentError, "Cannot serialize a #{version_range.scheme} range as #{canonical_scheme}"
|
|
161
|
+
end
|
|
115
162
|
|
|
116
|
-
|
|
163
|
+
scheme = canonical_scheme
|
|
164
|
+
if version_range.unbounded? && (!version_range.raw_constraints || version_range.raw_constraints.empty?)
|
|
165
|
+
return "vers:#{scheme}/*"
|
|
166
|
+
end
|
|
167
|
+
if version_range.empty? && (!version_range.raw_constraints || version_range.raw_constraints.empty?)
|
|
168
|
+
return "vers:#{scheme}/"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
intervals = serialization_intervals(version_range, scheme)
|
|
117
172
|
constraints = []
|
|
118
173
|
|
|
119
174
|
# Detect != pattern: two intervals (-∞,V) ∪ (V,+∞)
|
|
120
175
|
if intervals.length == 2
|
|
121
176
|
a, b = intervals
|
|
122
177
|
if a.min.nil? && !a.max_inclusive && b.max.nil? && !b.min_inclusive && a.max == b.min
|
|
123
|
-
|
|
124
|
-
constraints
|
|
178
|
+
version = encode_vers_version(normalize_vers_version(a.max, scheme))
|
|
179
|
+
constraints << "!=#{version}"
|
|
180
|
+
sort_constraints!(constraints, scheme)
|
|
125
181
|
return "vers:#{scheme}/#{constraints.join('|')}"
|
|
126
182
|
end
|
|
127
183
|
end
|
|
128
184
|
|
|
129
185
|
intervals.each do |interval|
|
|
186
|
+
next if interval.unbounded?
|
|
187
|
+
|
|
130
188
|
if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
|
|
131
189
|
# Exact version
|
|
132
|
-
constraints << interval.min.to_s
|
|
190
|
+
constraints << encode_vers_version(normalize_vers_version(interval.min.to_s, scheme))
|
|
133
191
|
else
|
|
134
192
|
# Range constraints
|
|
135
193
|
if interval.min
|
|
136
194
|
operator = interval.min_inclusive ? ">=" : ">"
|
|
137
|
-
|
|
195
|
+
version = encode_vers_version(normalize_vers_version(interval.min, scheme))
|
|
196
|
+
constraints << "#{operator}#{version}"
|
|
138
197
|
end
|
|
139
198
|
|
|
140
199
|
if interval.max
|
|
141
200
|
operator = interval.max_inclusive ? "<=" : "<"
|
|
142
|
-
|
|
201
|
+
version = encode_vers_version(normalize_vers_version(interval.max, scheme))
|
|
202
|
+
constraints << "#{operator}#{version}"
|
|
143
203
|
end
|
|
144
204
|
end
|
|
145
205
|
end
|
|
146
206
|
|
|
147
|
-
|
|
207
|
+
version_range.exclusions.each do |version|
|
|
208
|
+
normalized = normalize_vers_version(version, scheme)
|
|
209
|
+
constraints << "!=#{encode_vers_version(normalized)}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sort_constraints!(constraints, scheme)
|
|
148
213
|
|
|
149
214
|
"vers:#{scheme}/#{constraints.join('|')}"
|
|
150
215
|
end
|
|
151
216
|
|
|
152
217
|
private
|
|
153
218
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
219
|
+
def validate_input_length!(input)
|
|
220
|
+
return if input.nil?
|
|
221
|
+
return if input.length <= MAX_INPUT_LENGTH
|
|
222
|
+
raise ArgumentError, "Range string too long (#{input.length} > #{MAX_INPUT_LENGTH})"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def validate_vers_constraints!(constraints, scheme, require_canonical_order)
|
|
226
|
+
if constraints.start_with?("|")
|
|
227
|
+
raise ArgumentError, "non-canonical VERS: leading pipe is not permitted"
|
|
228
|
+
end
|
|
229
|
+
if constraints.end_with?("|")
|
|
230
|
+
raise ArgumentError, "non-canonical VERS: trailing pipe is not permitted"
|
|
231
|
+
end
|
|
232
|
+
if constraints.include?("||")
|
|
233
|
+
raise ArgumentError, "non-canonical VERS: consecutive pipes are not permitted"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
previous = nil
|
|
237
|
+
previous_raw = nil
|
|
238
|
+
seen_versions = []
|
|
239
|
+
constraints.split("|").each do |raw|
|
|
240
|
+
constraint = Constraint.parse(raw)
|
|
241
|
+
validate_vers_version!(constraint.version, scheme)
|
|
242
|
+
decoded_version = decode_vers_version(constraint.version)
|
|
243
|
+
|
|
244
|
+
if require_canonical_order
|
|
245
|
+
if seen_versions.any? { |version| Version.compare_with_scheme(version, decoded_version, scheme).zero? }
|
|
246
|
+
raise ArgumentError, "non-canonical VERS: duplicate versions are not permitted"
|
|
247
|
+
end
|
|
248
|
+
if previous
|
|
249
|
+
order = Version.compare_with_scheme(previous, decoded_version, scheme)
|
|
250
|
+
if order.positive? || (order.zero? && previous_raw > raw)
|
|
251
|
+
raise ArgumentError, "non-canonical VERS: constraints are not sorted by version"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
seen_versions << decoded_version
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
previous = decoded_version
|
|
258
|
+
previous_raw = raw
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def validate_vers_version!(version, scheme)
|
|
263
|
+
index = 0
|
|
264
|
+
while index < version.bytesize
|
|
265
|
+
unless version.getbyte(index) == 37
|
|
266
|
+
index += 1
|
|
267
|
+
next
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
first = version.getbyte(index + 1)
|
|
271
|
+
second = version.getbyte(index + 2)
|
|
272
|
+
unless ascii_hex?(first) && ascii_hex?(second)
|
|
273
|
+
raise ArgumentError, "non-canonical VERS: invalid percent-encoding in version"
|
|
274
|
+
end
|
|
275
|
+
if lowercase_ascii_hex?(first) || lowercase_ascii_hex?(second)
|
|
276
|
+
raise ArgumentError, "non-canonical VERS: percent-encoding in version is not canonical"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
index += 3
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if version.match?(/[><=!*\/]/)
|
|
283
|
+
raise ArgumentError, "non-canonical VERS: reserved characters in version must be percent-encoded"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
return unless scheme == "datetime"
|
|
287
|
+
if version.include?("%3A")
|
|
288
|
+
raise ArgumentError, "non-canonical VERS: datetime time colons must be unencoded"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
decoded = decode_vers_version(version)
|
|
292
|
+
if (decoded.length > 10 && decoded[10] == "t") || decoded.end_with?("z")
|
|
293
|
+
raise ArgumentError, "non-canonical VERS: datetime must use uppercase T and Z"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ascii_hex?(byte)
|
|
298
|
+
byte && ((48..57).cover?(byte) || (65..70).cover?(byte) || lowercase_ascii_hex?(byte))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def lowercase_ascii_hex?(byte)
|
|
302
|
+
byte && (97..102).cover?(byte)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def sort_constraints!(constraints, scheme)
|
|
306
|
+
constraints.sort! do |left, right|
|
|
307
|
+
left_version = decode_vers_version(left.sub(OPERATOR_PREFIX_REGEX, ""))
|
|
308
|
+
right_version = decode_vers_version(right.sub(OPERATOR_PREFIX_REGEX, ""))
|
|
309
|
+
comparison = Version.compare_with_scheme(left_version, right_version, scheme)
|
|
310
|
+
comparison.zero? ? left <=> right : comparison
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def serialization_intervals(version_range, scheme)
|
|
315
|
+
raw_constraints = version_range.raw_constraints
|
|
316
|
+
return version_range.intervals unless raw_constraints
|
|
317
|
+
return raw_constraints unless %w[npm cargo].include?(scheme)
|
|
318
|
+
return raw_constraints if version_range.empty?
|
|
319
|
+
|
|
320
|
+
grouped = intersect_consecutive_intervals(raw_constraints, scheme)
|
|
321
|
+
raw_range = VersionRange.new(grouped, scheme: scheme)
|
|
322
|
+
equivalent_intervals?(raw_range.intervals, version_range.intervals, scheme) ? raw_constraints : version_range.intervals
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def equivalent_intervals?(left, right, scheme)
|
|
326
|
+
return false unless left.length == right.length
|
|
327
|
+
|
|
328
|
+
left.zip(right).all? do |left_interval, right_interval|
|
|
329
|
+
left_interval.min_inclusive == right_interval.min_inclusive &&
|
|
330
|
+
left_interval.max_inclusive == right_interval.max_inclusive &&
|
|
331
|
+
equivalent_bound?(left_interval.min, right_interval.min, scheme) &&
|
|
332
|
+
equivalent_bound?(left_interval.max, right_interval.max, scheme)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def equivalent_bound?(left, right, scheme)
|
|
337
|
+
return true if left.nil? && right.nil?
|
|
338
|
+
return false if left.nil? || right.nil?
|
|
339
|
+
|
|
340
|
+
Version.compare_for_range(left, right, scheme).zero?
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def normalize_vers_version(version, scheme)
|
|
344
|
+
canonical_scheme = Scheme.canonical(scheme)
|
|
345
|
+
if %w[npm cargo].include?(canonical_scheme) && SemverVersion.valid?(version)
|
|
346
|
+
return SemverVersion.normalize(version)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
return version if version.include?("-")
|
|
350
|
+
return version unless SEMVER_OUTPUT_SCHEMES.include?(canonical_scheme)
|
|
351
|
+
return version unless SemverVersion.valid?(version)
|
|
352
|
+
|
|
353
|
+
case version.count(".")
|
|
354
|
+
when 0
|
|
355
|
+
"#{version}.0.0"
|
|
356
|
+
when 1
|
|
357
|
+
"#{version}.0"
|
|
358
|
+
else
|
|
359
|
+
version
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def encode_vers_version(version)
|
|
364
|
+
version.gsub(/[\%|><=!\/* ]/, VERS_META_ENCODINGS)
|
|
158
365
|
end
|
|
159
366
|
|
|
160
|
-
def parse_constraints(constraints_string, scheme)
|
|
161
|
-
|
|
367
|
+
def parse_constraints(constraints_string, scheme, decode_versions: false)
|
|
368
|
+
canonical_scheme = Scheme.canonical(scheme)
|
|
369
|
+
return VersionRange.unbounded(scheme: canonical_scheme) if constraints_string == "*"
|
|
370
|
+
|
|
371
|
+
# Limit constraint count to bound the O(n^2 log n) exclusion loop
|
|
372
|
+
# below: each != splits an interval and reconstructs the range.
|
|
373
|
+
constraint_strings = constraints_string.split(/[|,]/, MAX_CONSTRAINTS + 1)
|
|
374
|
+
if constraint_strings.length > MAX_CONSTRAINTS
|
|
375
|
+
raise ArgumentError, "Too many constraints (> #{MAX_CONSTRAINTS})"
|
|
376
|
+
end
|
|
162
377
|
intervals = []
|
|
163
378
|
exclusions = []
|
|
164
|
-
interval_scheme =
|
|
379
|
+
interval_scheme = canonical_scheme
|
|
165
380
|
|
|
166
381
|
constraint_strings.each do |constraint_string|
|
|
167
382
|
constraint = Constraint.parse(constraint_string.strip)
|
|
383
|
+
if decode_versions
|
|
384
|
+
constraint = Constraint.new(constraint.operator, decode_vers_version(constraint.version))
|
|
385
|
+
end
|
|
168
386
|
|
|
169
387
|
if constraint.exclusion?
|
|
170
388
|
exclusions << constraint.version
|
|
@@ -174,186 +392,622 @@ module Vers
|
|
|
174
392
|
end
|
|
175
393
|
end
|
|
176
394
|
|
|
395
|
+
grouped_intervals = intersect_consecutive_intervals(intervals, interval_scheme)
|
|
396
|
+
|
|
177
397
|
# Start with the union of all positive constraints, or unbounded if only exclusions
|
|
178
|
-
range = if
|
|
179
|
-
VersionRange.new(
|
|
398
|
+
range = if grouped_intervals.any?
|
|
399
|
+
VersionRange.new(
|
|
400
|
+
grouped_intervals,
|
|
401
|
+
raw_constraints: intervals,
|
|
402
|
+
scheme: interval_scheme,
|
|
403
|
+
exclusions: exclusions
|
|
404
|
+
)
|
|
180
405
|
elsif exclusions.any?
|
|
181
|
-
VersionRange.
|
|
406
|
+
VersionRange.new(
|
|
407
|
+
[Interval.unbounded(scheme: interval_scheme)],
|
|
408
|
+
raw_constraints: [],
|
|
409
|
+
scheme: interval_scheme,
|
|
410
|
+
exclusions: exclusions
|
|
411
|
+
)
|
|
182
412
|
else
|
|
183
413
|
VersionRange.new([], scheme: interval_scheme)
|
|
184
414
|
end
|
|
185
415
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
416
|
+
range
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def intersect_consecutive_intervals(intervals, scheme)
|
|
420
|
+
grouped = []
|
|
421
|
+
index = 0
|
|
422
|
+
|
|
423
|
+
while index < intervals.length
|
|
424
|
+
current = intervals.fetch(index)
|
|
425
|
+
following = intervals[index + 1]
|
|
426
|
+
opposite_bounds = following &&
|
|
427
|
+
((current.min && !current.max && following.max && !following.min) ||
|
|
428
|
+
(current.max && !current.min && following.min && !following.max))
|
|
429
|
+
|
|
430
|
+
if opposite_bounds
|
|
431
|
+
intersection = current.intersect(following)
|
|
432
|
+
unless intersection.empty?
|
|
433
|
+
grouped << intersection
|
|
434
|
+
index += 2
|
|
435
|
+
next
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
grouped << current.with_scheme(scheme)
|
|
440
|
+
index += 1
|
|
189
441
|
end
|
|
190
442
|
|
|
191
|
-
|
|
443
|
+
grouped
|
|
192
444
|
end
|
|
193
445
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
446
|
+
def decode_vers_version(version)
|
|
447
|
+
version.gsub(/%([0-9A-Fa-f]{2})/) { [$1.to_i(16)].pack("C") }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def parse_composer_range(range_string)
|
|
451
|
+
constraint = range_string.to_s.strip
|
|
452
|
+
return VersionRange.unbounded(scheme: "composer") if %w[* x X @dev].include?(constraint)
|
|
453
|
+
|
|
454
|
+
parts = constraint.split(/\s*\|\|?\s*/)
|
|
455
|
+
raise ArgumentError, "Invalid Composer range: #{range_string}" if parts.any?(&:empty?)
|
|
456
|
+
|
|
457
|
+
ranges = parts.map { |part| parse_composer_conjunction(part) }
|
|
458
|
+
ranges.reduce { |combined, range| combined.union(range) }
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def parse_composer_conjunction(constraint)
|
|
462
|
+
value = constraint.strip.sub(/\s+as\s+.+\z/i, "")
|
|
463
|
+
if (match = /\A(\S+)\s+-\s+(\S+)\z/.match(value))
|
|
464
|
+
return parse_composer_hyphen_range(match[1], match[2])
|
|
206
465
|
end
|
|
207
466
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
467
|
+
parts = value.tr(",", " ").split
|
|
468
|
+
ranges = parts.map { |part| parse_composer_constraint(part) }
|
|
469
|
+
ranges.reduce { |combined, range| combined.intersect(range) }
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def parse_composer_constraint(constraint)
|
|
473
|
+
return VersionRange.unbounded(scheme: "composer") if constraint == "@dev"
|
|
474
|
+
|
|
475
|
+
value, stability = constraint.split("@", 2)
|
|
476
|
+
return VersionRange.unbounded(scheme: "composer") if value.empty?
|
|
477
|
+
return parse_composer_wildcard(value) if composer_wildcard?(value)
|
|
478
|
+
return parse_composer_caret(value.delete_prefix("^")) if value.start_with?("^")
|
|
479
|
+
return parse_composer_tilde(value.delete_prefix("~")) if value.start_with?("~")
|
|
480
|
+
return composer_exact_range(value) if ComposerVersion.branch?(value)
|
|
481
|
+
|
|
482
|
+
operator = value[/\A(?:==|<>|!=|>=|<=|>|<|=)/] || "="
|
|
483
|
+
version = value.delete_prefix(operator)
|
|
484
|
+
operator = "=" if operator == "=="
|
|
485
|
+
operator = "!=" if operator == "<>"
|
|
486
|
+
implicit = operator == "=" || operator == "!=" ? nil : "dev"
|
|
487
|
+
normalized = ComposerVersion.normalize(version, implicit_stability: stability || implicit)
|
|
488
|
+
|
|
489
|
+
return composer_exclusion_range(normalized) if operator == "!="
|
|
490
|
+
return composer_exact_range(normalized) if operator == "="
|
|
491
|
+
|
|
492
|
+
composer_interval_range(Constraint.new(operator, normalized).to_interval(scheme: "composer"))
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def parse_composer_caret(version)
|
|
496
|
+
parts = ComposerVersion.release_parts(version)
|
|
497
|
+
raise ArgumentError, "Invalid Composer caret version: #{version}" unless parts
|
|
498
|
+
|
|
499
|
+
bump = 0
|
|
500
|
+
if VersionComparison.compare_numbers(parts[0], "0").zero? && parts.length > 1
|
|
501
|
+
bump = 1
|
|
502
|
+
if VersionComparison.compare_numbers(parts[1], "0").zero? && parts.length > 2
|
|
503
|
+
bump = 2
|
|
504
|
+
end
|
|
211
505
|
end
|
|
212
506
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
507
|
+
lower = ComposerVersion.normalize(version, implicit_stability: "dev")
|
|
508
|
+
upper = increment_composer_release(parts, bump)
|
|
509
|
+
composer_interval_range(Interval.new(min: lower, max: upper, min_inclusive: true, max_inclusive: false, scheme: "composer"))
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def parse_composer_tilde(version)
|
|
513
|
+
parts = ComposerVersion.release_parts(version)
|
|
514
|
+
raise ArgumentError, "Invalid Composer tilde version: #{version}" unless parts
|
|
515
|
+
|
|
516
|
+
bump = parts.length > 2 ? parts.length - 2 : 0
|
|
517
|
+
lower = ComposerVersion.normalize(version, implicit_stability: "dev")
|
|
518
|
+
upper = increment_composer_release(parts, bump)
|
|
519
|
+
composer_interval_range(Interval.new(min: lower, max: upper, min_inclusive: true, max_inclusive: false, scheme: "composer"))
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def parse_composer_wildcard(value)
|
|
523
|
+
return VersionRange.unbounded(scheme: "composer") if %w[* x X].include?(value)
|
|
524
|
+
|
|
525
|
+
segments = value.sub(/\Av(?=\d)/i, "").split(".")
|
|
526
|
+
prefix = []
|
|
527
|
+
wildcard = false
|
|
528
|
+
segments.each do |segment|
|
|
529
|
+
if segment == "*" || segment.casecmp?("x")
|
|
530
|
+
wildcard = true
|
|
531
|
+
elsif wildcard || !VersionComparison.numeric?(segment)
|
|
532
|
+
raise ArgumentError, "Invalid Composer wildcard: #{value}"
|
|
220
533
|
else
|
|
221
|
-
|
|
534
|
+
prefix << segment
|
|
222
535
|
end
|
|
223
536
|
end
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
537
|
+
raise ArgumentError, "Invalid Composer wildcard: #{value}" unless wildcard
|
|
538
|
+
return VersionRange.unbounded(scheme: "composer") if prefix.empty?
|
|
539
|
+
|
|
540
|
+
lower_parts = prefix.map { |part| VersionComparison.normalize_number(part) }
|
|
541
|
+
lower_parts << "0" while lower_parts.length < 3
|
|
542
|
+
lower = "#{lower_parts.join(".")}-dev"
|
|
543
|
+
upper = increment_composer_release(prefix, prefix.length - 1)
|
|
544
|
+
composer_interval_range(Interval.new(min: lower, max: upper, min_inclusive: true, max_inclusive: false, scheme: "composer"))
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def parse_composer_hyphen_range(lower, upper)
|
|
548
|
+
lower_parts = ComposerVersion.release_parts(lower)
|
|
549
|
+
upper_parts = ComposerVersion.release_parts(upper)
|
|
550
|
+
raise ArgumentError, "Invalid Composer hyphen range: #{lower} - #{upper}" unless lower_parts && upper_parts
|
|
551
|
+
|
|
552
|
+
minimum = ComposerVersion.normalize(lower, implicit_stability: "dev")
|
|
553
|
+
if upper_parts.length < 3 && !ComposerVersion.explicit_stability?(upper)
|
|
554
|
+
maximum = increment_composer_release(upper_parts, upper_parts.length - 1)
|
|
555
|
+
interval = Interval.new(min: minimum, max: maximum, min_inclusive: true, max_inclusive: false, scheme: "composer")
|
|
229
556
|
else
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
557
|
+
maximum = ComposerVersion.normalize(upper)
|
|
558
|
+
interval = Interval.new(min: minimum, max: maximum, min_inclusive: true, max_inclusive: true, scheme: "composer")
|
|
559
|
+
end
|
|
560
|
+
composer_interval_range(interval)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def increment_composer_release(parts, index)
|
|
564
|
+
length = [parts.length, 3].max
|
|
565
|
+
incremented = Array.new(length, "0")
|
|
566
|
+
parts.each_with_index { |part, part_index| incremented[part_index] = VersionComparison.normalize_number(part) }
|
|
567
|
+
incremented[index] = (incremented[index].to_i + 1).to_s
|
|
568
|
+
((index + 1)...length).each { |part_index| incremented[part_index] = "0" }
|
|
569
|
+
"#{incremented.join(".")}-dev"
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def composer_wildcard?(value)
|
|
573
|
+
value.split(".").any? { |part| part == "*" || part.casecmp?("x") }
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def composer_interval_range(interval)
|
|
577
|
+
VersionRange.new([interval], raw_constraints: [interval], scheme: "composer")
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def composer_exact_range(version)
|
|
581
|
+
interval = Interval.exact(ComposerVersion.branch?(version) ? version : ComposerVersion.normalize(version), scheme: "composer")
|
|
582
|
+
composer_interval_range(interval)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def composer_exclusion_range(version)
|
|
586
|
+
VersionRange.new(
|
|
587
|
+
[Interval.unbounded(scheme: "composer")],
|
|
588
|
+
raw_constraints: [],
|
|
589
|
+
scheme: "composer",
|
|
590
|
+
exclusions: [version]
|
|
591
|
+
)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def parse_pub_range(range_string)
|
|
595
|
+
constraint = range_string.to_s.strip
|
|
596
|
+
return VersionRange.unbounded(scheme: "pub") if constraint == "any"
|
|
597
|
+
raise ArgumentError, "Empty Pub range" if constraint.empty?
|
|
598
|
+
raise ArgumentError, "Unsupported Pub range: #{range_string}" if constraint.match?(/[,|*]/) || constraint.include?("!=")
|
|
599
|
+
return parse_pub_caret(constraint.delete_prefix("^")) if constraint.start_with?("^")
|
|
600
|
+
|
|
601
|
+
ranges = tokenize_pub_constraints(constraint).map { |part| parse_pub_constraint(part) }
|
|
602
|
+
adjust_pub_upper_bounds(ranges.reduce { |combined, range| combined.intersect(range) })
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def tokenize_pub_constraints(constraint)
|
|
606
|
+
remaining = constraint.strip
|
|
607
|
+
tokens = []
|
|
608
|
+
|
|
609
|
+
until remaining.empty?
|
|
610
|
+
operator = remaining[/\A(?:>=|<=|>|<)/].to_s
|
|
611
|
+
remaining = remaining.delete_prefix(operator).lstrip
|
|
612
|
+
match = PUB_VERSION_PREFIX_REGEX.match(remaining)
|
|
613
|
+
raise ArgumentError, "Invalid Pub range: #{constraint}" unless match
|
|
614
|
+
|
|
615
|
+
tokens << "#{operator}#{match[0]}"
|
|
616
|
+
remaining = remaining[match[0].length..].to_s.strip
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
tokens
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def parse_pub_constraint(constraint)
|
|
623
|
+
operator = constraint[/\A(?:>=|<=|>|<)/].to_s
|
|
624
|
+
version = constraint.delete_prefix(operator)
|
|
625
|
+
raise ArgumentError, "Invalid Pub version: #{version}" unless PubVersion.valid?(version)
|
|
626
|
+
|
|
627
|
+
interval = if operator.empty?
|
|
628
|
+
Interval.exact(version, scheme: "pub")
|
|
629
|
+
else
|
|
630
|
+
Constraint.new(operator, version).to_interval(scheme: "pub")
|
|
291
631
|
end
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
632
|
+
VersionRange.new([interval], raw_constraints: [interval], scheme: "pub")
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def parse_pub_caret(version)
|
|
636
|
+
parsed = PubVersion.parse(version)
|
|
637
|
+
raise ArgumentError, "Invalid Pub caret version: #{version}" unless parsed
|
|
638
|
+
|
|
639
|
+
upper = if VersionComparison.compare_numbers(parsed.core[0], "0").zero?
|
|
640
|
+
"0.#{parsed.core[1].to_i + 1}.0-0"
|
|
641
|
+
else
|
|
642
|
+
"#{parsed.core[0].to_i + 1}.0.0-0"
|
|
643
|
+
end
|
|
644
|
+
interval = Interval.new(min: version, max: upper, min_inclusive: true, max_inclusive: false, scheme: "pub")
|
|
645
|
+
VersionRange.new([interval], raw_constraints: [interval], scheme: "pub")
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def adjust_pub_upper_bounds(range)
|
|
649
|
+
replacements = {}
|
|
650
|
+
intervals = range.intervals.map do |interval|
|
|
651
|
+
next interval unless pub_upper_needs_first_prerelease?(interval)
|
|
652
|
+
|
|
653
|
+
replacements[interval.max] = "#{interval.max}-0"
|
|
654
|
+
Interval.new(
|
|
655
|
+
min: interval.min,
|
|
656
|
+
max: replacements.fetch(interval.max),
|
|
657
|
+
min_inclusive: interval.min_inclusive,
|
|
658
|
+
max_inclusive: interval.max_inclusive,
|
|
659
|
+
scheme: "pub"
|
|
660
|
+
)
|
|
661
|
+
end
|
|
662
|
+
raw_constraints = (range.raw_constraints || range.intervals).map do |interval|
|
|
663
|
+
replacement = !interval.max_inclusive && replacements[interval.max]
|
|
664
|
+
next interval unless replacement
|
|
665
|
+
|
|
666
|
+
Interval.new(
|
|
667
|
+
min: interval.min,
|
|
668
|
+
max: replacement,
|
|
669
|
+
min_inclusive: interval.min_inclusive,
|
|
670
|
+
max_inclusive: interval.max_inclusive,
|
|
671
|
+
scheme: "pub"
|
|
672
|
+
)
|
|
673
|
+
end
|
|
674
|
+
VersionRange.new(intervals, raw_constraints: raw_constraints, scheme: "pub", exclusions: range.exclusions)
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def pub_upper_needs_first_prerelease?(interval)
|
|
678
|
+
return false unless interval.max && !interval.max_inclusive && PubVersion.valid?(interval.max)
|
|
679
|
+
|
|
680
|
+
maximum = PubVersion.parse(interval.max)
|
|
681
|
+
return false unless maximum.prerelease.empty? && maximum.build.empty?
|
|
682
|
+
return true unless interval.min && PubVersion.valid?(interval.min)
|
|
683
|
+
|
|
684
|
+
minimum = PubVersion.parse(interval.min)
|
|
685
|
+
return true if minimum.prerelease.empty?
|
|
686
|
+
|
|
687
|
+
!minimum.core.zip(maximum.core).all? do |minimum_part, maximum_part|
|
|
688
|
+
VersionComparison.compare_numbers(minimum_part, maximum_part).zero?
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def parse_conan_range(range_string)
|
|
693
|
+
constraint = range_string.to_s.strip.split(",", 2).first.to_s.strip
|
|
694
|
+
return VersionRange.empty(scheme: "conan") if constraint.empty?
|
|
695
|
+
|
|
696
|
+
if %w[* *-].include?(constraint)
|
|
697
|
+
interval = Interval.greater_than("0.0.0", inclusive: true, scheme: "conan")
|
|
698
|
+
return VersionRange.new([interval], raw_constraints: [interval], scheme: "conan")
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
if constraint.include?("||")
|
|
702
|
+
parts = constraint.split("||").map(&:strip)
|
|
703
|
+
raise ArgumentError, "Invalid Conan range: #{range_string}" if parts.any?(&:empty?)
|
|
704
|
+
|
|
705
|
+
return parts.map { |part| parse_conan_range(part) }.reduce { |combined, range| combined.union(range) }
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
if constraint.match?(/\s/)
|
|
709
|
+
parts = constraint.split
|
|
710
|
+
return parts.map { |part| parse_conan_range(part) }.reduce { |combined, range| combined.intersect(range) }
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
constraint = constraint.delete_suffix("-")
|
|
714
|
+
if constraint.start_with?("~", "^")
|
|
715
|
+
operator = constraint[0]
|
|
716
|
+
version = constraint[1..]
|
|
717
|
+
upper = conan_upper_bound(version, operator)
|
|
718
|
+
interval = Interval.new(
|
|
719
|
+
min: version,
|
|
720
|
+
max: upper,
|
|
721
|
+
min_inclusive: true,
|
|
722
|
+
max_inclusive: false,
|
|
723
|
+
scheme: "conan"
|
|
724
|
+
)
|
|
725
|
+
return VersionRange.new([interval], raw_constraints: [interval], scheme: "conan")
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
parse_constraints(constraint, "conan")
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def conan_upper_bound(version, operator)
|
|
732
|
+
parts = version.split(".")
|
|
733
|
+
unless parts.any? && parts.all? { |part| VersionComparison.numeric?(part) }
|
|
734
|
+
raise ArgumentError, "Invalid Conan compatible version: #{version}"
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
index = 0
|
|
738
|
+
if operator == "~" && parts.length > 1
|
|
739
|
+
index = 1
|
|
740
|
+
elsif operator == "^"
|
|
741
|
+
index += 1 while index < parts.length - 1 && VersionComparison.compare_numbers(parts[index], "0").zero?
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
upper = parts.first(index + 1).map { |part| VersionComparison.normalize_number(part) }
|
|
745
|
+
upper[index] = (upper[index].to_i + 1).to_s
|
|
746
|
+
"#{upper.join(".")}-"
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def parse_openssl_range(range_string)
|
|
750
|
+
parts = range_string.to_s.split(",", -1).map(&:strip)
|
|
751
|
+
intervals = parts.map do |version|
|
|
752
|
+
unless OpenSSLVersion.valid?(version)
|
|
753
|
+
raise ArgumentError, "Invalid OpenSSL version: #{version}"
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
Interval.exact(version, scheme: "openssl")
|
|
757
|
+
end
|
|
758
|
+
VersionRange.new(intervals, raw_constraints: intervals, scheme: "openssl")
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def parse_nginx_range(range_string)
|
|
762
|
+
constraint = range_string.to_s.strip
|
|
763
|
+
if constraint.include?(",")
|
|
764
|
+
parts = constraint.split(",", -1).map(&:strip)
|
|
765
|
+
raise ArgumentError, "Invalid Nginx range: #{range_string}" if parts.any?(&:empty?)
|
|
766
|
+
|
|
767
|
+
return parts.map { |part| parse_nginx_range(part) }.reduce { |combined, range| combined.union(range) }
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
if constraint.end_with?("+")
|
|
771
|
+
version = constraint.delete_suffix("+")
|
|
772
|
+
unless SemverVersion.valid?(version)
|
|
773
|
+
raise ArgumentError, "Invalid Nginx range: #{range_string}"
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
parts = version.split(".")
|
|
777
|
+
maximum = if parts.fetch(1).to_i.even?
|
|
778
|
+
"#{parts.fetch(0)}.#{parts.fetch(1).to_i + 1}.0"
|
|
779
|
+
end
|
|
780
|
+
interval = Interval.new(
|
|
781
|
+
min: version,
|
|
782
|
+
max: maximum,
|
|
783
|
+
min_inclusive: true,
|
|
784
|
+
max_inclusive: false,
|
|
785
|
+
scheme: "nginx"
|
|
786
|
+
)
|
|
787
|
+
return VersionRange.new([interval], raw_constraints: [interval], scheme: "nginx")
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
if NGINX_RANGE_REGEX.match?(constraint)
|
|
791
|
+
minimum, maximum = constraint.split("-", 2)
|
|
792
|
+
unless SemverVersion.valid?(minimum) && SemverVersion.valid?(maximum)
|
|
793
|
+
raise ArgumentError, "Invalid Nginx range: #{range_string}"
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
interval = Interval.new(
|
|
797
|
+
min: minimum,
|
|
798
|
+
max: maximum,
|
|
799
|
+
min_inclusive: true,
|
|
800
|
+
max_inclusive: true,
|
|
801
|
+
scheme: "nginx"
|
|
802
|
+
)
|
|
803
|
+
return VersionRange.new([interval], raw_constraints: [interval], scheme: "nginx")
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
parse_constraints(constraint, "nginx")
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# NPM range parsing (^, ~, -, ||, etc.)
|
|
810
|
+
def parse_npm_range(range_string, scheme: "npm")
|
|
811
|
+
constraint = range_string.to_s.strip
|
|
812
|
+
return VersionRange.unbounded(scheme: scheme) if constraint.empty? || %w[* x X].include?(constraint)
|
|
813
|
+
|
|
814
|
+
if constraint.include?("||")
|
|
815
|
+
or_parts = constraint.split("||", MAX_CONSTRAINTS + 1).map(&:strip)
|
|
816
|
+
if or_parts.length > MAX_CONSTRAINTS
|
|
817
|
+
raise ArgumentError, "Too many || clauses (> #{MAX_CONSTRAINTS})"
|
|
818
|
+
end
|
|
819
|
+
ranges = or_parts.map { |part| parse_npm_range(part, scheme: scheme) }
|
|
820
|
+
return ranges.reduce { |combined, range| combined.union(range) }
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
if constraint.include?(" - ")
|
|
824
|
+
lower, upper = constraint.split(" - ", 2).map(&:strip)
|
|
825
|
+
return parse_npm_hyphen_range(lower, upper, scheme: scheme)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
if constraint.match?(/[ \t\r\n]/)
|
|
829
|
+
tokens = constraint.split
|
|
830
|
+
merged = []
|
|
831
|
+
tokens.each do |token|
|
|
832
|
+
if merged.last&.match?(/\A(?:>=|<=|!=|>|<|=|~>|~|\^)\z/)
|
|
833
|
+
merged[-1] = "#{merged.last}#{token}"
|
|
834
|
+
else
|
|
835
|
+
merged << token
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
ranges = merged.map { |part| parse_npm_single_range(part, scheme: scheme) }
|
|
839
|
+
return ranges.reduce { |combined, range| combined.intersect(range) }
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
parse_npm_single_range(constraint, scheme: scheme)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def parse_npm_single_range(range_string, scheme: "npm")
|
|
846
|
+
constraint = range_string.to_s.strip
|
|
847
|
+
return parse_caret_range(constraint.delete_prefix("^").strip, scheme: scheme) if constraint.start_with?("^")
|
|
848
|
+
return parse_tilde_range(constraint.delete_prefix("~>").strip, scheme: scheme) if constraint.start_with?("~>")
|
|
849
|
+
return parse_tilde_range(constraint.delete_prefix("~").strip, scheme: scheme) if constraint.start_with?("~")
|
|
850
|
+
|
|
851
|
+
operator, version = extract_npm_operator(constraint)
|
|
852
|
+
if constraint.end_with?(".x", ".X", ".*") || partial_npm_version?(version)
|
|
853
|
+
return parse_npm_partial_range(version, operator, scheme: scheme)
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
parsed = Constraint.parse(constraint)
|
|
857
|
+
unless NpmVersion.valid?(parsed.version)
|
|
858
|
+
raise ArgumentError, "Invalid NPM range format: #{range_string}"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
if parsed.exclusion?
|
|
862
|
+
VersionRange.unbounded(scheme: scheme).exclude(parsed.version)
|
|
863
|
+
else
|
|
864
|
+
interval = parsed.to_interval(scheme: scheme)
|
|
865
|
+
VersionRange.new([interval], scheme: scheme)
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def parse_caret_range(version, scheme: "npm")
|
|
870
|
+
return VersionRange.unbounded(scheme: scheme) if version.empty? || %w[* x X].include?(version)
|
|
871
|
+
return parse_npm_partial_range(version, "", scheme: scheme) if version.end_with?(".x", ".X", ".*")
|
|
872
|
+
|
|
873
|
+
parsed = SemverVersion.parse(version)
|
|
874
|
+
raise ArgumentError, "Invalid NPM caret version: #{version}" unless parsed && NpmVersion.valid?(version)
|
|
875
|
+
|
|
876
|
+
base = version.split("+", 2).first.split("-", 2).first.delete_prefix("v").delete_prefix("V")
|
|
877
|
+
segments = base.count(".") + 1
|
|
878
|
+
major, minor, patch = parsed.core.map(&:to_i)
|
|
879
|
+
upper = if segments == 1 || major.positive?
|
|
880
|
+
"#{major + 1}.0.0"
|
|
881
|
+
elsif segments == 2 || minor.positive?
|
|
882
|
+
"0.#{minor + 1}.0"
|
|
883
|
+
else
|
|
884
|
+
"0.0.#{patch + 1}"
|
|
885
|
+
end
|
|
886
|
+
interval = Interval.new(min: version, max: upper, min_inclusive: true, max_inclusive: false, scheme: scheme)
|
|
887
|
+
VersionRange.new([interval], scheme: scheme)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def parse_tilde_range(version, scheme: "npm")
|
|
891
|
+
return VersionRange.unbounded(scheme: scheme) if version.empty? || %w[* x X].include?(version)
|
|
892
|
+
return parse_npm_partial_range(version, "", scheme: scheme) if version.end_with?(".x", ".X", ".*")
|
|
893
|
+
|
|
894
|
+
parsed = SemverVersion.parse(version)
|
|
895
|
+
raise ArgumentError, "Invalid NPM tilde version: #{version}" unless parsed && NpmVersion.valid?(version)
|
|
896
|
+
|
|
897
|
+
base = version.split("+", 2).first.split("-", 2).first.delete_prefix("v").delete_prefix("V")
|
|
898
|
+
segments = base.count(".") + 1
|
|
899
|
+
major, minor, patch = parsed.core.map(&:to_i)
|
|
900
|
+
upper = if segments >= 2
|
|
901
|
+
"#{major}.#{minor + 1}.0"
|
|
902
|
+
else
|
|
903
|
+
"#{major + 1}.0.0"
|
|
904
|
+
end
|
|
905
|
+
interval = Interval.new(min: version, max: upper, min_inclusive: true, max_inclusive: false, scheme: scheme)
|
|
906
|
+
raw_constraints = if parsed.prerelease.empty?
|
|
907
|
+
nil
|
|
908
|
+
else
|
|
909
|
+
base_version = "#{major}.#{minor}.#{patch}"
|
|
910
|
+
next_patch = "#{major}.#{minor}.#{patch + 1}"
|
|
911
|
+
[
|
|
912
|
+
Interval.new(min: version, max: base_version, min_inclusive: true, max_inclusive: false, scheme: scheme),
|
|
913
|
+
Interval.new(min: base_version, max: next_patch, min_inclusive: true, max_inclusive: false, scheme: scheme)
|
|
914
|
+
]
|
|
915
|
+
end
|
|
916
|
+
VersionRange.new([interval], raw_constraints: raw_constraints, scheme: scheme)
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def parse_npm_partial_range(version, operator, scheme: "npm")
|
|
920
|
+
lower, upper = npm_partial_bounds(version)
|
|
921
|
+
return VersionRange.unbounded(scheme: scheme) unless lower
|
|
922
|
+
|
|
923
|
+
interval = case operator
|
|
924
|
+
when "", "="
|
|
925
|
+
Interval.new(min: lower, max: upper, min_inclusive: true, max_inclusive: false, scheme: scheme)
|
|
926
|
+
when ">="
|
|
927
|
+
Interval.greater_than(lower, inclusive: true, scheme: scheme)
|
|
928
|
+
when ">"
|
|
929
|
+
Interval.greater_than(upper, inclusive: true, scheme: scheme)
|
|
930
|
+
when "<="
|
|
931
|
+
Interval.less_than(upper, scheme: scheme)
|
|
932
|
+
when "<"
|
|
933
|
+
Interval.less_than(lower, scheme: scheme)
|
|
299
934
|
else
|
|
300
|
-
|
|
935
|
+
raise ArgumentError, "Invalid operator for NPM partial range: #{operator}"
|
|
301
936
|
end
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
@@parser_cache[cache_key] = result
|
|
305
|
-
result
|
|
937
|
+
|
|
938
|
+
VersionRange.new([interval], raw_constraints: [interval], scheme: scheme)
|
|
306
939
|
end
|
|
307
940
|
|
|
308
|
-
def
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
# ^1.2.3 := >=1.2.3 <2.0.0
|
|
312
|
-
"#{v.major + 1}.0.0"
|
|
313
|
-
elsif v.minor && v.minor > 0
|
|
314
|
-
# ^0.2.3 := >=0.2.3 <0.3.0
|
|
315
|
-
"0.#{v.minor + 1}.0"
|
|
316
|
-
else
|
|
317
|
-
# ^0.0.3 := >=0.0.3 <0.0.4
|
|
318
|
-
"0.0.#{(v.patch || 0) + 1}"
|
|
319
|
-
end
|
|
941
|
+
def npm_partial_bounds(version)
|
|
942
|
+
constraint = version.to_s.strip
|
|
943
|
+
return [nil, nil] if constraint.empty? || %w[* x X].include?(constraint)
|
|
320
944
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
945
|
+
segments = constraint.delete_prefix("v").delete_prefix("V").split(".", -1)
|
|
946
|
+
raise ArgumentError, "Invalid NPM partial version: #{version}" if segments.length > 3
|
|
947
|
+
|
|
948
|
+
parts = []
|
|
949
|
+
wildcard = false
|
|
950
|
+
segments.each do |segment|
|
|
951
|
+
if %w[* x X].include?(segment)
|
|
952
|
+
wildcard = true
|
|
953
|
+
elsif wildcard || !VersionComparison.numeric?(segment)
|
|
954
|
+
raise ArgumentError, "Invalid NPM partial version: #{version}"
|
|
955
|
+
else
|
|
956
|
+
parts << segment.to_i
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
return [nil, nil] if parts.empty?
|
|
960
|
+
|
|
961
|
+
lower_parts = parts.dup
|
|
962
|
+
lower_parts << 0 while lower_parts.length < 3
|
|
963
|
+
upper_parts = lower_parts.dup
|
|
964
|
+
upper_parts[parts.length - 1] += 1
|
|
965
|
+
(parts.length...upper_parts.length).each { |index| upper_parts[index] = 0 }
|
|
966
|
+
[lower_parts.join("."), upper_parts.join(".")]
|
|
324
967
|
end
|
|
325
968
|
|
|
326
|
-
def
|
|
327
|
-
|
|
969
|
+
def partial_npm_version?(version)
|
|
970
|
+
constraint = version.to_s.strip.delete_prefix("v").delete_prefix("V")
|
|
971
|
+
segments = constraint.split(".", -1)
|
|
972
|
+
return false if segments.length > 3
|
|
328
973
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
])
|
|
337
|
-
release_range = VersionRange.new([
|
|
338
|
-
Interval.new(min: base, max: next_patch, min_inclusive: true, max_inclusive: false)
|
|
339
|
-
])
|
|
340
|
-
return pre_range.union(release_range)
|
|
974
|
+
partial = segments.length < 3
|
|
975
|
+
segments.each do |segment|
|
|
976
|
+
if %w[* x X].include?(segment)
|
|
977
|
+
partial = true
|
|
978
|
+
elsif !VersionComparison.numeric?(segment)
|
|
979
|
+
return false
|
|
980
|
+
end
|
|
341
981
|
end
|
|
982
|
+
partial
|
|
983
|
+
end
|
|
342
984
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
# ~1.2 := >=1.2.0 <1.3.0
|
|
348
|
-
"#{v.major}.#{v.minor + 1}.0"
|
|
349
|
-
else
|
|
350
|
-
# ~1 := >=1.0.0 <2.0.0
|
|
351
|
-
"#{v.major + 1}.0.0"
|
|
352
|
-
end
|
|
985
|
+
def extract_npm_operator(constraint)
|
|
986
|
+
operator = constraint[/\A(?:>=|<=|!=|>|<|=)/].to_s
|
|
987
|
+
[operator, constraint.delete_prefix(operator).strip]
|
|
988
|
+
end
|
|
353
989
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
990
|
+
def parse_npm_hyphen_range(lower, upper, scheme: "npm")
|
|
991
|
+
minimum = partial_npm_version?(lower) ? npm_partial_bounds(lower).fetch(0) : lower
|
|
992
|
+
if partial_npm_version?(upper)
|
|
993
|
+
maximum = npm_partial_bounds(upper).fetch(1)
|
|
994
|
+
maximum_inclusive = false
|
|
995
|
+
else
|
|
996
|
+
maximum = upper
|
|
997
|
+
maximum_inclusive = true
|
|
998
|
+
end
|
|
999
|
+
unless NpmVersion.valid?(minimum) && NpmVersion.valid?(maximum)
|
|
1000
|
+
raise ArgumentError, "Invalid NPM hyphen range: #{lower} - #{upper}"
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
interval = Interval.new(
|
|
1004
|
+
min: minimum,
|
|
1005
|
+
max: maximum,
|
|
1006
|
+
min_inclusive: true,
|
|
1007
|
+
max_inclusive: maximum_inclusive,
|
|
1008
|
+
scheme: scheme
|
|
1009
|
+
)
|
|
1010
|
+
VersionRange.new([interval], scheme: scheme)
|
|
357
1011
|
end
|
|
358
1012
|
|
|
359
1013
|
# Gem range parsing (~>, >=, etc.)
|
|
@@ -389,9 +1043,77 @@ module Vers
|
|
|
389
1043
|
|
|
390
1044
|
# Python/PyPI range parsing
|
|
391
1045
|
def parse_pypi_range(range_string)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1046
|
+
constraint = range_string.to_s.strip
|
|
1047
|
+
raise ArgumentError, "Empty PyPI range" if constraint.empty?
|
|
1048
|
+
|
|
1049
|
+
if constraint.include?(",")
|
|
1050
|
+
ranges = constraint.split(",", -1).map do |part|
|
|
1051
|
+
raise ArgumentError, "Empty PyPI constraint" if part.strip.empty?
|
|
1052
|
+
|
|
1053
|
+
parse_pypi_range(part)
|
|
1054
|
+
end
|
|
1055
|
+
return ranges.reduce { |combined, range| combined.intersect(range) }
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
return parse_pypi_compatible_range(constraint.delete_prefix("~=").strip) if constraint.start_with?("~=")
|
|
1059
|
+
if constraint.start_with?("===")
|
|
1060
|
+
raise ArgumentError, "PyPI arbitrary equality constraints are not supported: #{constraint}"
|
|
1061
|
+
end
|
|
1062
|
+
if constraint.start_with?("==") && constraint.delete_prefix("==").strip.end_with?(".*")
|
|
1063
|
+
return parse_pypi_prefix_range(constraint.delete_prefix("==").strip, exclude: false)
|
|
1064
|
+
end
|
|
1065
|
+
if constraint.start_with?("!=") && constraint.delete_prefix("!=").strip.end_with?(".*")
|
|
1066
|
+
return parse_pypi_prefix_range(constraint.delete_prefix("!=").strip, exclude: true)
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
constraint = "=#{constraint.delete_prefix("==").strip}" if constraint.start_with?("==")
|
|
1070
|
+
parse_constraints(constraint, "pypi")
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def parse_pypi_compatible_range(version)
|
|
1074
|
+
parsed = PyPIVersion.parse(version)
|
|
1075
|
+
unless parsed && parsed.release.length >= 2
|
|
1076
|
+
raise ArgumentError, "Invalid PyPI compatible release: #{version}"
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
upper_release = parsed.release[0...-1].map { |part| VersionComparison.normalize_number(part) }
|
|
1080
|
+
upper_release[-1] = (upper_release.fetch(-1).to_i + 1).to_s
|
|
1081
|
+
upper = upper_release.join(".")
|
|
1082
|
+
unless VersionComparison.compare_numbers(parsed.epoch, "0").zero?
|
|
1083
|
+
upper = "#{VersionComparison.normalize_number(parsed.epoch)}!#{upper}"
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
interval = Interval.new(min: version, max: upper, min_inclusive: true, max_inclusive: false, scheme: "pypi")
|
|
1087
|
+
VersionRange.new([interval], raw_constraints: [interval], scheme: "pypi")
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
def parse_pypi_prefix_range(version, exclude:)
|
|
1091
|
+
prefix = version.delete_suffix(".*")
|
|
1092
|
+
parsed = PyPIVersion.parse(prefix)
|
|
1093
|
+
invalid = !parsed || !parsed.pre_tag.nil? || !parsed.post_number.nil? ||
|
|
1094
|
+
!parsed.dev_number.nil? || parsed.local.any?
|
|
1095
|
+
raise ArgumentError, "Invalid PyPI prefix constraint: #{version}" if invalid
|
|
1096
|
+
|
|
1097
|
+
release = parsed.release.map { |part| VersionComparison.normalize_number(part) }
|
|
1098
|
+
upper_release = release.dup
|
|
1099
|
+
upper_release[-1] = (upper_release.fetch(-1).to_i + 1).to_s
|
|
1100
|
+
epoch = if VersionComparison.compare_numbers(parsed.epoch, "0").zero?
|
|
1101
|
+
""
|
|
1102
|
+
else
|
|
1103
|
+
"#{VersionComparison.normalize_number(parsed.epoch)}!"
|
|
1104
|
+
end
|
|
1105
|
+
lower = "#{epoch}#{release.join(".")}.dev0"
|
|
1106
|
+
upper = "#{epoch}#{upper_release.join(".")}.dev0"
|
|
1107
|
+
|
|
1108
|
+
intervals = if exclude
|
|
1109
|
+
[
|
|
1110
|
+
Interval.less_than(lower, scheme: "pypi"),
|
|
1111
|
+
Interval.greater_than(upper, inclusive: true, scheme: "pypi")
|
|
1112
|
+
]
|
|
1113
|
+
else
|
|
1114
|
+
[Interval.new(min: lower, max: upper, min_inclusive: true, max_inclusive: false, scheme: "pypi")]
|
|
1115
|
+
end
|
|
1116
|
+
VersionRange.new(intervals, raw_constraints: intervals, scheme: "pypi")
|
|
395
1117
|
end
|
|
396
1118
|
|
|
397
1119
|
# Maven range parsing
|
|
@@ -498,7 +1220,7 @@ module Vers
|
|
|
498
1220
|
begin
|
|
499
1221
|
parsed_range = parse_maven_range(range_part)
|
|
500
1222
|
ranges << parsed_range
|
|
501
|
-
rescue
|
|
1223
|
+
rescue ArgumentError
|
|
502
1224
|
# If parsing fails, skip this part
|
|
503
1225
|
end
|
|
504
1226
|
end
|
|
@@ -677,4 +1399,4 @@ module Vers
|
|
|
677
1399
|
parse_constraints(range_string, 'rpm')
|
|
678
1400
|
end
|
|
679
1401
|
end
|
|
680
|
-
end
|
|
1402
|
+
end
|