vers 1.2.1 → 1.3.1
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/Rakefile +97 -49
- data/lib/vers/constraint.rb +12 -7
- data/lib/vers/interval.rb +24 -14
- data/lib/vers/parser.rb +41 -12
- data/lib/vers/version.rb +29 -13
- data/lib/vers/version_range.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a8066da910f228939dadbc96afeadcf5122c99ed9a06a67b9858c4720d7e070
|
|
4
|
+
data.tar.gz: 59204f511b9adfb7863d97f754915fe96725093e6f0fa4dc7bfc3f2703e5e387
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4af64390c9efd167082d891eb192f051a04e231a889e751a646e7ce93d55d5736c5f98368ca1b83b6e4adaebc7f962ce4282155eabc245c522ed142a63dda27
|
|
7
|
+
data.tar.gz: 119edf96cc65e3fc88d2f89b543ecbae593c6afeb136db0144793c981999476925ae5c671508965676b4d31d2a2cf1d4858feaaab45f443110561a5b74c00b06
|
data/Rakefile
CHANGED
|
@@ -187,74 +187,122 @@ namespace :benchmark do
|
|
|
187
187
|
task :memory do
|
|
188
188
|
require "benchmark"
|
|
189
189
|
require "json"
|
|
190
|
+
require "objspace"
|
|
190
191
|
require_relative "lib/vers"
|
|
191
|
-
|
|
192
|
-
puts "
|
|
192
|
+
|
|
193
|
+
puts "VERS Memory & Allocation Benchmarks"
|
|
193
194
|
puts "=" * 50
|
|
194
|
-
|
|
195
|
-
# Load sample ranges
|
|
195
|
+
|
|
196
196
|
test_data_file = File.join(__dir__, "test-suite-data.json")
|
|
197
|
-
|
|
197
|
+
|
|
198
198
|
unless File.exist?(test_data_file)
|
|
199
|
-
puts "
|
|
199
|
+
puts "test-suite-data.json not found. Using fallback examples."
|
|
200
200
|
sample_ranges = [
|
|
201
|
-
{ input
|
|
202
|
-
{ input
|
|
203
|
-
{ input
|
|
201
|
+
{ "input" => "^1.2.3", "scheme" => "npm" },
|
|
202
|
+
{ "input" => "~> 1.2", "scheme" => "gem" },
|
|
203
|
+
{ "input" => ">=1.0,<2.0", "scheme" => "pypi" }
|
|
204
204
|
]
|
|
205
205
|
else
|
|
206
206
|
test_data = JSON.parse(File.read(test_data_file))
|
|
207
207
|
sample_ranges = test_data.select { |data| !data["is_invalid"] }.first(100)
|
|
208
208
|
end
|
|
209
|
-
|
|
210
|
-
puts "
|
|
209
|
+
|
|
210
|
+
puts "Sample size: #{sample_ranges.length} version ranges"
|
|
211
211
|
puts
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
|
|
212
|
+
|
|
213
|
+
# Measure object allocations during cold parsing (no cache)
|
|
214
|
+
Vers::Version.class_variable_set(:@@version_cache, {})
|
|
215
|
+
Vers::Constraint.class_variable_set(:@@constraint_cache, {})
|
|
216
|
+
Vers::Parser.class_variable_set(:@@parser_cache, {})
|
|
217
|
+
|
|
218
|
+
GC.start
|
|
219
|
+
GC.disable
|
|
220
|
+
before = ObjectSpace.count_objects.dup
|
|
221
|
+
|
|
215
222
|
version_ranges = []
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
begin
|
|
220
|
-
parsed = Vers.parse_native(range['input'], range['scheme'])
|
|
221
|
-
version_ranges << parsed
|
|
222
|
-
rescue
|
|
223
|
-
# Skip invalid ranges
|
|
224
|
-
end
|
|
225
|
-
end
|
|
223
|
+
sample_ranges.each do |range|
|
|
224
|
+
parsed = Vers.parse_native(range['input'], range['scheme']) rescue nil
|
|
225
|
+
version_ranges << parsed if parsed
|
|
226
226
|
end
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
|
|
228
|
+
after = ObjectSpace.count_objects
|
|
229
|
+
GC.enable
|
|
230
|
+
|
|
231
|
+
string_alloc = after[:T_STRING] - before[:T_STRING]
|
|
232
|
+
array_alloc = after[:T_ARRAY] - before[:T_ARRAY]
|
|
233
|
+
hash_alloc = after[:T_HASH] - before[:T_HASH]
|
|
234
|
+
object_alloc = after[:T_OBJECT] - before[:T_OBJECT]
|
|
235
|
+
match_alloc = (after[:T_MATCH] || 0) - (before[:T_MATCH] || 0)
|
|
236
|
+
total_alloc = string_alloc + array_alloc + hash_alloc + object_alloc + match_alloc
|
|
237
|
+
|
|
238
|
+
puts "Cold parse allocations (#{version_ranges.length} ranges, no cache):"
|
|
239
|
+
puts " Total objects: #{total_alloc}"
|
|
240
|
+
puts " Strings: #{string_alloc}"
|
|
241
|
+
puts " Arrays: #{array_alloc}"
|
|
242
|
+
puts " Hashes: #{hash_alloc}"
|
|
243
|
+
puts " Objects: #{object_alloc}"
|
|
244
|
+
puts " MatchData: #{match_alloc}"
|
|
245
|
+
puts " Per range: #{(total_alloc.to_f / version_ranges.length).round(1)}"
|
|
230
246
|
puts
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
|
|
248
|
+
# Measure cached parse allocations (everything already cached)
|
|
249
|
+
GC.start
|
|
250
|
+
GC.disable
|
|
251
|
+
before = ObjectSpace.count_objects.dup
|
|
252
|
+
|
|
253
|
+
sample_ranges.each do |range|
|
|
254
|
+
Vers.parse_native(range['input'], range['scheme']) rescue nil
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
after = ObjectSpace.count_objects
|
|
258
|
+
GC.enable
|
|
259
|
+
|
|
260
|
+
cached_strings = after[:T_STRING] - before[:T_STRING]
|
|
261
|
+
cached_arrays = after[:T_ARRAY] - before[:T_ARRAY]
|
|
262
|
+
cached_objects = after[:T_OBJECT] - before[:T_OBJECT]
|
|
263
|
+
cached_match = (after[:T_MATCH] || 0) - (before[:T_MATCH] || 0)
|
|
264
|
+
cached_alloc = cached_strings + cached_arrays + cached_objects + cached_match
|
|
265
|
+
|
|
266
|
+
puts "Cached parse allocations (#{version_ranges.length} ranges, warm cache):"
|
|
267
|
+
puts " Total objects: #{cached_alloc}"
|
|
268
|
+
puts " Strings: #{cached_strings}"
|
|
269
|
+
puts " MatchData: #{cached_match}"
|
|
270
|
+
puts " Per range: #{(cached_alloc.to_f / version_ranges.length).round(1)}"
|
|
237
271
|
puts
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
272
|
+
|
|
273
|
+
# Measure memory size of retained objects
|
|
274
|
+
total_memsize = version_ranges.sum { |r| ObjectSpace.memsize_of(r) }
|
|
275
|
+
puts "Retained memory:"
|
|
276
|
+
puts " #{version_ranges.length} VersionRange objects: #{total_memsize} bytes"
|
|
277
|
+
puts " Average per object: #{(total_memsize.to_f / version_ranges.length).round(0)} bytes"
|
|
278
|
+
puts
|
|
279
|
+
|
|
280
|
+
# Repeated operation allocations
|
|
281
|
+
puts "Repeated operation allocations (#{version_ranges.length} calls each):"
|
|
282
|
+
|
|
242
283
|
operations = {
|
|
243
|
-
"to_s
|
|
244
|
-
"contains?
|
|
245
|
-
"empty?
|
|
246
|
-
"unbounded? check" => proc { version_ranges.each(&:unbounded?) }
|
|
284
|
+
"to_s" => proc { version_ranges.each(&:to_s) },
|
|
285
|
+
"contains?" => proc { version_ranges.each { |r| r.contains?("1.5.0") } },
|
|
286
|
+
"empty?" => proc { version_ranges.each(&:empty?) },
|
|
247
287
|
}
|
|
248
|
-
|
|
249
|
-
operations.each do |
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
288
|
+
|
|
289
|
+
operations.each do |name, op|
|
|
290
|
+
# Warm up
|
|
291
|
+
op.call
|
|
292
|
+
|
|
293
|
+
GC.start
|
|
294
|
+
GC.disable
|
|
295
|
+
before = ObjectSpace.count_objects.dup
|
|
296
|
+
op.call
|
|
297
|
+
after = ObjectSpace.count_objects
|
|
298
|
+
GC.enable
|
|
299
|
+
|
|
300
|
+
allocs = [:T_STRING, :T_ARRAY, :T_OBJECT, :T_MATCH, :T_HASH].sum { |k| (after[k] || 0) - (before[k] || 0) }
|
|
301
|
+
puts " #{name.ljust(15)}: #{allocs} allocations"
|
|
254
302
|
end
|
|
255
|
-
|
|
303
|
+
|
|
256
304
|
puts
|
|
257
|
-
puts "
|
|
305
|
+
puts "Memory benchmark completed!"
|
|
258
306
|
end
|
|
259
307
|
|
|
260
308
|
desc "Run complexity stress tests"
|
data/lib/vers/constraint.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Vers
|
|
|
23
23
|
|
|
24
24
|
# Cache for parsed constraints
|
|
25
25
|
@@constraint_cache = {}
|
|
26
|
-
@@cache_size_limit =
|
|
26
|
+
@@cache_size_limit = 1000
|
|
27
27
|
|
|
28
28
|
attr_reader :operator, :version
|
|
29
29
|
|
|
@@ -54,14 +54,19 @@ module Vers
|
|
|
54
54
|
# Vers::Constraint.parse("!=2.0.0") # => #<Vers::Constraint:0x... @operator="!=", @version="2.0.0">
|
|
55
55
|
#
|
|
56
56
|
def self.parse(constraint_string)
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
# Bound input length before cache lookup so oversized strings never
|
|
58
|
+
# become cache keys and never trigger eviction.
|
|
59
|
+
if constraint_string.length > Version::MAX_LENGTH
|
|
60
|
+
raise ArgumentError, "Constraint string too long (#{constraint_string.length} > #{Version::MAX_LENGTH})"
|
|
60
61
|
end
|
|
61
|
-
|
|
62
|
-
# Return cached constraint if available
|
|
62
|
+
|
|
63
63
|
return @@constraint_cache[constraint_string] if @@constraint_cache.key?(constraint_string)
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
if @@constraint_cache.size >= @@cache_size_limit
|
|
66
|
+
keys = @@constraint_cache.keys
|
|
67
|
+
keys.first(keys.size / 2).each { |k| @@constraint_cache.delete(k) }
|
|
68
|
+
end
|
|
69
|
+
|
|
65
70
|
constraint = parse_uncached(constraint_string)
|
|
66
71
|
@@constraint_cache[constraint_string] = constraint
|
|
67
72
|
constraint
|
data/lib/vers/interval.rb
CHANGED
|
@@ -12,8 +12,7 @@ module Vers
|
|
|
12
12
|
@min_inclusive = min_inclusive
|
|
13
13
|
@max_inclusive = max_inclusive
|
|
14
14
|
@scheme = scheme
|
|
15
|
-
|
|
16
|
-
validate_bounds!
|
|
15
|
+
@empty = compute_empty
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
def self.empty(scheme: nil)
|
|
@@ -37,9 +36,7 @@ module Vers
|
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def empty?
|
|
40
|
-
|
|
41
|
-
return true if min && max && version_compare(min, max) == 0 && (!min_inclusive || !max_inclusive)
|
|
42
|
-
false
|
|
39
|
+
@empty
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
def unbounded?
|
|
@@ -180,7 +177,22 @@ module Vers
|
|
|
180
177
|
|
|
181
178
|
def overlaps?(other)
|
|
182
179
|
return false if empty? || other.empty?
|
|
183
|
-
|
|
180
|
+
return true if unbounded? || other.unbounded?
|
|
181
|
+
|
|
182
|
+
# Check if the intervals can't overlap by comparing bounds directly
|
|
183
|
+
if max && other.min
|
|
184
|
+
cmp = version_compare(max, other.min)
|
|
185
|
+
return false if cmp < 0
|
|
186
|
+
return false if cmp == 0 && (!max_inclusive || !other.min_inclusive)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if min && other.max
|
|
190
|
+
cmp = version_compare(min, other.max)
|
|
191
|
+
return false if cmp > 0
|
|
192
|
+
return false if cmp == 0 && (!min_inclusive || !other.max_inclusive)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
true
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
def adjacent?(other)
|
|
@@ -211,14 +223,12 @@ module Vers
|
|
|
211
223
|
|
|
212
224
|
private
|
|
213
225
|
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
elsif comparison == 0 && (!min_inclusive || !max_inclusive)
|
|
221
|
-
return
|
|
226
|
+
def compute_empty
|
|
227
|
+
if min && max
|
|
228
|
+
cmp = version_compare(min, max)
|
|
229
|
+
cmp > 0 || (cmp == 0 && (!min_inclusive || !max_inclusive))
|
|
230
|
+
else
|
|
231
|
+
false
|
|
222
232
|
end
|
|
223
233
|
end
|
|
224
234
|
|
data/lib/vers/parser.rb
CHANGED
|
@@ -26,10 +26,24 @@ module Vers
|
|
|
26
26
|
NPM_HYPHEN_REGEX = /\A(.+?)\s+-\s+(.+)\z/
|
|
27
27
|
NPM_X_RANGE_MAJOR_REGEX = /\A(\d+)\.x\z/
|
|
28
28
|
NPM_X_RANGE_MINOR_REGEX = /\A(\d+)\.(\d+)\.x\z/
|
|
29
|
+
OPERATOR_PREFIX_REGEX = /\A[><=!]+/
|
|
29
30
|
|
|
30
31
|
# Cache for parsed ranges to improve performance
|
|
31
32
|
@@parser_cache = {}
|
|
32
|
-
@@cache_size_limit =
|
|
33
|
+
@@cache_size_limit = 500
|
|
34
|
+
|
|
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
|
|
33
47
|
|
|
34
48
|
##
|
|
35
49
|
# Parses a vers URI string into a VersionRange
|
|
@@ -46,6 +60,8 @@ module Vers
|
|
|
46
60
|
# parser.parse("vers:pypi/==1.2.3")
|
|
47
61
|
#
|
|
48
62
|
def parse(vers_string)
|
|
63
|
+
validate_input_length!(vers_string)
|
|
64
|
+
|
|
49
65
|
if vers_string == "*"
|
|
50
66
|
return VersionRange.unbounded
|
|
51
67
|
end
|
|
@@ -74,6 +90,8 @@ module Vers
|
|
|
74
90
|
# parser.parse_native(">=1.0,<2.0", "pypi")
|
|
75
91
|
#
|
|
76
92
|
def parse_native(range_string, scheme)
|
|
93
|
+
validate_input_length!(range_string)
|
|
94
|
+
|
|
77
95
|
case scheme
|
|
78
96
|
when "npm"
|
|
79
97
|
parse_npm_range(range_string)
|
|
@@ -150,14 +168,25 @@ module Vers
|
|
|
150
168
|
|
|
151
169
|
private
|
|
152
170
|
|
|
171
|
+
def validate_input_length!(input)
|
|
172
|
+
return if input.nil?
|
|
173
|
+
return if input.length <= MAX_INPUT_LENGTH
|
|
174
|
+
raise ArgumentError, "Range string too long (#{input.length} > #{MAX_INPUT_LENGTH})"
|
|
175
|
+
end
|
|
176
|
+
|
|
153
177
|
def sort_key_for_constraint(constraint)
|
|
154
|
-
version = constraint.sub(
|
|
178
|
+
version = constraint.sub(OPERATOR_PREFIX_REGEX, '')
|
|
155
179
|
v = Version.cached_new(version)
|
|
156
180
|
[v, constraint]
|
|
157
181
|
end
|
|
158
182
|
|
|
159
183
|
def parse_constraints(constraints_string, scheme)
|
|
160
|
-
|
|
184
|
+
# Limit constraint count to bound the O(n^2 log n) exclusion loop
|
|
185
|
+
# below: each != splits an interval and reconstructs the range.
|
|
186
|
+
constraint_strings = constraints_string.split(/[|,]/, MAX_CONSTRAINTS + 1)
|
|
187
|
+
if constraint_strings.length > MAX_CONSTRAINTS
|
|
188
|
+
raise ArgumentError, "Too many constraints (> #{MAX_CONSTRAINTS})"
|
|
189
|
+
end
|
|
161
190
|
intervals = []
|
|
162
191
|
exclusions = []
|
|
163
192
|
interval_scheme = %w[maven nuget].include?(scheme) ? scheme : nil
|
|
@@ -199,7 +228,10 @@ module Vers
|
|
|
199
228
|
|
|
200
229
|
# Handle || (OR) operator
|
|
201
230
|
if range_string.include?('||')
|
|
202
|
-
or_parts = range_string.split('||').map(&:strip)
|
|
231
|
+
or_parts = range_string.split('||', MAX_CONSTRAINTS + 1).map(&:strip)
|
|
232
|
+
if or_parts.length > MAX_CONSTRAINTS
|
|
233
|
+
raise ArgumentError, "Too many || clauses (> #{MAX_CONSTRAINTS})"
|
|
234
|
+
end
|
|
203
235
|
ranges = or_parts.map { |part| parse_npm_range(part) }
|
|
204
236
|
return ranges.reduce { |acc, range| acc.union(range) }
|
|
205
237
|
end
|
|
@@ -231,15 +263,12 @@ module Vers
|
|
|
231
263
|
end
|
|
232
264
|
|
|
233
265
|
def parse_npm_single_range(range_string)
|
|
234
|
-
# Check cache first
|
|
235
266
|
cache_key = "npm:#{range_string}"
|
|
236
|
-
if @@parser_cache.key?(cache_key)
|
|
237
|
-
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Limit cache size
|
|
267
|
+
return @@parser_cache[cache_key] if @@parser_cache.key?(cache_key)
|
|
268
|
+
|
|
241
269
|
if @@parser_cache.size >= @@cache_size_limit
|
|
242
|
-
@@parser_cache.
|
|
270
|
+
keys = @@parser_cache.keys
|
|
271
|
+
keys.first(keys.size / 2).each { |k| @@parser_cache.delete(k) }
|
|
243
272
|
end
|
|
244
273
|
|
|
245
274
|
result = case range_string
|
|
@@ -500,7 +529,7 @@ module Vers
|
|
|
500
529
|
begin
|
|
501
530
|
parsed_range = parse_maven_range(range_part)
|
|
502
531
|
ranges << parsed_range
|
|
503
|
-
rescue
|
|
532
|
+
rescue ArgumentError
|
|
504
533
|
# If parsing fails, skip this part
|
|
505
534
|
end
|
|
506
535
|
end
|
data/lib/vers/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Vers
|
|
4
|
-
VERSION = "1.
|
|
4
|
+
VERSION = "1.3.1"
|
|
5
5
|
|
|
6
6
|
##
|
|
7
7
|
# Handles version comparison and normalization across different package ecosystems.
|
|
@@ -18,19 +18,28 @@ module Vers
|
|
|
18
18
|
class Version
|
|
19
19
|
# Cache for parsed versions to avoid repeated parsing
|
|
20
20
|
@@version_cache = {}
|
|
21
|
-
@@cache_size_limit =
|
|
21
|
+
@@cache_size_limit = 2000
|
|
22
22
|
# Regex for parsing semantic version components including build metadata
|
|
23
23
|
SEMANTIC_VERSION_REGEX = /\A(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([^+]+))?(?:\+(.+))?\z/
|
|
24
24
|
|
|
25
|
+
# Maximum accepted length for a version string. Real-world version
|
|
26
|
+
# strings rarely exceed 100 characters; 256 leaves headroom for unusual
|
|
27
|
+
# prerelease tags while bounding regex/split work and cache key size.
|
|
28
|
+
MAX_LENGTH = 256
|
|
29
|
+
|
|
25
30
|
attr_reader :major, :minor, :patch, :prerelease, :build
|
|
26
31
|
|
|
27
32
|
##
|
|
28
33
|
# Creates a new Version object
|
|
29
34
|
#
|
|
30
35
|
# @param version_string [String] The version string to parse
|
|
36
|
+
# @raise [ArgumentError] if the version string exceeds MAX_LENGTH
|
|
31
37
|
#
|
|
32
38
|
def initialize(version_string)
|
|
33
39
|
@original = version_string.to_s
|
|
40
|
+
if @original.length > MAX_LENGTH
|
|
41
|
+
raise ArgumentError, "Version string too long (#{@original.length} > #{MAX_LENGTH})"
|
|
42
|
+
end
|
|
34
43
|
parse_version
|
|
35
44
|
end
|
|
36
45
|
|
|
@@ -41,11 +50,16 @@ module Vers
|
|
|
41
50
|
# @return [Version] Cached or new Version object
|
|
42
51
|
#
|
|
43
52
|
def self.cached_new(version_string)
|
|
44
|
-
#
|
|
53
|
+
# Skip caching for oversized keys to bound cache memory by entry
|
|
54
|
+
# count, not by attacker-controlled key length.
|
|
55
|
+
return new(version_string) if version_string.to_s.length > MAX_LENGTH
|
|
56
|
+
|
|
45
57
|
if @@version_cache.size >= @@cache_size_limit
|
|
46
|
-
|
|
58
|
+
# Keep the most recent half instead of clearing everything
|
|
59
|
+
keys = @@version_cache.keys
|
|
60
|
+
keys.first(keys.size / 2).each { |k| @@version_cache.delete(k) }
|
|
47
61
|
end
|
|
48
|
-
|
|
62
|
+
|
|
49
63
|
@@version_cache[version_string] ||= new(version_string)
|
|
50
64
|
end
|
|
51
65
|
|
|
@@ -145,15 +159,17 @@ module Vers
|
|
|
145
159
|
# @return [String] The normalized version string
|
|
146
160
|
#
|
|
147
161
|
def to_s
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
@to_s ||= begin
|
|
163
|
+
version = "#{major || 0}"
|
|
164
|
+
version += ".#{minor || 0}"
|
|
165
|
+
version += ".#{patch || 0}"
|
|
166
|
+
version += "-#{prerelease}" if prerelease
|
|
167
|
+
version.freeze
|
|
168
|
+
end
|
|
153
169
|
end
|
|
154
170
|
|
|
155
171
|
def ==(other)
|
|
156
|
-
other.is_a?(Version) && self <=> other == 0
|
|
172
|
+
other.is_a?(Version) && (self <=> other) == 0
|
|
157
173
|
end
|
|
158
174
|
|
|
159
175
|
def <(other)
|
|
@@ -320,7 +336,7 @@ module Vers
|
|
|
320
336
|
@original = @original.sub(/\Av/i, '')
|
|
321
337
|
|
|
322
338
|
# Handle simple numeric versions (optimized case)
|
|
323
|
-
if @original.match(/^\d+$/)
|
|
339
|
+
if @original.match?(/^\d+$/)
|
|
324
340
|
@major = @original.to_i
|
|
325
341
|
return
|
|
326
342
|
end
|
|
@@ -382,7 +398,7 @@ module Vers
|
|
|
382
398
|
return 1 if part_b.nil?
|
|
383
399
|
|
|
384
400
|
# Try numeric comparison first
|
|
385
|
-
if part_a.match(/^\d+$/) && part_b.match(/^\d+$/)
|
|
401
|
+
if part_a.match?(/^\d+$/) && part_b.match?(/^\d+$/)
|
|
386
402
|
numeric_cmp = part_a.to_i <=> part_b.to_i
|
|
387
403
|
return numeric_cmp unless numeric_cmp == 0
|
|
388
404
|
else
|
data/lib/vers/version_range.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Vers
|
|
|
9
9
|
|
|
10
10
|
def initialize(intervals = [], raw_constraints: nil, scheme: nil)
|
|
11
11
|
@scheme = scheme
|
|
12
|
-
@intervals = intervals.
|
|
12
|
+
@intervals = intervals.select { |i| i && !i.empty? }
|
|
13
13
|
if @scheme
|
|
14
14
|
@intervals.sort! { |a, b| compare_interval_bounds(a, b) }
|
|
15
15
|
else
|