diff --git a/src/univers/gentoo.py b/src/univers/gentoo.py index 543f8548..c91763fc 100644 --- a/src/univers/gentoo.py +++ b/src/univers/gentoo.py @@ -18,6 +18,8 @@ revision_regexp = re.compile(r".*(-r\d+)") +slot_regexp = re.compile(r":([A-Za-z0-9_][A-Za-z0-9+_.-]*(?:/[A-Za-z0-9_][A-Za-z0-9+_.-]*)?=?|\*)$") + suffix_value = {"pre": -2, "p": 1, "alpha": -4, "beta": -3, "rc": -1} """ @@ -25,22 +27,102 @@ """ +def parse_parts(version_string): + """ + Return a tuple of (base_version, revision_int, slot_str, has_wildcard_bool). + """ + if not version_string: + return "", 0, None, False + + version = remove_spaces(version_string) + + slot = None + if ":" in version: + match = slot_regexp.search(version) + if match: + slot = match.group(1) + version = version[: match.start()] + else: + return "", 0, None, False + + wildcard = False + if version.endswith("*"): + wildcard = True + version = version[:-1] + + revision = 0 + match = revision_regexp.search(version) + if match: + revision = int(match.group(1)[2:]) + version = version[: match.span(1)[0]] + + return version, revision, slot, wildcard + + +def strip_slot(version_string): + """ + Return the version string with any slot qualifier stripped. + """ + if not version_string: + return "" + version = remove_spaces(version_string) + if ":" in version: + match = slot_regexp.search(version) + if match: + return version[: match.start()] + return "" + return version + + +def match_wildcard(ver1, ver2): + """ + Return True if one of the versions is a Gentoo wildcard version and + the other matches it according to Gentoo PMS wildcard matching semantics. + If both are wildcards, return True only if their wildcard specifications match. + """ + _, _, _, wild1 = parse_parts(ver1) + _, _, _, wild2 = parse_parts(ver2) + + if not (wild1 or wild2): + return False + + if wild1 and wild2: + s1 = strip_slot(ver1).rstrip("*") + s2 = strip_slot(ver2).rstrip("*") + return s1 == s2 + + wild_str = ver1 if wild1 else ver2 + concrete_str = ver2 if wild1 else ver1 + + prefix = strip_slot(wild_str).rstrip("*") + target = strip_slot(concrete_str) + + if target == prefix: + return True + + if target.startswith(prefix): + next_char = target[len(prefix)] + if next_char in (".", "_", "-") or next_char.isalpha(): + return True + + return False + + def is_valid(string): - version, _ = parse_version_and_revision(remove_spaces(string)) - return _is_gentoo_version(version) + if not string: + return False + version, _, _, _ = parse_parts(string) + if not version: + return False + return bool(_is_gentoo_version(version)) def parse_version_and_revision(version_string): """ Return a tuple of (version string, revision int) given a ``version_string``. + Also strips any slot or wildcard suffix so that vercmp operates cleanly. """ - revision = 0 - version = version_string - match = revision_regexp.search(version_string) - if match: - revision = int(match.group(1)[2:]) - version = version_string[: match.span(1)[0]] - + version, revision, _, _ = parse_parts(version_string) return version, revision diff --git a/src/univers/versions.py b/src/univers/versions.py index 5d6101ac..22c4a0fc 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -414,9 +414,23 @@ class GentooVersion(Version): def is_valid(cls, string): return gentoo.is_valid(string) + @property + def slot(self): + """Return the slot string if present, else None.""" + _, _, slot, _ = gentoo.parse_parts(self.value) + return slot + + @property + def is_wildcard(self): + """Return True if this version ends with a wildcard '*'.""" + _, _, _, wildcard = gentoo.parse_parts(self.value) + return wildcard + def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented + if self.is_wildcard or other.is_wildcard: + return gentoo.match_wildcard(self.value, other.value) return gentoo.vercmp(self.value, other.value) == 0 def __lt__(self, other): diff --git a/tests/test_gentoo.py b/tests/test_gentoo.py index d2eddd5e..1cbad3c0 100644 --- a/tests/test_gentoo.py +++ b/tests/test_gentoo.py @@ -31,3 +31,82 @@ def comparison(self): def test_gentoo_vers_cmp(test_case): avc = GentooVersionComp.from_data(data=test_case) avc.assert_result() + + +def test_gentoo_slots_and_wildcards(): + # Issue #182 reported versions + assert GentooVersion.is_valid("1.3*") + assert GentooVersion.is_valid("3.24.48:3") + assert GentooVersion.is_valid("6.9.3:6") + assert GentooVersion.is_valid("7.3*") + assert GentooVersion.is_valid("7.4*") + + v_slot = GentooVersion("3.24.48:3") + assert v_slot.slot == "3" + assert not v_slot.is_wildcard + assert v_slot == GentooVersion("3.24.48:3") + assert v_slot == GentooVersion("3.24.48") + assert v_slot == GentooVersion("3.24.48:10") + assert v_slot < GentooVersion("3.24.49:3") + assert v_slot > GentooVersion("3.24.47:3") + + v_slot6 = GentooVersion("6.9.3:6") + assert v_slot6.slot == "6" + + # Sub-slot and slot operators + assert GentooVersion.is_valid("2.4.52:0/2.4") + assert GentooVersion("2.4.52:0/2.4").slot == "0/2.4" + assert GentooVersion.is_valid("3.0:3=") + assert GentooVersion("3.0:3=").slot == "3=" + + # Wildcards + v_wild = GentooVersion("1.3*") + assert v_wild.is_wildcard + assert v_wild.slot is None + assert v_wild == GentooVersion("1.3*") + assert v_wild == GentooVersion("1.3") + assert v_wild == GentooVersion("1.3.1") + assert v_wild == GentooVersion("1.3a") + assert v_wild == GentooVersion("1.3_alpha4") + assert v_wild == GentooVersion("1.3-r1") + assert v_wild != GentooVersion("1.30") + assert v_wild != GentooVersion("1.4") + assert v_wild < GentooVersion("1.4") + assert v_wild > GentooVersion("1.2") + assert GentooVersion("7.3*") < GentooVersion("7.4*") + + # Constraint satisfaction and round-tripping through EbuildVersionRange + from univers.version_constraint import VersionConstraint + from univers.version_range import EbuildVersionRange + from univers.version_range import VersionRange + + c_gt = VersionConstraint(comparator=">", version=GentooVersion("3.24.47:3")) + assert GentooVersion("3.24.48:3") in c_gt + assert GentooVersion("3.24.48") in c_gt + assert GentooVersion("3.24.49") in c_gt + assert GentooVersion("3.24.47") not in c_gt + + c_eq = VersionConstraint(comparator="=", version=GentooVersion("1.3*")) + assert GentooVersion("1.3*") in c_eq + assert GentooVersion("1.3") in c_eq + assert GentooVersion("1.3.1") in c_eq + assert GentooVersion("1.3a") in c_eq + assert GentooVersion("1.3_alpha4") in c_eq + assert GentooVersion("1.3-r1") in c_eq + assert GentooVersion("1.30") not in c_eq + assert GentooVersion("1.4") not in c_eq + + vr = EbuildVersionRange(constraints=[c_eq, c_gt]) + vr_str = str(vr) + assert vr_str == "vers:ebuild/1.3*|>3.24.47:3" + assert VersionRange.from_string(vr_str) == vr + assert GentooVersion("1.3.1") in vr + assert GentooVersion("1.30") not in vr + + # Invalid formats + assert not GentooVersion.is_valid("*") + assert not GentooVersion.is_valid("1.3**") + assert not GentooVersion.is_valid(":3") + assert not GentooVersion.is_valid("1:") + assert not GentooVersion.is_valid("3.24.48:") + assert not GentooVersion.is_valid("3.24.48:invalid!slot")