Class: Vers::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/vers/parser.rb

Overview

Parses vers URI strings and package manager specific version ranges

This class handles parsing of vers URI format (e.g., "vers:npm/>=1.2.3|<2.0.0") and provides extensible support for different package ecosystem syntaxes.

Examples

parser = Vers::Parser.new
range = parser.parse("vers:npm/>=1.2.3|<2.0.0")
range.contains?("1.5.0")  # => true

Constant Summary collapse

NGINX_RANGE_REGEX =
/\A\d+(?:\.\d+)+-\d+(?:\.\d+)+\z/
OPERATOR_PREFIX_REGEX =
/\A[><=!]+/
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-]+)*)?/
SEMVER_OUTPUT_SCHEMES =
%w[npm cargo nuget composer pub].freeze
VERS_META_ENCODINGS =
{
  "%" => "%25",
  "|" => "%7C",
  ">" => "%3E",
  "<" => "%3C",
  "=" => "%3D",
  "!" => "%21",
  "/" => "%2F",
  "*" => "%2A",
  " " => "%20"
}.freeze
MAX_INPUT_LENGTH =

Maximum accepted length for a range string at parse/parse_native entry points. Range strings concatenate multiple constraints so this is set higher than Version::MAX_LENGTH while still bounding split/regex work to a few KB.

2048
MAX_CONSTRAINTS =

Maximum number of |-separated or ||-separated constraints in a single range. The exclusion loop in parse_constraints does O(n^2 log n) work as each != splits an interval and reconstructs the range; capping n keeps the worst case under a few thousand interval operations.

64

Instance Method Summary collapse

Instance Method Details

#parse(vers_string, require_canonical_order: false) ⇒ VersionRange

Parses a vers URI string into a VersionRange

Examples

parser = Vers::Parser.new
parser.parse("vers:npm/>=1.2.3|<2.0.0")
parser.parse_native("~>1.0", "gem")
parser.parse_native("==1.2.3", "pypi")

Parameters:

  • vers_string (String)

    The vers URI string to parse

Returns:

Raises:

  • (ArgumentError)

    if the vers string is invalid



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/vers/parser.rb', line 62

def parse(vers_string, require_canonical_order: false)
  validate_input_length!(vers_string)

  return VersionRange.unbounded if vers_string == "*"
  unless vers_string.is_a?(String) && vers_string.start_with?("vers:")
    raise ArgumentError, "Invalid vers URI format: #{vers_string}"
  end
  if vers_string.match?(/[ \t\r\n]/)
    raise ArgumentError, "non-canonical VERS: whitespace is not permitted"
  end

  remainder = vers_string.delete_prefix("vers:")
  slash = remainder.index("/")
  unless slash && slash.positive?
    raise ArgumentError, "Invalid vers URI format: #{vers_string}"
  end

  raw_scheme = remainder[...slash]
  if raw_scheme != raw_scheme.downcase
    raise ArgumentError, "non-canonical VERS: type must be lowercase"
  end
  scheme = Scheme.canonical(raw_scheme)
  constraints_string = remainder[(slash + 1)..]
  if constraints_string.empty? || constraints_string == "*"
    return VersionRange.unbounded(scheme: scheme)
  end

  validate_vers_constraints!(constraints_string, scheme, require_canonical_order)

  parse_constraints(constraints_string, scheme, decode_versions: true)
end

#parse_native(range_string, scheme) ⇒ VersionRange

Parses a native package manager version range into a VersionRange

Examples

parser = Vers::Parser.new
parser.parse_native("^1.2.3", "npm")
parser.parse_native("~> 1.0", "gem")
parser.parse_native(">=1.0,<2.0", "pypi")

Parameters:

  • range_string (String)

    The native version range string

  • scheme (String)

    The package manager scheme (npm, gem, pypi, etc.)

Returns:



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/vers/parser.rb', line 108

def parse_native(range_string, scheme)
  validate_input_length!(range_string)

  canonical_scheme = Scheme.canonical(scheme)
  range = case canonical_scheme
  when "npm"
    parse_npm_range(range_string, scheme: "npm")
  when "gem"
    parse_gem_range(range_string)
  when "pypi"
    parse_pypi_range(range_string)
  when "composer"
    parse_composer_range(range_string)
  when "pub"
    parse_pub_range(range_string)
  when "conan"
    parse_conan_range(range_string)
  when "nginx"
    parse_nginx_range(range_string)
  when "openssl"
    parse_openssl_range(range_string)
  when "maven"
    parse_maven_range(range_string)
  when "cargo"
    parse_npm_range(range_string, scheme: "cargo")
  when "nuget"
    parse_nuget_range(range_string)
  when "hex"
    parse_hex_range(range_string)
  when "go"
    parse_go_range(range_string)
  when "deb"
    parse_debian_range(range_string)
  when "rpm"
    parse_rpm_range(range_string)
  else
    # Fall back to generic constraint parsing
    parse_constraints(range_string, canonical_scheme)
  end
  range.with_scheme(canonical_scheme)
end

#to_vers_string(version_range, scheme) ⇒ String

Converts a VersionRange back to a vers URI string

Parameters:

  • version_range (VersionRange)

    The version range to convert

  • scheme (String)

    The package manager scheme

Returns:

  • (String)

    The vers URI string



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/vers/parser.rb', line 157

def to_vers_string(version_range, scheme)
  canonical_scheme = Scheme.canonical(scheme)
  if version_range.scheme && canonical_scheme != version_range.scheme
    raise ArgumentError, "Cannot serialize a #{version_range.scheme} range as #{canonical_scheme}"
  end

  scheme = canonical_scheme
  if version_range.unbounded? && (!version_range.raw_constraints || version_range.raw_constraints.empty?)
    return "vers:#{scheme}/*"
  end
  if version_range.empty? && (!version_range.raw_constraints || version_range.raw_constraints.empty?)
    return "vers:#{scheme}/"
  end

  intervals = serialization_intervals(version_range, scheme)
  constraints = []

  # Detect != pattern: two intervals (-∞,V) ∪ (V,+∞)
  if intervals.length == 2
    a, b = intervals
    if a.min.nil? && !a.max_inclusive && b.max.nil? && !b.min_inclusive && a.max == b.min
      version = encode_vers_version(normalize_vers_version(a.max, scheme))
      constraints << "!=#{version}"
      sort_constraints!(constraints, scheme)
      return "vers:#{scheme}/#{constraints.join('|')}"
    end
  end

  intervals.each do |interval|
    next if interval.unbounded?

    if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
      # Exact version
      constraints << encode_vers_version(normalize_vers_version(interval.min.to_s, scheme))
    else
      # Range constraints
      if interval.min
        operator = interval.min_inclusive ? ">=" : ">"
        version = encode_vers_version(normalize_vers_version(interval.min, scheme))
        constraints << "#{operator}#{version}"
      end

      if interval.max
        operator = interval.max_inclusive ? "<=" : "<"
        version = encode_vers_version(normalize_vers_version(interval.max, scheme))
        constraints << "#{operator}#{version}"
      end
    end
  end

  version_range.exclusions.each do |version|
    normalized = normalize_vers_version(version, scheme)
    constraints << "!=#{encode_vers_version(normalized)}"
  end

  sort_constraints!(constraints, scheme)

  "vers:#{scheme}/#{constraints.join('|')}"
end