vers 1.3.1 → 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 +7 -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 +893 -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 +68 -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,21 +17,21 @@ 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
|
-
|
|
34
|
-
|
|
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
35
|
# Maximum accepted length for a range string at parse/parse_native
|
|
36
36
|
# entry points. Range strings concatenate multiple constraints so this
|
|
37
37
|
# is set higher than Version::MAX_LENGTH while still bounding
|
|
@@ -56,23 +56,39 @@ module Vers
|
|
|
56
56
|
#
|
|
57
57
|
# parser = Vers::Parser.new
|
|
58
58
|
# parser.parse("vers:npm/>=1.2.3|<2.0.0")
|
|
59
|
-
# parser.
|
|
60
|
-
# parser.
|
|
59
|
+
# parser.parse_native("~>1.0", "gem")
|
|
60
|
+
# parser.parse_native("==1.2.3", "pypi")
|
|
61
61
|
#
|
|
62
|
-
def parse(vers_string)
|
|
62
|
+
def parse(vers_string, require_canonical_order: false)
|
|
63
63
|
validate_input_length!(vers_string)
|
|
64
64
|
|
|
65
|
-
if vers_string == "*"
|
|
66
|
-
|
|
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"
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
validate_vers_constraints!(constraints_string, scheme, require_canonical_order)
|
|
90
|
+
|
|
91
|
+
parse_constraints(constraints_string, scheme, decode_versions: true)
|
|
76
92
|
end
|
|
77
93
|
|
|
78
94
|
##
|
|
@@ -92,31 +108,43 @@ module Vers
|
|
|
92
108
|
def parse_native(range_string, scheme)
|
|
93
109
|
validate_input_length!(range_string)
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
canonical_scheme = Scheme.canonical(scheme)
|
|
112
|
+
range = case canonical_scheme
|
|
96
113
|
when "npm"
|
|
97
|
-
parse_npm_range(range_string)
|
|
98
|
-
when "gem"
|
|
114
|
+
parse_npm_range(range_string, scheme: "npm")
|
|
115
|
+
when "gem"
|
|
99
116
|
parse_gem_range(range_string)
|
|
100
117
|
when "pypi"
|
|
101
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)
|
|
102
129
|
when "maven"
|
|
103
130
|
parse_maven_range(range_string)
|
|
104
131
|
when "cargo"
|
|
105
|
-
parse_npm_range(range_string)
|
|
132
|
+
parse_npm_range(range_string, scheme: "cargo")
|
|
106
133
|
when "nuget"
|
|
107
134
|
parse_nuget_range(range_string)
|
|
108
|
-
when "hex"
|
|
135
|
+
when "hex"
|
|
109
136
|
parse_hex_range(range_string)
|
|
110
|
-
when "go"
|
|
137
|
+
when "go"
|
|
111
138
|
parse_go_range(range_string)
|
|
112
|
-
when "deb"
|
|
139
|
+
when "deb"
|
|
113
140
|
parse_debian_range(range_string)
|
|
114
141
|
when "rpm"
|
|
115
142
|
parse_rpm_range(range_string)
|
|
116
143
|
else
|
|
117
144
|
# Fall back to generic constraint parsing
|
|
118
|
-
parse_constraints(range_string,
|
|
145
|
+
parse_constraints(range_string, canonical_scheme)
|
|
119
146
|
end
|
|
147
|
+
range.with_scheme(canonical_scheme)
|
|
120
148
|
end
|
|
121
149
|
|
|
122
150
|
##
|
|
@@ -127,41 +155,61 @@ module Vers
|
|
|
127
155
|
# @return [String] The vers URI string
|
|
128
156
|
#
|
|
129
157
|
def to_vers_string(version_range, scheme)
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
162
|
|
|
133
|
-
|
|
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)
|
|
134
172
|
constraints = []
|
|
135
173
|
|
|
136
174
|
# Detect != pattern: two intervals (-∞,V) ∪ (V,+∞)
|
|
137
175
|
if intervals.length == 2
|
|
138
176
|
a, b = intervals
|
|
139
177
|
if a.min.nil? && !a.max_inclusive && b.max.nil? && !b.min_inclusive && a.max == b.min
|
|
140
|
-
|
|
141
|
-
constraints
|
|
178
|
+
version = encode_vers_version(normalize_vers_version(a.max, scheme))
|
|
179
|
+
constraints << "!=#{version}"
|
|
180
|
+
sort_constraints!(constraints, scheme)
|
|
142
181
|
return "vers:#{scheme}/#{constraints.join('|')}"
|
|
143
182
|
end
|
|
144
183
|
end
|
|
145
184
|
|
|
146
185
|
intervals.each do |interval|
|
|
186
|
+
next if interval.unbounded?
|
|
187
|
+
|
|
147
188
|
if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
|
|
148
189
|
# Exact version
|
|
149
|
-
constraints << interval.min.to_s
|
|
190
|
+
constraints << encode_vers_version(normalize_vers_version(interval.min.to_s, scheme))
|
|
150
191
|
else
|
|
151
192
|
# Range constraints
|
|
152
193
|
if interval.min
|
|
153
194
|
operator = interval.min_inclusive ? ">=" : ">"
|
|
154
|
-
|
|
195
|
+
version = encode_vers_version(normalize_vers_version(interval.min, scheme))
|
|
196
|
+
constraints << "#{operator}#{version}"
|
|
155
197
|
end
|
|
156
198
|
|
|
157
199
|
if interval.max
|
|
158
200
|
operator = interval.max_inclusive ? "<=" : "<"
|
|
159
|
-
|
|
201
|
+
version = encode_vers_version(normalize_vers_version(interval.max, scheme))
|
|
202
|
+
constraints << "#{operator}#{version}"
|
|
160
203
|
end
|
|
161
204
|
end
|
|
162
205
|
end
|
|
163
206
|
|
|
164
|
-
|
|
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)
|
|
165
213
|
|
|
166
214
|
"vers:#{scheme}/#{constraints.join('|')}"
|
|
167
215
|
end
|
|
@@ -174,13 +222,152 @@ module Vers
|
|
|
174
222
|
raise ArgumentError, "Range string too long (#{input.length} > #{MAX_INPUT_LENGTH})"
|
|
175
223
|
end
|
|
176
224
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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?
|
|
181
341
|
end
|
|
182
342
|
|
|
183
|
-
def
|
|
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)
|
|
365
|
+
end
|
|
366
|
+
|
|
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
|
+
|
|
184
371
|
# Limit constraint count to bound the O(n^2 log n) exclusion loop
|
|
185
372
|
# below: each != splits an interval and reconstructs the range.
|
|
186
373
|
constraint_strings = constraints_string.split(/[|,]/, MAX_CONSTRAINTS + 1)
|
|
@@ -189,10 +376,13 @@ module Vers
|
|
|
189
376
|
end
|
|
190
377
|
intervals = []
|
|
191
378
|
exclusions = []
|
|
192
|
-
interval_scheme =
|
|
379
|
+
interval_scheme = canonical_scheme
|
|
193
380
|
|
|
194
381
|
constraint_strings.each do |constraint_string|
|
|
195
382
|
constraint = Constraint.parse(constraint_string.strip)
|
|
383
|
+
if decode_versions
|
|
384
|
+
constraint = Constraint.new(constraint.operator, decode_vers_version(constraint.version))
|
|
385
|
+
end
|
|
196
386
|
|
|
197
387
|
if constraint.exclusion?
|
|
198
388
|
exclusions << constraint.version
|
|
@@ -202,189 +392,622 @@ module Vers
|
|
|
202
392
|
end
|
|
203
393
|
end
|
|
204
394
|
|
|
395
|
+
grouped_intervals = intersect_consecutive_intervals(intervals, interval_scheme)
|
|
396
|
+
|
|
205
397
|
# Start with the union of all positive constraints, or unbounded if only exclusions
|
|
206
|
-
range = if
|
|
207
|
-
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
|
+
)
|
|
208
405
|
elsif exclusions.any?
|
|
209
|
-
VersionRange.
|
|
406
|
+
VersionRange.new(
|
|
407
|
+
[Interval.unbounded(scheme: interval_scheme)],
|
|
408
|
+
raw_constraints: [],
|
|
409
|
+
scheme: interval_scheme,
|
|
410
|
+
exclusions: exclusions
|
|
411
|
+
)
|
|
210
412
|
else
|
|
211
413
|
VersionRange.new([], scheme: interval_scheme)
|
|
212
414
|
end
|
|
213
415
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
441
|
end
|
|
218
442
|
|
|
219
|
-
|
|
443
|
+
grouped
|
|
444
|
+
end
|
|
445
|
+
|
|
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])
|
|
465
|
+
end
|
|
466
|
+
|
|
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
|
|
505
|
+
end
|
|
506
|
+
|
|
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}"
|
|
533
|
+
else
|
|
534
|
+
prefix << segment
|
|
535
|
+
end
|
|
536
|
+
end
|
|
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")
|
|
556
|
+
else
|
|
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")
|
|
631
|
+
end
|
|
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")
|
|
220
807
|
end
|
|
221
808
|
|
|
222
809
|
# NPM range parsing (^, ~, -, ||, etc.)
|
|
223
|
-
def parse_npm_range(range_string)
|
|
224
|
-
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# Handle || (OR) operator
|
|
230
|
-
if range_string.include?('||')
|
|
231
|
-
or_parts = range_string.split('||', MAX_CONSTRAINTS + 1).map(&:strip)
|
|
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)
|
|
232
816
|
if or_parts.length > MAX_CONSTRAINTS
|
|
233
817
|
raise ArgumentError, "Too many || clauses (> #{MAX_CONSTRAINTS})"
|
|
234
818
|
end
|
|
235
|
-
ranges = or_parts.map { |part| parse_npm_range(part) }
|
|
236
|
-
return ranges.reduce { |
|
|
819
|
+
ranges = or_parts.map { |part| parse_npm_range(part, scheme: scheme) }
|
|
820
|
+
return ranges.reduce { |combined, range| combined.union(range) }
|
|
237
821
|
end
|
|
238
822
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return
|
|
823
|
+
if constraint.include?(" - ")
|
|
824
|
+
lower, upper = constraint.split(" - ", 2).map(&:strip)
|
|
825
|
+
return parse_npm_hyphen_range(lower, upper, scheme: scheme)
|
|
242
826
|
end
|
|
243
827
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
253
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}"
|
|
254
859
|
end
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if all_exact
|
|
259
|
-
ranges.reduce { |acc, range| acc.union(range) }
|
|
860
|
+
|
|
861
|
+
if parsed.exclusion?
|
|
862
|
+
VersionRange.unbounded(scheme: scheme).exclude(parsed.version)
|
|
260
863
|
else
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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)
|
|
330
934
|
else
|
|
331
|
-
|
|
935
|
+
raise ArgumentError, "Invalid operator for NPM partial range: #{operator}"
|
|
332
936
|
end
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
@@parser_cache[cache_key] = result
|
|
336
|
-
result
|
|
937
|
+
|
|
938
|
+
VersionRange.new([interval], raw_constraints: [interval], scheme: scheme)
|
|
337
939
|
end
|
|
338
940
|
|
|
339
|
-
def
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
# ^1.2.3 := >=1.2.3 <2.0.0
|
|
343
|
-
"#{v.major + 1}.0.0"
|
|
344
|
-
elsif v.minor && v.minor > 0
|
|
345
|
-
# ^0.2.3 := >=0.2.3 <0.3.0
|
|
346
|
-
"0.#{v.minor + 1}.0"
|
|
347
|
-
else
|
|
348
|
-
# ^0.0.3 := >=0.0.3 <0.0.4
|
|
349
|
-
"0.0.#{(v.patch || 0) + 1}"
|
|
350
|
-
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)
|
|
351
944
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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(".")]
|
|
355
967
|
end
|
|
356
968
|
|
|
357
|
-
def
|
|
358
|
-
|
|
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
|
|
359
973
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
])
|
|
368
|
-
release_range = VersionRange.new([
|
|
369
|
-
Interval.new(min: base, max: next_patch, min_inclusive: true, max_inclusive: false)
|
|
370
|
-
])
|
|
371
|
-
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
|
|
372
981
|
end
|
|
982
|
+
partial
|
|
983
|
+
end
|
|
373
984
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# ~1.2 := >=1.2.0 <1.3.0
|
|
379
|
-
"#{v.major}.#{v.minor + 1}.0"
|
|
380
|
-
else
|
|
381
|
-
# ~1 := >=1.0.0 <2.0.0
|
|
382
|
-
"#{v.major + 1}.0.0"
|
|
383
|
-
end
|
|
985
|
+
def extract_npm_operator(constraint)
|
|
986
|
+
operator = constraint[/\A(?:>=|<=|!=|>|<|=)/].to_s
|
|
987
|
+
[operator, constraint.delete_prefix(operator).strip]
|
|
988
|
+
end
|
|
384
989
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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)
|
|
388
1011
|
end
|
|
389
1012
|
|
|
390
1013
|
# Gem range parsing (~>, >=, etc.)
|
|
@@ -420,9 +1043,77 @@ module Vers
|
|
|
420
1043
|
|
|
421
1044
|
# Python/PyPI range parsing
|
|
422
1045
|
def parse_pypi_range(range_string)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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")
|
|
426
1117
|
end
|
|
427
1118
|
|
|
428
1119
|
# Maven range parsing
|
|
@@ -708,4 +1399,4 @@ module Vers
|
|
|
708
1399
|
parse_constraints(range_string, 'rpm')
|
|
709
1400
|
end
|
|
710
1401
|
end
|
|
711
|
-
end
|
|
1402
|
+
end
|