semver_dialects-3.4.0/0000755000004100000410000000000014650137150014721 5ustar www-datawww-datasemver_dialects-3.4.0/lib/0000755000004100000410000000000014650137150015467 5ustar www-datawww-datasemver_dialects-3.4.0/lib/semver_dialects.rb0000644000004100000410000001730414650137150021172 0ustar www-datawww-data# 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 # Captures all errors that could be possibly raised class Error < StandardError end class UnsupportedPackageTypeError < Error def initialize(pkgType) @pkgType = pkgType end def message "unsupported package type '#{@pkgType}'" end end class UnsupportedVersionError < Error def initialize(raw_version) @raw_version = raw_version end def message "unsupported version '#{@raw_version}'" end end class InvalidVersionError < Error def initialize(raw_version) @raw_version = raw_version end def message "invalid version '#{@raw_version}'" end end class InvalidConstraintError < Error def initialize(raw_constraint) @raw_constraint = raw_constraint end def message "invalid constraint '#{@raw_constraint}'" end end class IncompleteScanError < InvalidVersionError attr_reader :rest def initialize(rest) @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 # 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(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 '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 end semver_dialects-3.4.0/lib/utils.rb0000644000004100000410000000115514650137150017156 0ustar www-datawww-data# frozen_string_literal: true # monkey-patch String class class String def unwrap s = self s = s[1..s.length - 1] if s.start_with?('(') s = s[0..s.length - 2] if s.end_with?(')') s end def range_present? !empty? end def number? !!Integer(self, exception: false) end def initial self[0, 1] end def unquote delete_suffix('"').delete_prefix('"').delete_suffix('\'').delete_prefix('\'') end def csv_unquote unquote.unquote.unquote end def remove_trailing_number gsub(/([^\d]*)\d+$/, '\1') end def chars_only gsub(/[^0-9A-Za-z]/, '') end end semver_dialects-3.4.0/lib/semver_dialects/0000755000004100000410000000000014650137150020640 5ustar www-datawww-datasemver_dialects-3.4.0/lib/semver_dialects/boundary.rb0000644000004100000410000000277514650137150023023 0ustar www-datawww-data# frozen_string_literal: true # Boundary is a boundary used in an interval. # It can either be above all versions (infinity), # below all versions (negative infinity), or any version. module SemverDialects class Boundary include Comparable attr_accessor :semver def initialize(semver) @semver = semver end def to_s @semver.to_s end def <=>(other) return nil unless other.is_a?(Boundary) return -1 if other.instance_of?(AboveAll) return 1 if other.instance_of?(BelowAll) semver <=> other.semver end def is_initial_version? @semver.is_zero? end end # BelowAll represents a boundary below all possible versions. # When used as the lower boundary of an interval, any version # that is smaller than the upper boundary is in the interval. class BelowAll < Boundary def initialize; end def to_s '-inf' end def is_initial_version? false end def <=>(other) return 0 if other.instance_of?(BelowAll) -1 if other.is_a?(Boundary) end end # AboveAll represents a boundary above all possible versions. # When used as the upper boundary of an interval, any version # that is greater than the lower boundary is in the interval. class AboveAll < Boundary def initialize; end def to_s '+inf' end def is_initial_version? false end def <=>(other) return 0 if other.instance_of?(AboveAll) 1 if other.is_a?(Boundary) end end end semver_dialects-3.4.0/lib/semver_dialects/base_version.rb0000644000004100000410000000416114650137150023646 0ustar www-datawww-data# frozen_string_literal: true # Class that describes a version that can be compared to another version in a generic way. # # A version is made of +tokens+ and an optional +addition+. # Tokens and additions must be comparable using the spaceship operator. # An addition behaves like a version and must respond to +tokens+. # # Tokens are used to represent the dot separated list of segments # like 1.2.3 (semantic version) or alpha.1 (pre-release tag). # # Version 1.2.3-alpha.1-2024.03.25 is made of 3 +Version+ objects # whose tokens are represented as 1.2.3, alpha.1, 2024.03.25. # Version alpha.1 is the addition of version 1.2.3, # and version 2024.03.25 is the addition of version alpha.1. # # This class can support of the comparison logic of many syntaxes # by implementing specific token classes. # module SemverDialects class BaseVersion include Comparable attr_reader :tokens, :addition def initialize(tokens, addition: nil) @tokens = tokens @addition = addition end def to_s main = tokens.join('.') main += "-#{addition}" if addition main end def <=>(other) cmp = compare_tokens(tokens, other.tokens) return cmp unless cmp == 0 compare_additions(addition, other.addition) end # Returns true if the version tokens are equivalent to zero # and the addition is also equivalent to zero. def is_zero? return false if compare_tokens(tokens, [0]) != 0 return true if addition.nil? addition.is_zero? end private def compare_tokens(a, b) # rubocop:disable Naming/MethodParameterName max_idx = [a.size, b.size].max - 1 (0..max_idx).each do |idx| cmp = compare_token_pair(a[idx], b[idx]) return cmp unless cmp == 0 end 0 end def compare_token_pair(a, b) # rubocop:disable Naming/MethodParameterName (a || 0) <=> (b || 0) end def compare_additions(a, b) # rubocop:disable Naming/MethodParameterName return 0 if a.nil? && b.nil? (a || empty_addition).<=>(b || empty_addition) end def empty_addition self.class.new([]) end end end semver_dialects-3.4.0/lib/semver_dialects/maven.rb0000644000004100000410000001234714650137150022302 0ustar www-datawww-data# frozen_string_literal: true require 'strscan' module SemverDialects module Maven ALPHA = -5 BETA = -4 MILESTONE = -3 RC = -2 SNAPSHOT = -1 SP = 'sp' class Version < BaseVersion attr_accessor :addition # Return an array similar to the one Maven generates when parsing versions. # # $ java -jar ${MAVEN_HOME}/lib/maven-artifact-3.9.6.jar 1a1 # Display parameters as parsed by Maven (in canonical form and as a list of tokens) and comparison result: # 1. 1a1 -> 1-alpha-1; tokens: [1, [alpha, [1]]] # def to_a return tokens if addition.nil? tokens.clone.append(addition.to_a) end def to_s(as_addition = false) s = '' if tokens.any? s += '-' if as_addition s += tokens.map do |token| case token when String token when Integer case token when ALPHA 'alpha' when BETA 'beta' when MILESTONE 'milestone' when RC 'rc' when SNAPSHOT 'snapshot' else token.to_s end end end.join('.') end s += addition.to_s(true) if addition s end private # Compare tokens as specified in https://maven.apache.org/pom.html#version-order-specification. # Negative integers are alpha, beta, milestone, rc, and snapshot qualifiers. # Special qualifier "sp" is right after GA and before any lexical or numeric token. # Strings should be converted to lower case before being compared by this method. # 1-a0 == 1-alpha < 1-0 == 1 == 1final == 1 ga < 1sp < 1-a < 1-1 def compare_token_pair(a = 0, b = 0) a ||= 0 b ||= 0 if a.is_a?(Integer) && b.is_a?(String) return a <= 0 ? -1 : 1 end if a.is_a?(String) && b.is_a?(Integer) return b <= 0 ? 1 : -1 end return -1 if a == SP && b.is_a?(String) && b != SP return 1 if b == SP && a.is_a?(String) && a != SP # Identifiers have both the same type. # This returns nil if the identifiers can't be compared. a <=> b end def empty_addition Version.new([]) end end class VersionParser def self.parse(input) new(input).parse end attr_reader :input def initialize(input) @input = input end def parse @scanner = StringScanner.new(input.downcase) @version = Version.new([]) @result = @version parse_version(false) result end private attr_reader :scanner, :version, :result # Parse a version and all its additions recursively. # It automatically creates a new partition for numbers # if number_begins_partition is true. def parse_version(number_begins_partition) # skip leading v if any scanner.skip(/v/) until scanner.eos? if (s = scanner.scan(/\d+/)) if number_begins_partition parse_addition(s.to_i) else version.tokens << s.to_i end elsif (s = scanner.match?(/a\d+/)) # aN is equivalent to alpha-N scanner.skip('a') parse_addition(ALPHA) elsif (s = scanner.match?(/b\d+/)) # bN is equivalent to beta-N scanner.skip('b') parse_addition(BETA) elsif (s = scanner.match?(/m\d+/)) # mN is equivalent to milestone-N scanner.skip('m') parse_addition(MILESTONE) elsif (s = scanner.scan(/(alpha|beta|milestone|rc|cr|sp|ga|final|release|snapshot)[a-z]+/)) # process "alpha" and others as normal lexical tokens if they're followed by a letter parse_addition(s) elsif (s = scanner.scan('alpha')) # handle alphaN, alpha-X, alpha.X, or ending alpha parse_addition(ALPHA) elsif (s = scanner.scan('beta')) parse_addition(BETA) elsif (s = scanner.scan('milestone')) parse_addition(MILESTONE) elsif (s = scanner.scan(/(rc|cr)/)) parse_addition(RC) elsif (s = scanner.scan('snapshot')) parse_addition(SNAPSHOT) elsif (s = scanner.scan(/ga|final|release/)) parse_addition elsif (s = scanner.scan('sp')) parse_addition(SP) elsif (s = scanner.scan(/[a-z_]+/)) parse_addition(s) elsif (s = scanner.scan('.')) number_begins_partition = false elsif (s = scanner.scan('-')) number_begins_partition = true else raise IncompleteScanError, scanner.rest end end end # Create an addition for the current version, make it the current version, and parse it. # Numbers start a new partition. def parse_addition(token = nil) version.addition = Version.new([token].compact) @version = version.addition scanner.skip(/-+/) parse_version(true) end end end end semver_dialects-3.4.0/lib/semver_dialects/interval_parser.rb0000644000004100000410000000520414650137150024366 0ustar www-datawww-data# frozen_string_literal: true # IntervalParser parses a simple constraint expressed in the npm syntax # (or equivalent) and returns a Interval that has an upper boundary # or a lower boundary. # # The constraint is a string that can either be: # - an operator (>, <, >=, <=, =) followed by a version # - a version; the interval starts and ends with that version # - "=*"; the interval has no boundaries and includes any version # # Technically IntervalParser returns a Interval such as # start_cut is BelowAll or end_cut is AboveAll. # The type of the Interval matches the operator # that's been detected. # module SemverDialects module IntervalParser # A constraint is made of an operator followed by a version string. # Use the regular expression of the SemanticVersion because this is the most generic one. CONSTRAINT_REGEXP = Regexp.new("(?[><=]+) *(?#{SemanticVersion::VERSION_PATTERN})").freeze def self.parse(typ, versionstring) if versionstring == '=*' # special case = All Versions return Interval.new(IntervalType::LEFT_OPEN | IntervalType::RIGHT_OPEN, BelowAll.new, AboveAll.new) end version_items = versionstring.split(' ') interval = Interval.new(IntervalType::LEFT_OPEN | IntervalType::RIGHT_OPEN, BelowAll.new, AboveAll.new) version_items.each do |version_item| matches = version_item.match CONSTRAINT_REGEXP raise InvalidConstraintError, versionstring if matches.nil? version = SemverDialects.parse_version(typ, matches[:version]) boundary = Boundary.new(version) case matches[:op] when '>=' new_interval = Interval.new(IntervalType::LEFT_CLOSED | IntervalType::RIGHT_OPEN, boundary, AboveAll.new) interval = interval.intersect(new_interval) when '<=' new_interval = Interval.new(IntervalType::LEFT_OPEN | IntervalType::RIGHT_CLOSED, BelowAll.new, boundary) interval = interval.intersect(new_interval) when '<' new_interval = Interval.new(IntervalType::LEFT_OPEN | IntervalType::RIGHT_OPEN, BelowAll.new, boundary) interval = interval.intersect(new_interval) when '>' new_interval = Interval.new(IntervalType::LEFT_OPEN | IntervalType::RIGHT_OPEN, boundary, AboveAll.new) interval = interval.intersect(new_interval) when '=', '==' new_interval = Interval.new(IntervalType::LEFT_CLOSED | IntervalType::RIGHT_CLOSED, boundary, boundary) interval = interval.intersect(new_interval) end end interval rescue InvalidVersionError raise InvalidConstraintError, versionstring end end end semver_dialects-3.4.0/lib/semver_dialects/interval_set.rb0000644000004100000410000000423514650137150023670 0ustar www-datawww-data# frozen_string_literal: true # IntervalSet is a disjunction of version intervals. # It can express a range like "[1.0,2.0],[3.0,4.0]" (Maven syntax), # that is between 1.0 and 2.0 (included) OR between 3.0 and 4.0 (included). module SemverDialects class IntervalSet attr_reader :intervals def initialize @intervals = [] @interval_set = Set.new end def add(interval) @intervals << interval @interval_set.add(interval) end def <<(item) add(item) end def size @intervals.size end def to_s @intervals.map(&:to_s).join(',') end def to_description_s @intervals.map(&:to_description_s).join(', ').capitalize end def to_npm_s @intervals.map(&:to_npm_s).join('||') end def to_conan_s to_npm_s end def to_nuget_s to_maven_s end def to_maven_s @intervals.map(&:to_maven_s).join(',') end def to_gem_s @intervals.map(&:to_gem_s).join('||') end def to_pypi_s @intervals.map(&:to_pypi_s).join('||') end def to_go_s @intervals.map(&:to_go_s).join('||') end def to_packagist_s @intervals.map(&:to_packagist_s).join('||') end def to_cargo_s to_npm_s end def to_version_s(package_type) case package_type when 'npm' to_npm_s when 'nuget' to_nuget_s when 'maven' to_maven_s when 'gem' to_gem_s when 'pypi' to_pypi_s when 'packagist' to_packagist_s when 'go' to_go_s when 'conan' to_conan_s else '' end end def includes?(other) @interval_set.include?(other) end def overlaps_with?(other) @interval_set.each do |interval| return true unless interval.intersect(other).instance_of?(EmptyInterval) end false end def first @intervals.first end def empty? @intervals.empty? end def any? @intervals.any? end def universal? @intervals.each do |interval| return true if interval.universal? end false end end end semver_dialects-3.4.0/lib/semver_dialects/interval_set_parser.rb0000644000004100000410000001377414650137150025254 0ustar www-datawww-data# frozen_string_literal: true # IntervalSetParser parses a string that represents an interval set # in a syntax that's specific to a package type. module SemverDialects module IntervalSetParser # parse parses a string and returns an IntervalSet. # The string is expected to be in a syntax that's specific the given package type. def self.parse(typ, interval_set_string) IntervalSet.new.tap do |set| translate(typ, interval_set_string).each do |interval_str| set << IntervalParser.parse(typ, interval_str) end end end def self.translate(typ, interval_set_string) case typ when 'maven' translate_maven(interval_set_string) when 'npm' translate_npm(interval_set_string) when 'conan' translate_conan(interval_set_string) when 'nuget' translate_nuget(interval_set_string) when 'go' translate_go(interval_set_string) when 'gem' translate_gem(interval_set_string) when 'pypi' translate_pypi(interval_set_string) when 'packagist' translate_packagist(interval_set_string) when 'cargo' translate_cargo(interval_set_string) else raise UnsupportedPackageTypeError, typ end end def self.translate_npm(interval_set_string) interval_set_string.split('||').map do |item| add_missing_operator(single_space_after_operator(item.strip.gsub(/&&/, ' '))) end end def self.translate_conan(interval_set_string) translate_npm(interval_set_string) end def self.translate_go(interval_set_string) translate_gem(interval_set_string) end def self.translate_gem(interval_set_string) interval_set_string.split('||').map do |item| add_missing_operator(single_space_after_operator(item.strip.gsub(/\s+/, ' '))) end end def self.translate_packagist(interval_set_string) translate_pypi(interval_set_string) end def self.translate_pypi(interval_set_string) interval_set_string.split('||').map do |item| add_missing_operator(single_space_after_operator(comma_to_space(item))) end end def self.translate_nuget(interval_set_string) translate_maven(interval_set_string) end def self.translate_maven(interval_set_string) lexing_maven_interval_set_string(interval_set_string).map { |item| translate_mvn_version_item(item) } end def self.translate_cargo(interval_set_string) translate_npm(interval_set_string) end def self.add_missing_operator(interval_set_string) starts_with_operator?(interval_set_string) ? interval_set_string : "=#{interval_set_string}" end def self.single_space_after_operator(interval_set_string) interval_set_string.gsub(/([>=<]+) +/, '\1').gsub(/\s+/, ' ') end def self.starts_with_operator?(version_item) version_item.match(/^[=><]/) ? true : false end def self.comma_to_space(interval_set_string) interval_set_string.strip.gsub(/,/, ' ') end def self.lexing_maven_interval_set_string(interval_set_string) open = false substring = '' ret = [] interval_set_string.each_char do |c| case c when '(', '[' if open puts "malformed maven version string #{interval_set_string}" exit(-1) else unless substring.empty? ret << substring substring = '' end open = true substring += c end when ')', ']' if !open puts "malformed maven version string #{interval_set_string}" exit(-1) else open = false substring += c ret << substring substring = '' end when ',' substring += c if open when ' ' # nothing to do substring += '' else substring += c end end if open puts "malformed maven version string #{interval_set_string}" exit(-1) end ret << substring unless substring.empty? ret end def self.parenthesized?(version_item) version_item.match(/^[(\[]/) && version_item.match(/[\])]$/) end def self.translate_mvn_version_item(version_item) content = '' parens_pattern = '' if parenthesized?(version_item) content = version_item[1, version_item.size - 2] parens_pattern = version_item[0] + version_item[version_item.size - 1] # special case -- unversal version range return '=*' if content.strip == ',' else # according to the doc, if there is a plain version string in maven, it means 'starting from version x' # https://docs.oracle.com/middleware/1212/core/MAVEN/maven_version.htm#MAVEN8903 content = "#{version_item}," parens_pattern = '[)' end args = content.split(',') first_non_empty_arg = args.find(&:range_present?) if content.start_with?(',') # {,y} case parens_pattern when '[]' "<=#{first_non_empty_arg}" when '()' "<#{first_non_empty_arg}" when '[)' "<#{first_non_empty_arg}" else # par_pattern == "(]" "<=#{first_non_empty_arg}" end elsif content.end_with?(',') # {x,} case parens_pattern when '[]' ">=#{first_non_empty_arg}" when '()' ">#{first_non_empty_arg}" when '[)' ">=#{first_non_empty_arg}" else # par_pattern == "(]" ">#{first_non_empty_arg}" end elsif content[','].nil? # [x,x] "=#{content}" else case parens_pattern when '[]' ">=#{args[0]} <=#{args[1]}" when '()' ">#{args[0]} <#{args[1]}" when '[)' ">=#{args[0]} <#{args[1]}" else # par_pattern == "(]" ">#{args[0]} <=#{args[1]}" end end end end end semver_dialects-3.4.0/lib/semver_dialects/interval.rb0000644000004100000410000001672614650137150023025 0ustar www-datawww-data# frozen_string_literal: true module SemverDialects module IntervalType UNKNOWN = 0 LEFT_OPEN = 1 LEFT_CLOSED = 2 RIGHT_OPEN = 4 RIGHT_CLOSED = 8 end # Interval is an interval that starts with a lower boundary # and ends with an upper boundary. The interval includes the boundaries # or not depending on its type. class Interval # Returns an interval that only includes the given version. def self.from_version(version) boundary = Boundary.new(version) Interval.new(IntervalType::LEFT_CLOSED | IntervalType::RIGHT_CLOSED, boundary, boundary) end attr_accessor :type, :start_cut, :end_cut def initialize(type, start_cut, end_cut) @type = type @start_cut = start_cut @end_cut = end_cut end def intersect(other_interval) return EmptyInterval.new if empty? # this look odd -- we have to use it here though, because it may be that placeholders are present inside # the version for which > and < would yield true return EmptyInterval.new if !(@start_cut <= other_interval.end_cut) || !(other_interval.start_cut <= @end_cut) start_cut_new = max(@start_cut, other_interval.start_cut) end_cut_new = min(@end_cut, other_interval.end_cut) # compute the boundaries for the intersection type = compute_intersection_boundary(self, other_interval, start_cut_new, end_cut_new) interval = Interval.new(type, start_cut_new, end_cut_new) half_open = !(interval.bit_set?(IntervalType::RIGHT_CLOSED) && interval.bit_set?(IntervalType::LEFT_CLOSED)) interval.singleton? && half_open ? EmptyInterval.new : interval end def special(cut) cut.instance_of?(AboveAll) || cut.instance_of?(BelowAll) end def to_s s = '' s += bit_set?(IntervalType::LEFT_CLOSED) ? '[' : '' s += bit_set?(IntervalType::LEFT_OPEN) ? '(' : '' s += [@start_cut, @end_cut].join(',') s += bit_set?(IntervalType::RIGHT_CLOSED) ? ']' : '' s += bit_set?(IntervalType::RIGHT_OPEN) ? ')' : '' s end # this function returns a human-readable descriptions of the version strings def to_description_s s = '' if distinct? s = "version #{@start_cut}" elsif universal? s = 'all versions ' else s = 'all versions ' s += if start_cut.instance_of?(BelowAll) '' elsif bit_set?(IntervalType::LEFT_OPEN) "after #{@start_cut} " else bit_set?(IntervalType::LEFT_CLOSED) ? "starting from #{@start_cut} " : '' end s += if end_cut.instance_of?(AboveAll) '' elsif bit_set?(IntervalType::RIGHT_OPEN) "before #{@end_cut}" else bit_set?(IntervalType::RIGHT_CLOSED) ? "up to #{@end_cut}" : '' end end s.strip end def to_nuget_s to_maven_s end def to_maven_s s = '' # special case -- distinct version if distinct? s += "[#{@start_cut}]" else s += if start_cut.instance_of?(BelowAll) '(,' elsif bit_set?(IntervalType::LEFT_OPEN) "[#{@start_cut}," else bit_set?(IntervalType::LEFT_CLOSED) ? "[#{@start_cut}," : '' end s += if end_cut.instance_of?(AboveAll) ')' elsif bit_set?(IntervalType::RIGHT_OPEN) "#{@end_cut})" else bit_set?(IntervalType::RIGHT_CLOSED) ? "#{@end_cut}]" : '' end end s end def distinct? bit_set?(IntervalType::LEFT_CLOSED) && bit_set?(IntervalType::RIGHT_CLOSED) && @start_cut == @end_cut end def subsumes?(other) @start_cut <= other.start_cut && @end_cut >= other.end_cut end def universal? (bit_set?(IntervalType::LEFT_OPEN) && bit_set?(IntervalType::RIGHT_OPEN) && @start_cut.instance_of?(BelowAll) && @end_cut.instance_of?(AboveAll)) || @start_cut.is_initial_version? && @end_cut.instance_of?(AboveAll) end def to_gem_s get_canoncial_s end def to_ruby_s get_canoncial_s end def to_npm_s get_canoncial_s end def to_conan_s get_canoncial_s end def to_go_s get_canoncial_s end def to_pypi_s get_canoncial_s(',', '==') end def to_packagist_s get_canoncial_s(',') end def to_cargo_s get_canoncial_s end def empty? instance_of?(EmptyInterval) end def singleton? @start_cut == @end_cut && @start_cut.semver == @end_cut.semver end def ==(other) @start_cut == other.start_cut && @end_cut == other.end_cut && @type == other.type end def bit_set?(interval_type) @type & interval_type != 0 end protected def compute_intersection_boundary(interval_a, interval_b, start_cut_new, end_cut_new) compute_boundary(interval_a, interval_b, start_cut_new, end_cut_new, IntervalType::LEFT_OPEN, IntervalType::RIGHT_OPEN) end def compute_boundary(interval_a, interval_b, start_cut_new, end_cut_new, left_check, right_check) start_cut_a = interval_a.start_cut end_cut_a = interval_a.end_cut type_a = interval_a.type start_cut_b = interval_b.start_cut end_cut_b = interval_b.end_cut type_b = interval_b.type left_fill = left_check == IntervalType::LEFT_OPEN ? IntervalType::LEFT_CLOSED : IntervalType::LEFT_OPEN right_fill = right_check == IntervalType::RIGHT_OPEN ? IntervalType::RIGHT_CLOSED : IntervalType::RIGHT_OPEN # compute the boundaries for the union if start_cut_b == start_cut_a one_left_closed = left_type(type_a) == left_check || left_type(type_b) == left_check left_type = one_left_closed ? left_check : left_fill else left_type = start_cut_new == start_cut_a ? left_type(type_a) : left_type(type_b) end if end_cut_b == end_cut_a one_right_closed = right_type(type_a) == right_check || right_type(type_b) == right_check right_type = one_right_closed ? right_check : right_fill else right_type = end_cut_new == end_cut_a ? right_type(type_a) : right_type(type_b) end left_type | right_type end def get_canoncial_s(delimiter = ' ', eq = '=') if distinct? "#{eq}#{@start_cut}" else first = if start_cut.instance_of?(BelowAll) '' elsif bit_set?(IntervalType::LEFT_OPEN) ">#{@start_cut}" else bit_set?(IntervalType::LEFT_CLOSED) ? ">=#{@start_cut}" : '' end second = if end_cut.instance_of?(AboveAll) '' elsif bit_set?(IntervalType::RIGHT_OPEN) "<#{@end_cut}" else bit_set?(IntervalType::RIGHT_CLOSED) ? "<=#{@end_cut}" : '' end !first.empty? && !second.empty? ? "#{first}#{delimiter}#{second}" : first + second end end def max(cut_a, cut_b) cut_a > cut_b ? cut_a : cut_b end def min(cut_a, cut_b) cut_a < cut_b ? cut_a : cut_b end def right_type(type) (IntervalType::RIGHT_OPEN | IntervalType::RIGHT_CLOSED) & type end def left_type(type) (IntervalType::LEFT_OPEN | IntervalType::LEFT_CLOSED) & type end end class EmptyInterval < Interval def initialize; end def to_s 'empty' end end end semver_dialects-3.4.0/lib/semver_dialects/command.rb0000644000004100000410000000444314650137150022610 0ustar www-datawww-data# frozen_string_literal: true require 'forwardable' module SemverDialects # A CLI command class Command extend Forwardable def_delegators :command, :run # Execute this command # # @api public def execute(*) raise( NotImplementedError, "#{self.class}##{__method__} must be implemented" ) end # The external commands runner # # @see http://www.rubydoc.info/gems/tty-command # # @api public def command(**options) require 'tty-command' TTY::Command.new(options) end # The cursor movement # # @see http://www.rubydoc.info/gems/tty-cursor # # @api public def cursor require 'tty-cursor' TTY::Cursor end # Open a file or text in the user's preferred editor # # @see http://www.rubydoc.info/gems/tty-editor # # @api public def editor require 'tty-editor' TTY::Editor end # File manipulation utility methods # # @see http://www.rubydoc.info/gems/tty-file # # @api public def generator require 'tty-file' TTY::File end # Terminal output paging # # @see http://www.rubydoc.info/gems/tty-pager # # @api public def pager(**options) require 'tty-pager' TTY::Pager.new(options) end # Terminal platform and OS properties # # @see http://www.rubydoc.info/gems/tty-pager # # @api public def platform require 'tty-platform' TTY::Platform.new end # The interactive prompt # # @see http://www.rubydoc.info/gems/tty-prompt # # @api public def prompt(**options) require 'tty-prompt' TTY::Prompt.new(options) end # Get terminal screen properties # # @see http://www.rubydoc.info/gems/tty-screen # # @api public def screen require 'tty-screen' TTY::Screen end # The unix which utility # # @see http://www.rubydoc.info/gems/tty-which # # @api public def which(*args) require 'tty-which' TTY::Which.which(*args) end # Check if executable exists # # @see http://www.rubydoc.info/gems/tty-which # # @api public def exec_exist?(*args) require 'tty-which' TTY::Which.exist?(*args) end end end semver_dialects-3.4.0/lib/semver_dialects/cli.rb0000644000004100000410000000171514650137150021740 0ustar www-datawww-data# frozen_string_literal: true require 'thor' module SemverDialects # Handle the application command line parsing # and the dispatch to various command objects # # @api public class CLI < Thor # Error raised by this runner Error = Class.new(StandardError) desc 'version', 'semver_dialects version' def version require_relative 'version' puts "v#{SemverDialects::VERSION}" end map %w[--version -v] => :version desc 'check_version TYPE VERSION CONSTRAINT', 'Command description...' method_option :help, aliases: '-h', type: :boolean, desc: 'Display usage information' def check_version(type, version, constraint) if options[:help] invoke :help, ['check_version'] else require_relative 'commands/check_version' ecode = SemverDialects::Commands::CheckVersion.new(type, version, constraint, options).execute exit(ecode) end end end end semver_dialects-3.4.0/lib/semver_dialects/commands/0000755000004100000410000000000014650137150022441 5ustar www-datawww-datasemver_dialects-3.4.0/lib/semver_dialects/commands/check_version.rb0000644000004100000410000000164614650137150025617 0ustar www-datawww-data# frozen_string_literal: true require_relative '../command' require_relative '../../semver_dialects' module SemverDialects module Commands # The check version command implementation class CheckVersion < SemverDialects::Command def initialize(type, version, constraint, options) @type = type @version = version @constraint = constraint @options = options @avail_types = %w[gem npm ruby pypi php maven go] end def execute(_input: $stdin, output: $stdout) typ = @type.downcase raise SemverDialects::Error, 'wrong package type' unless @avail_types.include?(typ) if SemverDialects.version_satisfies?(typ, @version, @constraint) output.puts "#{@version} matches #{@constraint} for #{@type}" 0 else output.puts "#{@version} does not match #{@constraint}" 1 end end end end end semver_dialects-3.4.0/lib/semver_dialects/semantic_version.rb0000644000004100000410000001532314650137150024541 0ustar www-datawww-data# frozen_string_literal: true require_relative '../utils' module SemverDialects # SemanticVersion is a generic version class. # It parses and compares versions of any syntax. # It can't always be accurate because a single comparison logic # can't possibly handle all the supported syntaxes. # Since it's generic, it doesn't validate versions. class SemanticVersion include Comparable ANY_NUMBER = 'x' attr_reader :version_string, :prefix_segments, :suffix_segments, :segments # String to build a regexp that matches a version. # # A version might start with a leading "v", then it must have a digit, # then it might have any sequence made of alphanumerical characters, # underscores, dots, dashes, and wildcards. VERSION_PATTERN = 'v?[0-9][a-zA-Z0-9_.*+-]*' # Regexp for a string that only contains a single version string. VERSION_ONLY_REGEXP = Regexp.new("\\A#{VERSION_PATTERN}\\z").freeze def initialize(version_string) raise InvalidVersionError, version_string unless VERSION_ONLY_REGEXP.match version_string @version_string = version_string @prefix_segments = [] @suffix_segments = [] version, = version_string.delete_prefix('v').split('+') @segments = split_version_string!(version) end def split_version_string!(version_string) delim_pattern = /[.-]/ split_array = version_string.split(delim_pattern).map do |grp| grp.split(/(\d+)/).reject { |cell| cell.nil? || cell.empty? } end.flatten # go as far to the right as possible considering numbers and placeholders prefix_delimiter = 0 (0..split_array.size - 1).each do |i| break unless split_array[i].number? || split_array[i] == 'X' || split_array[i] == 'x' prefix_delimiter = i end # remove redundant trailing zeros prefix_delimiter.downto(0).each do |i| break unless split_array[i] == '0' split_array.delete_at(i) prefix_delimiter -= 1 end unless prefix_delimiter < 0 @prefix_segments = split_array[0..prefix_delimiter].map do |group_string| SemanticVersionSegment.new(group_string) end end if split_array.size - 1 >= prefix_delimiter + 1 @suffix_segments = split_array[prefix_delimiter + 1, split_array.size].map do |group_string| SemanticVersionSegment.new(group_string) end end @prefix_segments.clone.concat(@suffix_segments) end def _get_equalized_arrays_for(array_a, array_b) first_array = array_a.clone second_array = array_b.clone if first_array.size < second_array.size (second_array.size - first_array.size).times do first_array << SemanticVersionSegment.new('0') end elsif first_array.size > second_array.size (first_array.size - second_array.size).times do second_array << SemanticVersionSegment.new('0') end end [first_array, second_array] end def get_equalized_arrays_for(semver_a, semver_b) first_array_prefix = semver_a.prefix_segments.clone second_array_prefix = semver_b.prefix_segments.clone first_array_suffix = semver_a.suffix_segments.clone second_array_suffix = semver_b.suffix_segments.clone first_array_prefix, second_array_prefix = _get_equalized_arrays_for(first_array_prefix, second_array_prefix) first_array_suffix, second_array_suffix = _get_equalized_arrays_for(first_array_suffix, second_array_suffix) [first_array_prefix.concat(first_array_suffix), second_array_prefix.concat(second_array_suffix)] end def is_zero? @prefix_segments.empty? || @prefix_segments.all?(&:is_zero?) end def pre_release? @suffix_segments.any?(&:is_pre_release) end def post_release? @suffix_segments.any?(&:is_post_release) end def <=>(other) return nil unless other.is_a?(SemanticVersion) self_array, other_array = get_equalized_arrays_for(self, other) zipped_arrays = self_array.zip(other_array) zipped_arrays.each do |(a, b)| return 0 if a.wildcard? || b.wildcard? cmp = a <=> b return cmp if cmp != 0 end 0 end def to_normalized_s @segments.map(&:to_normalized_s).join(':') end def to_s @version_string end def minor @prefix_segments.size >= 1 ? @prefix_segments[1].to_s : '0' end def major @prefix_segments.size >= 2 ? @prefix_segments[0].to_s : '0' end def patch @prefix_segments.size >= 3 ? @prefix_segments[2].to_s : '0' end end class SemanticVersionSegment include Comparable attr_accessor :normalized_group_string, :original_group_string, :is_post_release, :is_pre_release @@group_suffixes = { # pre-releases 'PRE' => -16, 'PREVIEW' => -16, 'DEV' => -15, 'A' => -14, 'ALPHA' => -13, 'B' => -12, 'BETA' => -12, 'RC' => -11, 'M' => -10, 'RELEASE' => 0, 'FINAL' => 0, # PHP specific 'STABLE' => 0, # post-releases 'SP' => 1 } def initialize(group_string) @is_post_release = false @is_pre_release = false @version_string = group_string @original_group_string = group_string # use x as unique placeholder group_string_ucase = group_string.to_s.gsub(/\*/, 'x').upcase if @@group_suffixes.key?(group_string_ucase) value = @@group_suffixes[group_string_ucase] @is_post_release = value > 0 @is_pre_release = value < 0 @normalized_group_string = @@group_suffixes[group_string_ucase].to_s else @normalized_group_string = group_string_ucase end end def <=>(other) return nil unless other.is_a?(SemanticVersionSegment) self_semver = normalized_group_string other_semver = other.normalized_group_string both_are_numbers = self_semver.number? && other_semver.number? at_least_one_is_x = self_semver == 'X' || other_semver == 'X' a_numeric_b_non_numeric = self_semver.number? && !other_semver.number? b_numeric_a_non_numeric = other_semver.number? && !self_semver.number? if both_are_numbers self_semver.to_i <=> other_semver.to_i elsif at_least_one_is_x 0 elsif a_numeric_b_non_numeric -1 elsif b_numeric_a_non_numeric 1 else self_semver <=> other_semver end end def to_normalized_s @normalized_group_string end def to_s @version_string end def wildcard? normalized_group_string == 'X' end def is_number? normalized_group_string.number? end def is_zero? is_number? ? normalized_group_string.to_i == 0 : false end end end semver_dialects-3.4.0/lib/semver_dialects/rpm.rb0000644000004100000410000001221214650137150021761 0ustar www-datawww-data# frozen_string_literal: true require 'strscan' module SemverDialects module Rpm module TokenPairComparison # Token can be either alphabets, integers or tilde. # Caret is currently not supported. More details here https://gitlab.com/gitlab-org/gitlab/-/issues/428941#note_1882343489 # Precedence: numeric token > string token > no token > tilda (~) def compare_token_pair(a, b) return 1 if a != '~' && b == '~' return -1 if a == '~' && b != '~' return 1 if !a.nil? && b.nil? return -1 if a.nil? && !b.nil? return 1 if a.is_a?(Integer) && b.is_a?(String) return -1 if a.is_a?(String) && b.is_a?(Integer) # Remaining scenario are tokens of the same type ie Integer or String. Use <=> to compare a <=> b end end # This implementation references `go-rpm-version` https://github.com/knqyf263/go-rpm-version # Which is based on the official `rpmvercmp` https://github.com/rpm-software-management/rpm/blob/master/rpmio/rpmvercmp.c implementation # rpm versioning schema can be found here https://github.com/rpm-software-management/rpm/blob/master/docs/manual/dependencies.md#versioning # Details on how the caret and tilde symbols are handled can be found here https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/#_handling_non_sorting_versions_with_tilde_dot_and_caret class Version < BaseVersion include TokenPairComparison attr_reader :tokens, :addition, :epoch def initialize(tokens, epoch: nil, release_tag: nil) @tokens = tokens @addition = release_tag @epoch = epoch end def <=>(other) # Compare epoch first epoch_cmp = compare_epochs(epoch, other.epoch) return epoch_cmp unless epoch_cmp.zero? # Then compare version cmp = compare_tokens(tokens, other.tokens) return cmp unless cmp.zero? # And finally compare release tags compare_additions(addition, other.addition) end # Note that to_s does not accurately recreate the version string. # More details here https://gitlab.com/gitlab-org/gitlab/-/issues/428941#note_1882343489 def to_s main = if !epoch.nil? "#{epoch}:" + tokens.join('.') else tokens.join('.') end main += "-#{addition.tokens.join('.')}" unless addition.nil? # Remove . around ~ main.gsub(/\.~\./, '~') end private def compare_epochs(a, b) (a || 0) <=> (b || 0) end end class ReleaseTag < BaseVersion include TokenPairComparison def initialize(tokens) @tokens = tokens end end class VersionParser DASH = /-/ ALPHABET = /([a-zA-Z]+)/ TILDE = /~/ DIGIT = /([0-9]+)/ COLON = /:/ NON_ALPHANUMERIC_DASH_TILDE_AND_WHITESPACE = /[^a-zA-Z0-9~\s]+/ WHITE_SPACE = /\s/ def self.parse(input) new(input).parse end def initialize(input) @input = input @scanner = StringScanner.new(input) end # parse splits the input string into epoch, version and release tag Eg: :- # The version and release tag are split at the first `-` character if present # With the segment before the first `-` being version while the other being release tag # Subsequent `-` are disregarded def parse epoch = nil if (s = scanner.scan(/\d+:/)) epoch = s[..-2].to_i end # parse tokens until we reach the release tag, if any tokens = parse_tokens(false) # parse release tag release_tag = nil release_tag = ReleaseTag.new(parse_tokens(true)) if scanner.rest? raise IncompleteScanError, scanner.rest if scanner.rest? Version.new(tokens, epoch: epoch, release_tag: release_tag) end private attr_reader :scanner, :input def parse_tokens(stop_at_release_tag) tokens = [] until scanner.eos? case when (s = scanner.scan(DASH)) return tokens unless stop_at_release_tag # If release tag has been encountered, ignore subsequent dashes when (s = scanner.scan(ALPHABET)) tokens << s when (s = scanner.scan(TILDE)) tokens << s when (s = scanner.scan(DIGIT)) tokens << s.to_i when (s = scanner.scan(WHITE_SPACE)) # Whitespace is not permitted # https://github.com/rpm-software-management/rpm/blob/4d1b7401415003720ea9bef7bda248f7de4fa025/docs/manual/dependencies.md#versioning raise SemverDialects::InvalidVersionError, input when (s = scanner.scan(NON_ALPHANUMERIC_DASH_TILDE_AND_WHITESPACE)) # Non-ascii characters are considered equal # so they are ignored when parsing versions # https://github.com/rpm-software-management/rpm/blob/rpm-4.19.1.1-release/tests/rpmvercmp.at#L143 else raise SemverDialects::IncompleteScanError, scanner.rest end end tokens end end end end semver_dialects-3.4.0/lib/semver_dialects/apk.rb0000644000004100000410000001741714650137150021752 0ustar www-datawww-data# frozen_string_literal: true require 'strscan' module SemverDialects module Apk # This implementation references the version.c apk-tools implementation # https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/6052bfef57a81d82451b4cad86f78a2d01959767/src/version.c # apk version spec can be found here https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver class Version < BaseVersion PRE_RELEASE_ORDER = { 'alpha' => 0, 'beta' => 1, 'pre' => 2, 'rc' => 3 }.freeze POST_RELEASE_ORDER = { 'cvs' => 0, 'svn' => 1, 'git' => 2, 'hg' => 3, 'p' => 4 }.freeze attr_reader :tokens, :pre_release, :post_release, :revision def initialize(tokens, pre_release: [], post_release: [], revision: []) @tokens = tokens @pre_release = pre_release @post_release = post_release @revision = revision end def <=>(other) cmp = compare_tokens(tokens, other.tokens) return cmp unless cmp.zero? cmp = compare_pre_release(pre_release, other.pre_release) return cmp unless cmp.zero? cmp = compare_post_release(post_release, other.post_release) return cmp unless cmp.zero? compare_revisions(revision, other.revision) end # Note that to_s does not accurately recreate the version string # if alphabets are present in the version segment. # For instance 1.2.a or 1.2a would both be returned as 1.2.a with to_s # More details in https://gitlab.com/gitlab-org/ruby/gems/semver_dialects/-/merge_requests/97#note_1989192447 def to_s @to_s ||= begin main = tokens.join('.') main += "_#{pre_release.join('')}" unless pre_release.empty? main += "_#{post_release.join('')}" unless post_release.empty? main += "-#{revision.join('')}" unless revision.empty? main end end private # Token can be either integer or string # Precedence: numeric token > string token > no token def compare_token_pair(a, b) return 1 if !a.nil? && b.nil? return -1 if a.nil? && !b.nil? return 1 if a.is_a?(Integer) && b.is_a?(String) return -1 if a.is_a?(String) && b.is_a?(Integer) # Remaining scenario are tokens of the same type ie Integer or String. Use <=> to compare a <=> b end # Precedence: post-release > no release > pre-release # https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver def compare_pre_release(a, b) return 0 if a.empty? && b.empty? return -1 if !a.empty? && b.empty? return 1 if a.empty? && !b.empty? compare_suffix(a, b, PRE_RELEASE_ORDER) end # Precedence: post-release > no release > pre-release # https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver def compare_post_release(a, b) return 0 if a.empty? && b.empty? return 1 if !a.empty? && b.empty? return -1 if a.empty? && !b.empty? compare_suffix(a, b, POST_RELEASE_ORDER) end # Pre-release precedence: alpha < beta < pre < rc # Post-release precedence: cvs < svn < git < hg < p # Precedence for releases with number eg alpha1: # release without number < release with number def compare_suffix(a, b, order) a_suffix = order[a[0]] b_suffix = order[b[0]] return 1 if a_suffix > b_suffix return -1 if a_suffix < b_suffix a_value = a[1] b_value = b[1] return 1 if !a_value.nil? && b_value.nil? return -1 if a_value.nil? && !b_value.nil? (a_value || 0) <=> (b_value || 0) end def compare_revisions(a, b) return 0 if a.empty? && b.empty? return 1 if !a.empty? && b.empty? return -1 if a.empty? && !b.empty? a_value = a[1] b_value = b[1] return 1 if !a_value.nil? && b_value.nil? return -1 if a_value.nil? && !b_value.nil? (a_value || 0) <=> (b_value || 0) end end class VersionParser DASH = /-/ ALPHABETS = /([a-zA-Z]+)/ DIGITS = /([0-9]+)/ DIGIT = /[0-9]/ DOT = '.' UNDERSCORE = '_' PRE_RELEASE_SUFFIXES = %w[alpha beta pre rc].freeze POST_RELEASE_SUFFIXES = %w[cvs svn git hg p].freeze WHITE_SPACE = /\s/ def self.parse(input) new(input).parse end attr_reader :scanner, :input def initialize(input) @input = input @pre_release = [] @post_release = [] @revision = [] @scanner = StringScanner.new(input) end # Parse splits the raw version string into: # version, pre_release, post_release and revision # Format: _- # Note that version segment can contain alphabets # Release is always preceded with `_` # Revision is always preceded with `-` def parse tokens = parse_tokens Version.new(tokens, pre_release: @pre_release, post_release: @post_release, revision: @revision) end private def parse_tokens tokens = [] until scanner.eos? case when (s = scanner.scan(ALPHABETS)) tokens << s when (s = scanner.scan(DIGITS)) # TODO: add support to parse numbers with leading zero https://gitlab.com/gitlab-org/gitlab/-/issues/471509 raise SemverDialects::UnsupportedVersionError, input if s.start_with?('0') && s.length > 1 tokens << s.to_i when (s = scanner.scan(UNDERSCORE)) parse_release # Continue parsing if there's remaining tokens since revision which comes after release is optional return tokens if scanner.eos? when (s = scanner.scan(DASH)) parse_revision return tokens when (s = scanner.scan(WHITE_SPACE)) # Raise error if there's whitespace raise SemverDialects::InvalidVersionError, input when (s = scanner.scan(DOT)) # Skip parsing dot else raise SemverDialects::IncompleteScanError, scanner.rest end end tokens end # PRE_RELEASE_SUFFIXES: alpha, beta, pre, rc # POST_RELEASE_SUFFIXES: cvs, svn, git, hg, p # No other suffixes are allowed # Release can be either `` or `` with the number being optional def parse_release # TODO: Add support to parse version with multiple releases raise SemverDialects::UnsupportedVersionError, input if !@pre_release.empty? || !@post_release.empty? suffix_type = nil until scanner.eos? case when (s = scanner.scan(ALPHABETS)) if PRE_RELEASE_SUFFIXES.include?(s) suffix_type = :pre @pre_release << s elsif POST_RELEASE_SUFFIXES.include?(s) suffix_type = :post @post_release << s else raise SemverDialects::InvalidVersionError, input end return unless scanner.peek(1) =~ DIGIT when (s = scanner.scan(DIGITS)) if suffix_type == :pre @pre_release << s.to_i return elsif suffix_type == :post @post_release << s.to_i return end end end end # Revision can be either `r` or `r` with the number being optional def parse_revision until scanner.eos? case when (s = scanner.scan(ALPHABETS)) raise SemverDialects::InvalidVersionError, input unless s == 'r' @revision << s return unless scanner.peek(1) =~ DIGIT when (s = scanner.scan(DIGITS)) @revision << s.to_i return end end end end end end semver_dialects-3.4.0/lib/semver_dialects/version.rb0000644000004100000410000000011514650137150022647 0ustar www-datawww-data# frozen_string_literal: true module SemverDialects VERSION = '3.4.0' end semver_dialects-3.4.0/lib/semver_dialects/semver2.rb0000644000004100000410000001065614650137150022560 0ustar www-datawww-data# frozen_string_literal: true require 'strscan' module SemverDialects module Semver2 # Represents a token that matches any major, minor, or patch number. ANY_NUMBER = 'x' class Version < BaseVersion def initialize(tokens, prerelease_tag: nil) @tokens = tokens @addition = prerelease_tag end def <=>(other) if (idx = tokens.index(ANY_NUMBER)) a = tokens[0..(idx - 1)] b = other.tokens[0..(idx - 1)] return compare_tokens(a, b) end if (idx = other.tokens.index(ANY_NUMBER)) a = tokens[0..(idx - 1)] b = other.tokens[0..(idx - 1)] return compare_tokens(a, b) end super end private # Compares pre-release tags as specified in https://semver.org/#spec-item-9. def compare_additions(a, b) # rubocop:disable Naming/MethodParameterName # Pre-release versions have a lower precedence than the associated normal version. return -1 if !a.nil? && b.nil? # only self is a pre-release return 1 if a.nil? && !b.nil? # only other is a pre-release a <=> b end end class PrereleaseTag < BaseVersion def initialize(tokens) @tokens = tokens end # Returns true if the prerelease tag is empty. # In Semver 2 1.2.3-0 is NOT equivalent to 1.2.3. def is_zero? tokens.empty? end private # Compares pre-release identifiers as specified in https://semver.org/#spec-item-11. def compare_token_pair(a, b) # rubocop:disable Naming/MethodParameterName case a when Integer case b when String # Numeric identifiers always have lower precedence than non-numeric identifiers. return -1 when nil # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. return 1 end when String case b when Integer # Numeric identifiers always have lower precedence than non-numeric identifiers. return 1 when nil # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. return 1 end when nil case b when Integer # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. return -1 when String # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. return -1 end end # Identifiers have both the same type (numeric or non-numeric). # This returns nil if the identifiers can't be compared. a <=> b end end class VersionParser def self.parse(input) new(input).parse end attr_reader :input def initialize(input) @input = input @scanner = StringScanner.new(input) end def parse tokens = [] prerelease_tag = nil # skip ignore leading v if any scanner.skip('v') until scanner.eos? if (s = scanner.scan(/\d+/)) tokens << s.to_i elsif (s = scanner.scan(/\.x\z/i)) tokens << ANY_NUMBER elsif (s = scanner.scan('.')) # continue elsif (s = scanner.scan('-')) prerelease_tag = parse_prerelease_tag elsif (s = scanner.scan(/\+.*/)) # continue else raise IncompleteScanError, scanner.rest end end Version.new(tokens, prerelease_tag: prerelease_tag) end private attr_reader :scanner def parse_prerelease_tag tokens = [] at_build_tag = false until scanner.eos? || at_build_tag if (s = scanner.scan(/\d+(?![a-zA-Z-])/)) tokens << s.to_i elsif (s = scanner.scan(/[0-9a-zA-Z-]+/)) tokens << s elsif (s = scanner.scan('.')) # continue elsif (s = scanner.scan('+')) scanner.unscan at_build_tag = true else raise IncompleteScanError, scanner.rest end end PrereleaseTag.new(tokens) end end end end semver_dialects-3.4.0/semver_dialects.gemspec0000644000004100000410000000713514650137150021445 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: semver_dialects 3.4.0 ruby lib Gem::Specification.new do |s| s.name = "semver_dialects".freeze s.version = "3.4.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "allowed_push_host" => "https://rubygems.org", "changelog_uri" => "https://gitlab.com/gitlab-org/ruby/gems/semver_dialects/-/blob/master/CHANGELOG.md", "homepage_uri" => "https://rubygems.org/gems/semver_dialects", "source_code_uri" => "https://gitlab.com/gitlab-org/ruby/gems/semver_dialects" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Julian Thome".freeze, "Isaac Dawson".freeze, "James Jonhson".freeze] s.bindir = "exe".freeze s.date = "2024-07-10" s.description = "This gem helps to parse, process and compare semantic versions for Maven, NPM, PHP, RubyGems and python packages.".freeze s.email = ["jthome@gitlab.com".freeze, "idawson@gitlab.com".freeze, "jjohnson@gitlab.com".freeze] s.files = ["lib/semver_dialects.rb".freeze, "lib/semver_dialects/apk.rb".freeze, "lib/semver_dialects/base_version.rb".freeze, "lib/semver_dialects/boundary.rb".freeze, "lib/semver_dialects/cli.rb".freeze, "lib/semver_dialects/command.rb".freeze, "lib/semver_dialects/commands/check_version.rb".freeze, "lib/semver_dialects/interval.rb".freeze, "lib/semver_dialects/interval_parser.rb".freeze, "lib/semver_dialects/interval_set.rb".freeze, "lib/semver_dialects/interval_set_parser.rb".freeze, "lib/semver_dialects/maven.rb".freeze, "lib/semver_dialects/rpm.rb".freeze, "lib/semver_dialects/semantic_version.rb".freeze, "lib/semver_dialects/semver2.rb".freeze, "lib/semver_dialects/version.rb".freeze, "lib/utils.rb".freeze] s.homepage = "https://rubygems.org/gems/semver_dialects".freeze s.licenses = ["MIT".freeze] s.rubygems_version = "3.3.15".freeze s.summary = "This gem provides utility function to process semantic versions expressed in different dialects.".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_development_dependency(%q.freeze, ["~> 2.13"]) s.add_development_dependency(%q.freeze, ["~> 2.4"]) s.add_runtime_dependency(%q.freeze, ["~> 1.0.1"]) s.add_runtime_dependency(%q.freeze, ["~> 0.8.0"]) s.add_development_dependency(%q.freeze, ["~> 12.3.3"]) s.add_development_dependency(%q.freeze, ["~> 3.0"]) s.add_development_dependency(%q.freeze, ["~> 1.0"]) s.add_development_dependency(%q.freeze, ["~> 1.63"]) s.add_development_dependency(%q.freeze, ["~> 0.17.1"]) s.add_runtime_dependency(%q.freeze, ["~> 1.3"]) s.add_runtime_dependency(%q.freeze, ["~> 0.10.1"]) else s.add_dependency(%q.freeze, ["~> 2.13"]) s.add_dependency(%q.freeze, ["~> 2.4"]) s.add_dependency(%q.freeze, ["~> 1.0.1"]) s.add_dependency(%q.freeze, ["~> 0.8.0"]) s.add_dependency(%q.freeze, ["~> 12.3.3"]) s.add_dependency(%q.freeze, ["~> 3.0"]) s.add_dependency(%q.freeze, ["~> 1.0"]) s.add_dependency(%q.freeze, ["~> 1.63"]) s.add_dependency(%q.freeze, ["~> 0.17.1"]) s.add_dependency(%q.freeze, ["~> 1.3"]) s.add_dependency(%q.freeze, ["~> 0.10.1"]) end end