vers 1.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 +7 -0
- data/CHANGELOG.md +87 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +194 -0
- data/README.md +257 -0
- data/Rakefile +248 -0
- data/SECURITY.md +164 -0
- data/VERSION-RANGE-SPEC.rst +1009 -0
- data/lib/vers/constraint.rb +158 -0
- data/lib/vers/interval.rb +229 -0
- data/lib/vers/parser.rb +447 -0
- data/lib/vers/version.rb +338 -0
- data/lib/vers/version_range.rb +173 -0
- data/lib/vers.rb +215 -0
- data/sig/vers.rbs +4 -0
- data/test-suite-data.json +335 -0
- metadata +61 -0
data/lib/vers/parser.rb
ADDED
@@ -0,0 +1,447 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'constraint'
|
4
|
+
require_relative 'version_range'
|
5
|
+
|
6
|
+
module Vers
|
7
|
+
##
|
8
|
+
# Parses vers URI strings and package manager specific version ranges
|
9
|
+
#
|
10
|
+
# This class handles parsing of vers URI format (e.g., "vers:npm/>=1.2.3|<2.0.0")
|
11
|
+
# and provides extensible support for different package ecosystem syntaxes.
|
12
|
+
#
|
13
|
+
# == Examples
|
14
|
+
#
|
15
|
+
# parser = Vers::Parser.new
|
16
|
+
# range = parser.parse("vers:npm/>=1.2.3|<2.0.0")
|
17
|
+
# range.contains?("1.5.0") # => true
|
18
|
+
#
|
19
|
+
class Parser
|
20
|
+
# Regex for parsing vers URI format
|
21
|
+
VERS_URI_REGEX = /\Avers:([^\/]+)\/(.+)\z/
|
22
|
+
|
23
|
+
##
|
24
|
+
# Parses a vers URI string into a VersionRange
|
25
|
+
#
|
26
|
+
# @param vers_string [String] The vers URI string to parse
|
27
|
+
# @return [VersionRange] The parsed version range
|
28
|
+
# @raise [ArgumentError] if the vers string is invalid
|
29
|
+
#
|
30
|
+
# == Examples
|
31
|
+
#
|
32
|
+
# parser = Vers::Parser.new
|
33
|
+
# parser.parse("vers:npm/>=1.2.3|<2.0.0")
|
34
|
+
# parser.parse("vers:gem/~>1.0")
|
35
|
+
# parser.parse("vers:pypi/==1.2.3")
|
36
|
+
#
|
37
|
+
def parse(vers_string)
|
38
|
+
if vers_string == "*"
|
39
|
+
return VersionRange.unbounded
|
40
|
+
end
|
41
|
+
|
42
|
+
match = vers_string.match(VERS_URI_REGEX)
|
43
|
+
raise ArgumentError, "Invalid vers URI format: #{vers_string}" unless match
|
44
|
+
|
45
|
+
scheme = match[1]
|
46
|
+
constraints_string = match[2]
|
47
|
+
|
48
|
+
parse_constraints(constraints_string, scheme)
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Parses a native package manager version range into a VersionRange
|
53
|
+
#
|
54
|
+
# @param range_string [String] The native version range string
|
55
|
+
# @param scheme [String] The package manager scheme (npm, gem, pypi, etc.)
|
56
|
+
# @return [VersionRange] The parsed version range
|
57
|
+
#
|
58
|
+
# == Examples
|
59
|
+
#
|
60
|
+
# parser = Vers::Parser.new
|
61
|
+
# parser.parse_native("^1.2.3", "npm")
|
62
|
+
# parser.parse_native("~> 1.0", "gem")
|
63
|
+
# parser.parse_native(">=1.0,<2.0", "pypi")
|
64
|
+
#
|
65
|
+
def parse_native(range_string, scheme)
|
66
|
+
case scheme
|
67
|
+
when "npm"
|
68
|
+
parse_npm_range(range_string)
|
69
|
+
when "gem", "rubygems"
|
70
|
+
parse_gem_range(range_string)
|
71
|
+
when "pypi"
|
72
|
+
parse_pypi_range(range_string)
|
73
|
+
when "maven"
|
74
|
+
parse_maven_range(range_string)
|
75
|
+
when "nuget"
|
76
|
+
parse_nuget_range(range_string)
|
77
|
+
when "deb", "debian"
|
78
|
+
parse_debian_range(range_string)
|
79
|
+
when "rpm"
|
80
|
+
parse_rpm_range(range_string)
|
81
|
+
else
|
82
|
+
# Fall back to generic constraint parsing
|
83
|
+
parse_constraints(range_string, scheme)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Converts a VersionRange back to a vers URI string
|
89
|
+
#
|
90
|
+
# @param version_range [VersionRange] The version range to convert
|
91
|
+
# @param scheme [String] The package manager scheme
|
92
|
+
# @return [String] The vers URI string
|
93
|
+
#
|
94
|
+
def to_vers_string(version_range, scheme)
|
95
|
+
return "*" if version_range.unbounded?
|
96
|
+
return "vers:#{scheme}/" if version_range.empty?
|
97
|
+
|
98
|
+
constraints = []
|
99
|
+
|
100
|
+
version_range.intervals.each do |interval|
|
101
|
+
if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
|
102
|
+
# Exact version
|
103
|
+
constraints << "=#{interval.min}"
|
104
|
+
else
|
105
|
+
# Range constraints
|
106
|
+
if interval.min
|
107
|
+
operator = interval.min_inclusive ? ">=" : ">"
|
108
|
+
constraints << "#{operator}#{interval.min}"
|
109
|
+
end
|
110
|
+
|
111
|
+
if interval.max
|
112
|
+
operator = interval.max_inclusive ? "<=" : "<"
|
113
|
+
constraints << "#{operator}#{interval.max}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
"vers:#{scheme}/#{constraints.join('|')}"
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def parse_constraints(constraints_string, scheme)
|
124
|
+
constraint_strings = constraints_string.split('|')
|
125
|
+
intervals = []
|
126
|
+
exclusions = []
|
127
|
+
|
128
|
+
constraint_strings.each do |constraint_string|
|
129
|
+
constraint = Constraint.parse(constraint_string.strip)
|
130
|
+
|
131
|
+
if constraint.exclusion?
|
132
|
+
exclusions << constraint.version
|
133
|
+
else
|
134
|
+
interval = constraint.to_interval
|
135
|
+
intervals << interval if interval
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Start with the union of all positive constraints
|
140
|
+
range = VersionRange.new(intervals)
|
141
|
+
|
142
|
+
# Apply exclusions
|
143
|
+
exclusions.each do |version|
|
144
|
+
range = range.exclude(version)
|
145
|
+
end
|
146
|
+
|
147
|
+
range
|
148
|
+
end
|
149
|
+
|
150
|
+
# NPM range parsing (^, ~, -, ||, etc.)
|
151
|
+
def parse_npm_range(range_string)
|
152
|
+
# Handle empty string as unbounded
|
153
|
+
if range_string.nil? || range_string.strip.empty?
|
154
|
+
return VersionRange.unbounded
|
155
|
+
end
|
156
|
+
|
157
|
+
# Handle || (OR) operator
|
158
|
+
if range_string.include?('||')
|
159
|
+
or_parts = range_string.split('||').map(&:strip)
|
160
|
+
ranges = or_parts.map { |part| parse_npm_single_range(part) }
|
161
|
+
return ranges.reduce { |acc, range| acc.union(range) }
|
162
|
+
end
|
163
|
+
|
164
|
+
# Handle hyphen ranges first (before space splitting)
|
165
|
+
if range_string.match(/^(.+?)\s+-\s+(.+)$/)
|
166
|
+
return parse_npm_single_range(range_string)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Handle space-separated AND constraints
|
170
|
+
and_parts = range_string.split(/\s+/).reject(&:empty?)
|
171
|
+
ranges = and_parts.map { |part| parse_npm_single_range(part) }
|
172
|
+
ranges.reduce { |acc, range| acc.intersect(range) }
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_npm_single_range(range_string)
|
176
|
+
case range_string
|
177
|
+
when /^\^(.+)$/
|
178
|
+
# Caret range: ^1.2.3 := >=1.2.3 <2.0.0
|
179
|
+
version = Regexp.last_match(1)
|
180
|
+
parse_caret_range(version)
|
181
|
+
when /^~(.+)$/
|
182
|
+
# Tilde range: ~1.2.3 := >=1.2.3 <1.3.0
|
183
|
+
version = Regexp.last_match(1)
|
184
|
+
parse_tilde_range(version)
|
185
|
+
when /^(.+?)\s+-\s+(.+)$/
|
186
|
+
# Hyphen range: 1.2.3 - 2.3.4 := >=1.2.3 <=2.3.4
|
187
|
+
from_version = Regexp.last_match(1).strip
|
188
|
+
to_version = Regexp.last_match(2).strip
|
189
|
+
VersionRange.new([
|
190
|
+
Interval.new(min: from_version, max: to_version, min_inclusive: true, max_inclusive: true)
|
191
|
+
])
|
192
|
+
when "*", "x", "X"
|
193
|
+
VersionRange.unbounded
|
194
|
+
when /^(\d+)\.x$/
|
195
|
+
# X-range like "1.x" := >=1.0.0 <2.0.0
|
196
|
+
major = Regexp.last_match(1).to_i
|
197
|
+
VersionRange.new([
|
198
|
+
Interval.new(min: "#{major}.0.0", max: "#{major + 1}.0.0", min_inclusive: true, max_inclusive: false)
|
199
|
+
])
|
200
|
+
when /^(\d+)\.(\d+)\.x$/
|
201
|
+
# X-range like "1.2.x" := >=1.2.0 <1.3.0
|
202
|
+
major = Regexp.last_match(1).to_i
|
203
|
+
minor = Regexp.last_match(2).to_i
|
204
|
+
VersionRange.new([
|
205
|
+
Interval.new(min: "#{major}.#{minor}.0", max: "#{major}.#{minor + 1}.0", min_inclusive: true, max_inclusive: false)
|
206
|
+
])
|
207
|
+
when /^(blerg|git\+|https?:\/\/)/
|
208
|
+
# Invalid patterns that should raise errors
|
209
|
+
raise ArgumentError, "Invalid NPM range format: #{range_string}"
|
210
|
+
else
|
211
|
+
# Standard constraint
|
212
|
+
constraint = Constraint.parse(range_string)
|
213
|
+
if constraint.exclusion?
|
214
|
+
VersionRange.unbounded.exclude(constraint.version)
|
215
|
+
else
|
216
|
+
VersionRange.new([constraint.to_interval])
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def parse_caret_range(version)
|
222
|
+
v = Version.new(version)
|
223
|
+
if v.major > 0
|
224
|
+
# ^1.2.3 := >=1.2.3 <2.0.0
|
225
|
+
upper_version = "#{v.major + 1}.0.0"
|
226
|
+
elsif v.minor && v.minor > 0
|
227
|
+
# ^0.2.3 := >=0.2.3 <0.3.0
|
228
|
+
upper_version = "0.#{v.minor + 1}.0"
|
229
|
+
else
|
230
|
+
# ^0.0.3 := >=0.0.3 <0.0.4
|
231
|
+
upper_version = "0.0.#{(v.patch || 0) + 1}"
|
232
|
+
end
|
233
|
+
|
234
|
+
VersionRange.new([
|
235
|
+
Interval.new(min: version, max: upper_version, min_inclusive: true, max_inclusive: false)
|
236
|
+
])
|
237
|
+
end
|
238
|
+
|
239
|
+
def parse_tilde_range(version)
|
240
|
+
v = Version.new(version)
|
241
|
+
if v.minor
|
242
|
+
# ~1.2.3 := >=1.2.3 <1.3.0
|
243
|
+
upper_version = "#{v.major}.#{v.minor + 1}.0"
|
244
|
+
else
|
245
|
+
# ~1 := >=1.0.0 <2.0.0
|
246
|
+
upper_version = "#{v.major + 1}.0.0"
|
247
|
+
end
|
248
|
+
|
249
|
+
VersionRange.new([
|
250
|
+
Interval.new(min: version, max: upper_version, min_inclusive: true, max_inclusive: false)
|
251
|
+
])
|
252
|
+
end
|
253
|
+
|
254
|
+
# Gem range parsing (~>, >=, etc.)
|
255
|
+
def parse_gem_range(range_string)
|
256
|
+
if range_string.match(/^~>\s*(.+)$/)
|
257
|
+
# Pessimistic operator: ~> 1.2.3
|
258
|
+
version = Regexp.last_match(1).strip
|
259
|
+
parse_pessimistic_range(version)
|
260
|
+
else
|
261
|
+
# Standard constraints separated by commas
|
262
|
+
constraints = range_string.split(',').map(&:strip)
|
263
|
+
parse_constraints(constraints.join('|'), 'gem')
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def parse_pessimistic_range(version)
|
268
|
+
v = Version.new(version)
|
269
|
+
if v.patch
|
270
|
+
# ~> 1.2.3 := >= 1.2.3, < 1.3.0
|
271
|
+
upper_version = "#{v.major}.#{v.minor + 1}.0"
|
272
|
+
elsif v.minor
|
273
|
+
# ~> 1.2 := >= 1.2.0, < 2.0.0
|
274
|
+
upper_version = "#{v.major + 1}.0.0"
|
275
|
+
else
|
276
|
+
# ~> 1 := >= 1.0.0, < 2.0.0
|
277
|
+
upper_version = "#{v.major + 1}.0.0"
|
278
|
+
end
|
279
|
+
|
280
|
+
VersionRange.new([
|
281
|
+
Interval.new(min: version, max: upper_version, min_inclusive: true, max_inclusive: false)
|
282
|
+
])
|
283
|
+
end
|
284
|
+
|
285
|
+
# Python/PyPI range parsing
|
286
|
+
def parse_pypi_range(range_string)
|
287
|
+
# Handle comma-separated constraints
|
288
|
+
constraints = range_string.split(',').map(&:strip)
|
289
|
+
parse_constraints(constraints.join('|'), 'pypi')
|
290
|
+
end
|
291
|
+
|
292
|
+
# Maven range parsing
|
293
|
+
def parse_maven_range(range_string)
|
294
|
+
# Validate bracket notation first
|
295
|
+
if range_string.match(/^[\[\(].+[\]\)]$/)
|
296
|
+
# Check for malformed single version ranges
|
297
|
+
if range_string.match(/^\([^,]+\]$/) || range_string.match(/^\[[^,]+\)$/)
|
298
|
+
raise ArgumentError, "Malformed Maven range: mismatched brackets in '#{range_string}'"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
case range_string
|
303
|
+
when /^\[([^,]+),([^,]+)\]$/
|
304
|
+
# [1.0,2.0] := >=1.0 <=2.0
|
305
|
+
min_version = Regexp.last_match(1).strip
|
306
|
+
max_version = Regexp.last_match(2).strip
|
307
|
+
VersionRange.new([
|
308
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true)
|
309
|
+
])
|
310
|
+
when /^\(([^,]+),([^,]+)\)$/
|
311
|
+
# (1.0,2.0) := >1.0 <2.0
|
312
|
+
min_version = Regexp.last_match(1).strip
|
313
|
+
max_version = Regexp.last_match(2).strip
|
314
|
+
VersionRange.new([
|
315
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false)
|
316
|
+
])
|
317
|
+
when /^\[([^,]+),([^,]+)\)$/
|
318
|
+
# [1.0,2.0) := >=1.0 <2.0
|
319
|
+
min_version = Regexp.last_match(1).strip
|
320
|
+
max_version = Regexp.last_match(2).strip
|
321
|
+
VersionRange.new([
|
322
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false)
|
323
|
+
])
|
324
|
+
when /^\(([^,]+),([^,]+)\]$/
|
325
|
+
# (1.0,2.0] := >1.0 <=2.0
|
326
|
+
min_version = Regexp.last_match(1).strip
|
327
|
+
max_version = Regexp.last_match(2).strip
|
328
|
+
VersionRange.new([
|
329
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true)
|
330
|
+
])
|
331
|
+
when /^\[([^,]+)\]$/
|
332
|
+
# [1.0] := exactly 1.0
|
333
|
+
version = Regexp.last_match(1).strip
|
334
|
+
VersionRange.exact(version)
|
335
|
+
when /^\[([^,]+),\)$/
|
336
|
+
# [1.0,) := >=1.0
|
337
|
+
min_version = Regexp.last_match(1).strip
|
338
|
+
VersionRange.new([
|
339
|
+
Interval.new(min: min_version, min_inclusive: true)
|
340
|
+
])
|
341
|
+
when /^\(([^,]+),\)$/
|
342
|
+
# (1.0,) := >1.0
|
343
|
+
min_version = Regexp.last_match(1).strip
|
344
|
+
VersionRange.new([
|
345
|
+
Interval.new(min: min_version, min_inclusive: false)
|
346
|
+
])
|
347
|
+
when /^\(,([^,]+)\]$/
|
348
|
+
# (,1.0] := <=1.0
|
349
|
+
max_version = Regexp.last_match(1).strip
|
350
|
+
VersionRange.new([
|
351
|
+
Interval.new(max: max_version, max_inclusive: true)
|
352
|
+
])
|
353
|
+
when /^\(,([^,]+)\)$/
|
354
|
+
# (,1.0) := <1.0
|
355
|
+
max_version = Regexp.last_match(1).strip
|
356
|
+
VersionRange.new([
|
357
|
+
Interval.new(max: max_version, max_inclusive: false)
|
358
|
+
])
|
359
|
+
when /^[0-9]/
|
360
|
+
# Simple version number without brackets - in Maven, this is minimum version
|
361
|
+
if range_string.match(/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9.-]+)?$/)
|
362
|
+
VersionRange.new([
|
363
|
+
Interval.new(min: range_string, min_inclusive: true)
|
364
|
+
])
|
365
|
+
else
|
366
|
+
parse_constraints(range_string, 'maven')
|
367
|
+
end
|
368
|
+
when /^(.+),(.+)$/
|
369
|
+
# Handle union ranges like "(,1.0],[1.2,)"
|
370
|
+
parts = range_string.split(',')
|
371
|
+
if parts.length > 2
|
372
|
+
# Complex union - parse each part recursively
|
373
|
+
ranges = []
|
374
|
+
# Split and preserve bracket information
|
375
|
+
# Find all individual ranges by splitting on comma between brackets
|
376
|
+
individual_ranges = []
|
377
|
+
remaining = range_string.strip
|
378
|
+
|
379
|
+
while remaining.length > 0
|
380
|
+
# Find the next complete bracket range
|
381
|
+
if match = remaining.match(/^[\[\(][^\[\]\(\)]*[\]\)]/)
|
382
|
+
individual_ranges << match[0].strip
|
383
|
+
remaining = remaining[match.end(0)..-1].strip
|
384
|
+
# Skip over comma and whitespace
|
385
|
+
remaining = remaining.sub(/^\s*,\s*/, '')
|
386
|
+
else
|
387
|
+
break
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
if individual_ranges.length > 1
|
392
|
+
individual_ranges.each do |range_part|
|
393
|
+
begin
|
394
|
+
parsed_range = parse_maven_range(range_part)
|
395
|
+
ranges << parsed_range
|
396
|
+
rescue
|
397
|
+
# If parsing fails, skip this part
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
if ranges.any?
|
402
|
+
return ranges.reduce { |acc, range| acc.union(range) }
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Fall back to standard constraint parsing
|
408
|
+
parse_constraints(range_string, 'maven')
|
409
|
+
else
|
410
|
+
# Fall back to standard constraint parsing
|
411
|
+
parse_constraints(range_string, 'maven')
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# NuGet range parsing (similar to Maven but with some differences)
|
416
|
+
def parse_nuget_range(range_string)
|
417
|
+
# NuGet uses the same bracket notation as Maven
|
418
|
+
# But simple version strings like "1.0" are minimum versions, not exact
|
419
|
+
case range_string
|
420
|
+
when /^[\[\(].+[\]\)]$/
|
421
|
+
# Use Maven parsing for bracket notation
|
422
|
+
parse_maven_range(range_string)
|
423
|
+
when /^[0-9]/
|
424
|
+
# Simple version number - treat as minimum version for NuGet
|
425
|
+
VersionRange.new([
|
426
|
+
Interval.new(min: range_string, min_inclusive: true)
|
427
|
+
])
|
428
|
+
else
|
429
|
+
# Fall back to standard constraint parsing
|
430
|
+
parse_constraints(range_string, 'nuget')
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Debian range parsing
|
435
|
+
def parse_debian_range(range_string)
|
436
|
+
# Debian uses operators like >=, <=, =, >>, <<
|
437
|
+
range_string = range_string.gsub('>>', '>').gsub('<<', '<')
|
438
|
+
parse_constraints(range_string, 'deb')
|
439
|
+
end
|
440
|
+
|
441
|
+
# RPM range parsing
|
442
|
+
def parse_rpm_range(range_string)
|
443
|
+
# RPM uses similar operators to Debian
|
444
|
+
parse_constraints(range_string, 'rpm')
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|