diff --git a/src/univers/version_range.py b/src/univers/version_range.py index 705b9d0b..94a25f04 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -868,6 +868,37 @@ def from_native(cls, string): return cls(constraints=constraints) +class OpensslVersionRange(VersionRange): + """ + Openssl version range. + openssl doesn't use <,>,<= or >= + For more see 'https://www.openssl.org/news/vulnerabilities.xml' + + For example:: + >>> from univers.versions import OpensslVersion + >>> constraints = ( + ... VersionConstraint(version=OpensslVersion("1.0.1af")), + ... VersionConstraint(comparator="=", version=OpensslVersion("3.0.1")), + ... VersionConstraint(comparator="=", version=OpensslVersion("1.1.1nf")), + ... ) + >>> range = OpensslVersionRange(constraints=constraints) + >>> assert str(range) == 'vers:openssl/1.0.1af|1.1.1nf|3.0.1' + """ + + scheme = "openssl" + version_class = versions.OpensslVersion + + @classmethod + def from_native(cls, string): + cleaned = remove_spaces(string).lower() + constraints = [] + for version in cleaned.split(","): + version_obj = cls.version_class(version) + constraint = VersionConstraint(comparator="=", version=version_obj) + constraints.append(constraint) + return cls(constraints=constraints) + + def is_even(s): """ Return True if the string "s" is an even number and False if this is an odd @@ -902,4 +933,5 @@ def is_even(s): "ebuild": EbuildVersionRange, "archlinux": ArchLinuxVersionRange, "nginx": NginxVersionRange, + "openssl": OpensslVersionRange, } diff --git a/src/univers/versions.py b/src/univers/versions.py index 6ce4262e..2d48a0c8 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -343,3 +343,179 @@ def __gt__(self, other): if not isinstance(other, self.__class__): return NotImplemented return gentoo.vercmp(self.value, other.value) > 0 + + +@attr.s(frozen=True, order=False, eq=False, hash=True) +class LegacyOpensslVersion(Version): + """ + Represent an Legacy Openssl Version. + + For example:: + >>> LegacyOpensslVersion("1.0.1f") + LegacyOpensslVersion(string='1.0.1f') + >>> LegacyOpensslVersion("1.0.2ac") + LegacyOpensslVersion(string='1.0.2ac') + >>> LegacyOpensslVersion("1.0.2a") + LegacyOpensslVersion(string='1.0.2a') + >>> LegacyOpensslVersion("3.0.2") + Traceback (most recent call last): + ... + univers.versions.InvalidVersion: '3.0.2' is not a valid + """ + + @classmethod + def is_valid(cls, string): + return bool(cls.parse(string)) + + @classmethod + def parse(cls, string): + """ + Return a four-tuple of (major, minor, build, patch) version segments where + major, minor, build are integers and patch is a string possibly empty. + Return False if this is not a valid LegacyOpensslVersion. + + For example:: + >>> LegacyOpensslVersion.parse("1.0.1f") + (1, 0, 1, 'f') + >>> LegacyOpensslVersion.parse("1.0.2ac") + (1, 0, 2, 'ac') + >>> LegacyOpensslVersion.parse("2.0.2az") + False + """ + + # All known legacy base OpenSSL versions + all_legacy_base = ( + "0.9.1", + "0.9.2", + "0.9.3", + "0.9.4", + "0.9.5", + "0.9.6", + "0.9.7", + "0.9.8", + "1.0.0", + "1.0.1", + "1.0.2", + "1.1.0", + "1.1.1", + ) + if not string.startswith(all_legacy_base): + return False + + segments = string.split(".") + if not len(segments) == 3: + return False + major, minor, build = segments + major = int(major) + minor = int(minor) + if build.isdigit(): + build = int(build) + patch = "" + else: + patch = build[1:] + build = int(build[0]) + if patch[0].isdigit(): + return False + return major, minor, build, patch + + @classmethod + def build_value(cls, string): + return cls.parse(string) + + def __str__(self): + return f"{self.value[0]}.{self.value[1]}.{self.value[2]}{self.value[3]}" + + +@attr.s(frozen=True, order=False, eq=False, hash=True) +class OpensslVersion(Version): + """ + Internally tracks two types of openssl versions + - LegacyOpensslVersion for versions before version 3.0.0 such as 1.0.1g + - Semver for versions from 3.0.0 and up + For example:: + >>> old = OpensslVersion("1.1.0f") + >>> new = OpensslVersion("3.0.1") + >>> assert old == OpensslVersion(string="1.1.0f") + >>> assert new == OpensslVersion(string="3.0.1") + >>> assert old.value == LegacyOpensslVersion(string="1.1.0f") + >>> assert new.value == SemverVersion(string="3.0.1") + >>> OpensslVersion("1.2.4fg") + Traceback (most recent call last): + ... + univers.versions.InvalidVersion: '1.2.4fg' is not a valid + """ + + @classmethod + def is_valid(cls, string): + return cls.is_valid_new(string) or cls.is_valid_legacy(string) + + @classmethod + def build_value(cls, string): + """ + Return a wrapped version "value" object depending on + whether version is legacy or semver. + """ + if cls.is_valid_legacy(string): + return LegacyOpensslVersion(string) + if cls.is_valid_new(string): + return SemverVersion(string) + + @classmethod + def is_valid_new(cls, string): + """ + Check the validity of new Openssl Version. + + For example:: + >>> OpensslVersion.is_valid_new("1.0.1f") + False + >>> OpensslVersion.is_valid_new("3.0.0") + True + >>> OpensslVersion.is_valid_new("3.0.2") + True + """ + if SemverVersion.is_valid(string): + sem = semantic_version.Version.coerce(string) + return sem.major >= 3 + + @classmethod + def is_valid_legacy(cls, string): + return LegacyOpensslVersion.is_valid(string) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + if not isinstance(other.value, self.value.__class__): + return NotImplemented + return self.value.__eq__(other.value) + + def __lt__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + if isinstance(other.value, self.value.__class__): + return self.value.__lt__(other.value) + # By construction legacy version is always behind Semver + return isinstance(self.value, LegacyOpensslVersion) + + def __gt__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + if isinstance(other.value, self.value.__class__): + return self.value.__gt__(other.value) + # By construction semver version is always ahead of legacy + return isinstance(self.value, SemverVersion) + + def __le__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + if isinstance(other.value, self.value.__class__): + return self.value.__le__(other.value) + # version value are of diff type, then legacy one is always behind semver + return isinstance(self.value, LegacyOpensslVersion) + + def __ge__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + if isinstance(other.value, self.value.__class__): + return self.value.__ge__(other.value) + # version value are of diff type, then semver one is always ahead of legacy + return isinstance(self.value, SemverVersion) diff --git a/tests/test_openssl_vercmp.py b/tests/test_openssl_vercmp.py new file mode 100644 index 00000000..65191350 --- /dev/null +++ b/tests/test_openssl_vercmp.py @@ -0,0 +1,319 @@ +# +# Copyright (c) nexB Inc. and others. +# SPDX-License-Identifier: Apache-2.0 +# +# Visit https://aboutcode.org and https://github.com/nexB/univers for support and download. + +import operator + +import pytest +from univers.versions import LegacyOpensslVersion +from univers.versions import OpensslVersion +from univers.versions import SemverVersion + + +def compare(version1, comparator, version2): + """ + Compare version1 and version2 with comparator + and return True if comparison is valid, False otherwise + """ + operator_comparator = { + "<": operator.lt, + ">": operator.gt, + "<=": operator.le, + ">=": operator.ge, + "==": operator.eq, + "!=": operator.ne, + } + compare = operator_comparator[comparator] + return compare(version1, version2) + + +@pytest.mark.parametrize( + "version1, comparator, version2, expected", + [ + ("1.0.0e", ">", "1.0.0r", False), + ("1.0.0e", "<", "1.0.0r", True), + ("1.0.0e", ">=", "1.0.0r", False), + ("1.0.0e", "<=", "1.0.0r", True), + ("1.0.0e", "==", "1.0.0r", False), + ("1.0.0e", "!=", "1.0.0r", True), + ("1.0.1f", ">", "1.0.1m", False), + ("1.0.1f", "<", "1.0.1m", True), + ("1.0.1f", ">=", "1.0.1m", False), + ("1.0.1f", "<=", "1.0.1m", True), + ("1.0.1f", "==", "1.0.1m", False), + ("1.0.1f", "!=", "1.0.1m", True), + ("1.0.1g", ">", "1.0.2a", False), + ("1.0.1g", "<", "1.0.2a", True), + ("1.0.1g", ">=", "1.0.2a", False), + ("1.0.1g", "<=", "1.0.2a", True), + ("1.0.1g", "==", "1.0.2a", False), + ("1.0.1g", "!=", "1.0.2a", True), + ("1.0.1h", ">", "0.9.8", True), + ("1.0.1h", "<", "0.9.8", False), + ("1.0.1h", ">=", "0.9.8", True), + ("1.0.1h", "<=", "0.9.8", False), + ("1.0.1h", "==", "0.9.8", False), + ("1.0.1h", "!=", "0.9.8", True), + ("1.0.1h", ">", "0.9.8a", True), + ("1.0.1h", "<", "0.9.8a", False), + ("1.0.1h", ">=", "0.9.8a", True), + ("1.0.1h", "<=", "0.9.8a", False), + ("1.0.1h", "==", "0.9.8a", False), + ("1.0.1h", "!=", "0.9.8a", True), + ("0.9.8lt", ">", "0.9.8ztl", False), + ("0.9.8lt", "<", "0.9.8ztl", True), + ("0.9.8lt", ">=", "0.9.8ztl", False), + ("0.9.8lt", "<=", "0.9.8ztl", True), + ("0.9.8lt", "==", "0.9.8ztl", False), + ("0.9.8lt", "!=", "0.9.8ztl", True), + ("1.1.1ag", ">", "1.1.1ag", False), + ("1.1.1ag", "<", "1.1.1ag", False), + ("1.1.1ag", ">=", "1.1.1ag", True), + ("1.1.1ag", "<=", "1.1.1ag", True), + ("1.1.1ag", "==", "1.1.1ag", True), + ("1.1.1ag", "!=", "1.1.1ag", False), + ], +) +def test_old_old_comparison(version1, comparator, version2, expected): + old1 = OpensslVersion(version1) + old2 = OpensslVersion(version2) + assert compare(old1, comparator, old2) is expected + + +@pytest.mark.parametrize( + "version1, comparator, version2, expected", + [ + ("3.0.0", ">", "1.1.1z", True), + ("3.0.0", "<", "1.1.1z", False), + ("3.0.0", ">=", "1.1.1z", True), + ("3.0.0", "<=", "1.1.1z", False), + ("3.0.0", "==", "1.1.1z", False), + ("3.0.0", "!=", "1.1.1z", True), + ("3.1.0", ">", "1.0.1m", True), + ("3.1.0", "<", "1.0.1m", False), + ("3.1.0", ">=", "1.0.1m", True), + ("3.1.0", "<=", "1.0.1m", False), + ("3.1.0", "==", "1.0.1m", False), + ("3.1.0", "!=", "1.0.1m", True), + ("3.0.3", ">", "1.0.2a", True), + ("3.0.3", "<", "1.0.2a", False), + ("3.0.3", ">=", "1.0.2a", True), + ("3.0.3", "<=", "1.0.2a", False), + ("3.0.3", "==", "1.0.2a", False), + ("3.0.3", "!=", "1.0.2a", True), + ("3.0.1", ">", "0.9.8ztl", True), + ("3.0.1", "<", "0.9.8ztl", False), + ("3.0.1", ">=", "0.9.8ztl", True), + ("3.0.1", "<=", "0.9.8ztl", False), + ("3.0.1", "==", "0.9.8ztl", False), + ("3.0.1", "!=", "0.9.8ztl", True), + ("4.1.11", ">", "1.1.1ag", True), + ("4.1.11", "<", "1.1.1ag", False), + ("4.1.11", ">=", "1.1.1ag", True), + ("4.1.11", "<=", "1.1.1ag", False), + ("4.1.11", "==", "1.1.1ag", False), + ("4.1.11", "!=", "1.1.1ag", True), + ], +) +def test_new_old_comparison(version1, comparator, version2, expected): + new = OpensslVersion(version1) + old = OpensslVersion(version2) + assert compare(new, comparator, old) is expected + + +@pytest.mark.parametrize( + "version1, comparator, version2, expected", + [ + ("1.1.1z", ">", "3.0.0", False), + ("1.1.1z", "<", "3.0.0", True), + ("1.1.1z", ">=", "3.0.0", False), + ("1.1.1z", "<=", "3.0.0", True), + ("1.1.1z", "==", "3.0.0", False), + ("1.1.1z", "!=", "3.0.0", True), + ("1.0.1m", ">", "3.1.0", False), + ("1.0.1m", "<", "3.1.0", True), + ("1.0.1m", ">=", "3.1.0", False), + ("1.0.1m", "<=", "3.1.0", True), + ("1.0.1m", "==", "3.1.0", False), + ("1.0.1m", "!=", "3.1.0", True), + ("1.0.2a", ">", "3.0.3", False), + ("1.0.2a", "<", "3.0.3", True), + ("1.0.2a", ">=", "3.0.3", False), + ("1.0.2a", "<=", "3.0.3", True), + ("1.0.2a", "==", "3.0.3", False), + ("1.0.2a", "!=", "3.0.3", True), + ("0.9.8ztl", ">", "3.0.1", False), + ("0.9.8ztl", "<", "3.0.1", True), + ("0.9.8ztl", ">=", "3.0.1", False), + ("0.9.8ztl", "<=", "3.0.1", True), + ("0.9.8ztl", "==", "3.0.1", False), + ("0.9.8ztl", "!=", "3.0.1", True), + ("1.1.1ag", ">", "4.1.11", False), + ("1.1.1ag", "<", "4.1.11", True), + ("1.1.1ag", ">=", "4.1.11", False), + ("1.1.1ag", "<=", "4.1.11", True), + ("1.1.1ag", "==", "4.1.11", False), + ("1.1.1ag", "!=", "4.1.11", True), + ], +) +def test_old_new_comparison(version1, comparator, version2, expected): + old = OpensslVersion(version1) + new = OpensslVersion(version2) + assert compare(old, comparator, new) is expected + + +@pytest.mark.parametrize( + "version1, comparator, version2, expected", + [ + ("3.0.0", ">", "3.0.0", False), + ("3.0.0", "<", "3.0.0", False), + ("3.0.0", ">=", "3.0.0", True), + ("3.0.0", "<=", "3.0.0", True), + ("3.0.0", "==", "3.0.0", True), + ("3.0.0", "!=", "3.0.0", False), + ("3.1.0", ">", "3.0.2", True), + ("3.1.0", "<", "3.0.2", False), + ("3.1.0", ">=", "3.0.2", True), + ("3.1.0", "<=", "3.0.2", False), + ("3.1.0", "==", "3.0.2", False), + ("3.1.0", "!=", "3.0.2", True), + ("3.0.3", ">", "4.0.2", False), + ("3.0.3", "<", "4.0.2", True), + ("3.0.3", ">=", "4.0.2", False), + ("3.0.3", "<=", "4.0.2", True), + ("3.0.3", "==", "4.0.2", False), + ("3.0.3", "!=", "4.0.2", True), + ("3.0.1", ">", "3.0.2", False), + ("3.0.1", "<", "3.0.2", True), + ("3.0.1", ">=", "3.0.2", False), + ("3.0.1", "<=", "3.0.2", True), + ("3.0.1", "==", "3.0.2", False), + ("3.0.1", "!=", "3.0.2", True), + ("4.1.11", ">", "4.2.0", False), + ("4.1.11", "<", "4.2.0", True), + ("4.1.11", ">=", "4.2.0", False), + ("4.1.11", "<=", "4.2.0", True), + ("4.1.11", "==", "4.2.0", False), + ("4.1.11", "!=", "4.2.0", True), + ], +) +def test_new_new_comparison(version1, comparator, version2, expected): + new1 = OpensslVersion(version1) + new2 = OpensslVersion(version2) + assert compare(new1, comparator, new2) is expected + + +@pytest.mark.parametrize( + "version1, comparator, version2, expected", + [ + ("3.2.3", ">", "1.0.0r", "TypeError"), + ("3.2.3", "<", "1.0.0r", "TypeError"), + ("3.2.3", ">=", "1.0.0r", "TypeError"), + ("3.2.3", "<=", "1.0.0r", "TypeError"), + ("3.2.3", "==", "1.0.0r", False), + ("3.2.3", "!=", "1.0.0r", True), + ("1.0.1f", ">", "1.0.1m", "TypeError"), + ("1.0.1f", "<", "1.0.1m", "TypeError"), + ("1.0.1f", ">=", "1.0.1m", "TypeError"), + ("1.0.1f", "<=", "1.0.1m", "TypeError"), + ("1.0.1f", "==", "1.0.1m", False), + ("1.0.1f", "!=", "1.0.1m", True), + ("1.0.1g", ">", "1.0.2a", "TypeError"), + ("1.0.1g", "<", "1.0.2a", "TypeError"), + ("1.0.1g", ">=", "1.0.2a", "TypeError"), + ("1.0.1g", "<=", "1.0.2a", "TypeError"), + ("1.0.1g", "==", "1.0.2a", False), + ("1.0.1g", "!=", "1.0.2a", True), + ("3.0.0", ">", "0.9.8", "TypeError"), + ("3.0.0", "<", "0.9.8", "TypeError"), + ("3.0.0", ">=", "0.9.8", "TypeError"), + ("3.0.0", "<=", "0.9.8", "TypeError"), + ("3.0.0", "==", "0.9.8", False), + ("3.0.0", "!=", "0.9.8", True), + ("3.0.0", ">", "0.9.8a", "TypeError"), + ("3.0.0", "<", "0.9.8a", "TypeError"), + ("3.0.0", ">=", "0.9.8a", "TypeError"), + ("3.0.0", "<=", "0.9.8a", "TypeError"), + ("3.0.0", "==", "0.9.8a", False), + ("3.0.0", "!=", "0.9.8a", True), + ("0.9.8lt", ">", "0.9.8ztl", "TypeError"), + ("0.9.8lt", "<", "0.9.8ztl", "TypeError"), + ("0.9.8lt", ">=", "0.9.8ztl", "TypeError"), + ("0.9.8lt", "<=", "0.9.8ztl", "TypeError"), + ("0.9.8lt", "==", "0.9.8ztl", False), + ("0.9.8lt", "!=", "0.9.8ztl", True), + ("0.9.1", ">", "0.9.1", "TypeError"), + ("0.9.1", "<", "0.9.1", "TypeError"), + ("0.9.1", ">=", "0.9.1", "TypeError"), + ("0.9.1", "<=", "0.9.1", "TypeError"), + ("0.9.1", "==", "0.9.1", False), + ("0.9.1", "!=", "0.9.1", True), + ], +) +def test_openssl_legacy_comparison(version1, comparator, version2, expected): + openssl = OpensslVersion(version1) + legacy = LegacyOpensslVersion(version2) + try: + result = compare(openssl, comparator, legacy) + except TypeError: + result = "TypeError" + assert result is expected + + +@pytest.mark.parametrize( + "version1, comparator, version2, expected", + [ + ("3.2.3", ">", "1.0.0r", "TypeError"), + ("3.2.3", "<", "1.0.0r", "TypeError"), + ("3.2.3", ">=", "1.0.0r", "TypeError"), + ("3.2.3", "<=", "1.0.0r", "TypeError"), + ("3.2.3", "==", "1.0.0r", False), + ("3.2.3", "!=", "1.0.0r", True), + ("1.0.1f", ">", "1.0.1m", "TypeError"), + ("1.0.1f", "<", "1.0.1m", "TypeError"), + ("1.0.1f", ">=", "1.0.1m", "TypeError"), + ("1.0.1f", "<=", "1.0.1m", "TypeError"), + ("1.0.1f", "==", "1.0.1m", False), + ("1.0.1f", "!=", "1.0.1m", True), + ("1.0.1g", ">", "1.0.2a", "TypeError"), + ("1.0.1g", "<", "1.0.2a", "TypeError"), + ("1.0.1g", ">=", "1.0.2a", "TypeError"), + ("1.0.1g", "<=", "1.0.2a", "TypeError"), + ("1.0.1g", "==", "1.0.2a", False), + ("1.0.1g", "!=", "1.0.2a", True), + ("3.0.0", ">", "0.9.8", "TypeError"), + ("3.0.0", "<", "0.9.8", "TypeError"), + ("3.0.0", ">=", "0.9.8", "TypeError"), + ("3.0.0", "<=", "0.9.8", "TypeError"), + ("3.0.0", "==", "0.9.8", False), + ("3.0.0", "!=", "0.9.8", True), + ("3.0.0", ">", "0.9.8a", "TypeError"), + ("3.0.0", "<", "0.9.8a", "TypeError"), + ("3.0.0", ">=", "0.9.8a", "TypeError"), + ("3.0.0", "<=", "0.9.8a", "TypeError"), + ("3.0.0", "==", "0.9.8a", False), + ("3.0.0", "!=", "0.9.8a", True), + ("0.9.8lt", ">", "0.9.8ztl", "TypeError"), + ("0.9.8lt", "<", "0.9.8ztl", "TypeError"), + ("0.9.8lt", ">=", "0.9.8ztl", "TypeError"), + ("0.9.8lt", "<=", "0.9.8ztl", "TypeError"), + ("0.9.8lt", "==", "0.9.8ztl", False), + ("0.9.8lt", "!=", "0.9.8ztl", True), + ("0.9.1", ">", "0.9.1", "TypeError"), + ("0.9.1", "<", "0.9.1", "TypeError"), + ("0.9.1", ">=", "0.9.1", "TypeError"), + ("0.9.1", "<=", "0.9.1", "TypeError"), + ("0.9.1", "==", "0.9.1", False), + ("0.9.1", "!=", "0.9.1", True), + ], +) +def test_openssl_semver_comparison(version1, comparator, version2, expected): + openssl = OpensslVersion(version1) + semver = SemverVersion(version2) + try: + result = compare(openssl, comparator, semver) + except TypeError: + result = "TypeError" + assert result is expected diff --git a/tests/test_version_range.py b/tests/test_version_range.py index 4b2754c0..52b97427 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -14,9 +14,11 @@ from univers.version_range import VersionRange from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import NpmVersionRange +from univers.version_range import OpensslVersionRange from univers.versions import PypiVersion from univers.versions import RubygemsVersion from univers.versions import SemverVersion +from univers.versions import OpensslVersion class TestVersionRange(TestCase): @@ -233,10 +235,40 @@ def test_NpmVersionRange_from_native_with_approximately_equal_to_operator(self): version_range = NpmVersionRange.from_native(npm_range) assert version_range == expected + def test_OpensslVersionRange_from_native_single_legacy(self): + openssl_range = "0.9.8j" + expected = OpensslVersionRange( + constraints=( + VersionConstraint(comparator="=", version=OpensslVersion(string="0.9.8j")), + ) + ) + version_range = OpensslVersionRange.from_native(openssl_range) + assert version_range == expected + + def test_OpensslVersionRange_from_native_single_new_semver(self): + openssl_range = "3.0.1" + expected = OpensslVersionRange( + constraints=(VersionConstraint(comparator="=", version=OpensslVersion(string="3.0.1")),) + ) + version_range = OpensslVersionRange.from_native(openssl_range) + assert version_range == expected + + def test_OpensslVersionRange_from_native_mixed(self): + openssl_range = "3.0.0, 1.0.1b" + expected = OpensslVersionRange( + constraints=( + VersionConstraint(comparator="=", version=OpensslVersion(string="1.0.1b")), + VersionConstraint(comparator="=", version=OpensslVersion(string="3.0.0")), + ) + ) + version_range = OpensslVersionRange.from_native(openssl_range) + assert version_range == expected + VERSION_RANGE_TESTS_BY_SCHEME = { "nginx": ["0.8.40+", "0.7.52-0.8.39", "0.9.10", "1.5.0+, 1.4.1+"], "npm": ["^1.2.9", "~3.8.2", "5.0.0 - 7.2.3", "2.1 || 2.6", "1.1.2 1.2.2", "<=2.1 >=1.1"], + "openssl": ["1.1.1ak", "1.1.0", "3.0.2", "3.0.1, 0.9.7a", "1.0.2ck, 3.1.2"], }