1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 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 149 150 151 152 153 154 155 156 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 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
|
# frozen_string_literal: true
require 'semver_dialects/version'
require 'semver_dialects/base_version'
require 'semver_dialects/maven'
require 'semver_dialects/rpm'
require 'semver_dialects/apk'
require 'semver_dialects/semver2'
require 'semver_dialects/semantic_version'
require 'semver_dialects/boundary'
require 'semver_dialects/interval'
require 'semver_dialects/interval_parser'
require 'semver_dialects/interval_set'
require 'semver_dialects/interval_set_parser'
require 'deb_version'
module SemverDialects # rubocop:todo Style/Documentation
# Captures all errors that could be possibly raised
class Error < StandardError
end
class UnsupportedPackageTypeError < Error # rubocop:todo Style/Documentation
def initialize(pkg_type)
super
@pkg_type = pkg_type
end
def message
supported_types = SemverDialects.supported_package_types.join(', ')
"unsupported package type '#{@pkg_type}'. Supported types are: #{supported_types}"
end
end
class UnsupportedVersionError < Error # rubocop:todo Style/Documentation
def initialize(raw_version)
super
@raw_version = raw_version
end
def message
"unsupported version '#{@raw_version}'"
end
end
class InvalidVersionError < Error # rubocop:todo Style/Documentation
def initialize(raw_version)
super
@raw_version = raw_version
end
def message
"invalid version '#{@raw_version}'"
end
end
class InvalidConstraintError < Error # rubocop:todo Style/Documentation
def initialize(raw_constraint)
super
@raw_constraint = raw_constraint
end
def message
"invalid constraint '#{@raw_constraint}'"
end
end
class IncompleteScanError < InvalidVersionError # rubocop:todo Style/Documentation
attr_reader :rest
def initialize(rest)
super
@rest = rest
end
def message
"scan did not consume '#{@rest}'"
end
end
# Determines if a version of a given package type satisfies a constraint.
#
# On normal execution, this method might raise the following exceptions:
#
# - UnsupportedPackageTypeError if the package type is not supported
# - InvalidVersionError if the version is invalid
# - InvalidConstraintError if the constraint is invalid or contains invalid versions
#
def self.version_satisfies?(typ, raw_ver, raw_constraint)
# os package versions are handled very differently from application package versions
return os_pkg_version_satisfies?(typ, raw_ver, raw_constraint) if os_purl_type?(typ)
# build an interval that only contains the version
version = SemverDialects.parse_version(typ, raw_ver)
version_as_interval = Interval.from_version(version)
interval_set = IntervalSetParser.parse(typ, raw_constraint)
interval_set.overlaps_with?(version_as_interval)
end
def self.os_purl_type?(typ)
%w[deb rpm apk].include?(typ)
end
def self.os_pkg_version_satisfies?(typ, raw_ver, raw_constraint)
return unless %w[deb rpm apk].include?(typ)
# we only support the less than operator, because that's the only one currently output
# by the advisory exporter for operating system packages.
raise SemverDialects::InvalidConstraintError, raw_constraint unless raw_constraint[0] == '<'
v1 = SemverDialects.parse_version(typ, raw_ver)
v2 = SemverDialects.parse_version(typ, raw_constraint[1..])
v1 < v2
end
# Returns array of supported package types
def self.supported_package_types
%w[maven npm go pypi nuget gem packagist conan cargo apk deb rpm swift]
end
# Parse a version according to the syntax type.
def self.parse_version(typ, raw_ver)
# for efficiency most popular package types come first
case typ
when 'maven'
Maven::VersionParser.parse(raw_ver)
when 'npm'
# npm follows Semver 2.0.0.
Semver2::VersionParser.parse(cleanup(raw_ver))
when 'go'
# Go follows Semver 2.0.0.
#
# Go pseudo-versions are pre-releases as defined in Semver 2.0.0,
# and can be compared as such. However, a pseudo-version can't be compared
# to a pre-release or another pseudo-version of the same base version.
#
# quoting https://go.dev/ref/mod#pseudo-versions
#
# Each pseudo-version may be in one of three forms, depending on the base version.
# These forms ensure that a pseudo-version compares higher than its base version,
# but lower than the next tagged version.
#
# vX.0.0-yyyymmddhhmmss-abcdefabcdef is used when there is no known
# base version. As with all versions, the major version X must match the
# module’s major version suffix.
#
# vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef is used when the base version
# is a pre-release version like vX.Y.Z-pre.
#
# vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef is used when the base version
# is a release version like vX.Y.Z. For example, if the base version is
# v1.2.3, a pseudo-version might be v1.2.4-0.20191109021931-daa7c04131f5.
#
Semver2::VersionParser.parse(raw_ver)
when 'pypi'
# See https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers
# TODO: Implement a dedicated parser.
SemanticVersion.new(raw_ver)
when 'nuget'
# NuGet diverges from Semver 2.0.0.
#
# quoting https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#where-nugetversion-diverges-from-semantic-versioning
#
# NuGetVersion supports a 4th version segment, Revision, to be compatible
# with, or a superset of, System.Version. Therefore, excluding prerelease
# and metadata labels, a version string is Major.Minor.Patch.Revision. As
# per version normalization described above, if Revision is zero, it is
# omitted from the normalized version string.
#
# NuGetVersion only requires the major segment to be defined. All others
# are optional, and are equivalent to zero. This means that 1, 1.0,
# 1.0.0, and 1.0.0.0 are all accepted and equal.
#
# NuGetVersion uses case insensitive string comparisons for pre-release
# components. This means that 1.0.0-alpha and 1.0.0-Alpha are equal.
#
Semver2::VersionParser.parse(raw_ver.downcase)
when 'gem'
# Rubygem does not follow Semver. Its versioning scheme is not documented.
#
# quoting https://guides.rubygems.org/specification-reference/
#
# The version string can contain numbers and periods, such as 1.0.0. A
# gem is a ‘prerelease’ gem if the version has a letter in it, such as
# 1.0.0.pre.
Gem::Version.new(raw_ver)
when 'packagist'
# Packagist defines specific identifiers like alpha, beta, and stable,
# and the comparison rules for these are not compatible with Semver.
# See https://github.com/composer/semver/blob/1d09200268e7d1052ded8e5da9c73c96a63d18f5/src/VersionParser.php#L39
SemanticVersion.new(raw_ver)
when 'conan'
# Conan diverges from Semver 2.0.0.
#
# quoting https://docs.conan.io/2/tutorial/versioning/version_ranges.html#semantic-versioning
#
# Conan extends the semver specification to any number of digits, and
# also allows to include lowercase letters in it. This was done because
# during 1.X a lot of experience and feedback from users was gathered,
# and it became evident than in C++ the versioning scheme is often more
# complex, and users were demanding more flexibility, allowing versions
# like 1.2.3.a.8 if necessary.
#
# Conan versions non-digit identifiers follow the same rules as package
# names, they can only contain lowercase letters. This is to avoid
# 1.2.3-Beta to be a different version than 1.2.3-beta which can be
# problematic, even a security risk.
#
SemanticVersion.new(raw_ver)
when 'cargo'
# cargo follows Semver 2.0.0.
Semver2::VersionParser.parse(raw_ver)
when 'swift'
# swift follows Semver 2.0.0.
Semver2::VersionParser.parse(raw_ver)
when 'pub'
# pub follows Semver 2.0.0.
Semver2::VersionParser.parse(raw_ver)
when 'apk'
Apk::VersionParser.parse(raw_ver)
when 'deb'
DebVersion.new(raw_ver)
when 'rpm'
Rpm::VersionParser.parse(raw_ver)
else
raise UnsupportedPackageTypeError, typ
end
rescue ArgumentError
# Gem::Version.new raises an ArgumentError for invalid versions.
raise InvalidVersionError, raw_ver
end
# cleanup loose npm versions
def self.cleanup(raw_ver)
raw_ver.strip.gsub(/^[=v]/, '')
end
end
|