Skip to content

Commit d652391

Browse files
committed
Fix: support Alpine-specific version formats that Gentoo regex rejects (fixes #59)
Root cause: AlpineLinuxVersion.is_valid() delegates directly to gentoo.is_valid(), which only accepts the Gentoo version grammar. Alpine extends that grammar with extra patterns: a letter+digit portable-release suffix (e.g. "1.9.5p2"), the _git/_cvs/_svn snapshot suffixes, dash as a numeric component separator ("1.11-20-r0"), and minor malformations found in real package databases ("0.12.5.-r0", "0.8.21.r2"). Fix: override AlpineLinuxVersion.normalize() with _normalize_alpine_to_gentoo(), which rewrites these Alpine-only patterns into their Gentoo equivalents before validation and comparison: "1.9.5p2-r0" -> "1.9.5_p2-r0" "5.15.3_git20200401-r0" -> "5.15.3_alpha20200401-r0" "1.11-20-r0" -> "1.11.20-r0" "0.12.5.-r0" -> "0.12.5-r0" "0.8.21.r2" -> "0.8.21-r2"
1 parent f94ff10 commit d652391

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

src/univers/versions.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#
55
# Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download.
66

7+
import re
8+
79
import attr
810
import semantic_version
911
from packaging import version as packaging_version
@@ -58,6 +60,64 @@ def is_valid_alpine_version(s):
5860
return str(i) == left
5961

6062

63+
def _normalize_alpine_to_gentoo(string):
64+
"""
65+
Normalize an Alpine Linux version string to a Gentoo-compatible format
66+
so that Gentoo's vercmp can compare it correctly.
67+
68+
Alpine extends Gentoo-style versioning with additional patterns:
69+
- A dot immediately before the revision marker ("0.12.5.-r0" -> "0.12.5-r0")
70+
- Revision without a leading dash ("0.8.21.r2" -> "0.8.21-r2")
71+
- Alpine-only suffix words: git, cvs, svn (mapped to alpha), rev/jdk (mapped to p)
72+
- A single letter + digit directly after the dotted version ("1.9.5p2-r0" -> "1.9.5_p2-r0")
73+
- A dash as a numeric version separator ("1.11-20-r0" -> "1.11.20-r0")
74+
75+
For example:
76+
>>> _normalize_alpine_to_gentoo("1.9.5p2-r0")
77+
'1.9.5_p2-r0'
78+
>>> _normalize_alpine_to_gentoo("3.3.3p1-r3")
79+
'3.3.3_p1-r3'
80+
>>> _normalize_alpine_to_gentoo("5.15.3_git20200401-r0")
81+
'5.15.3_alpha20200401-r0'
82+
>>> _normalize_alpine_to_gentoo("1.11-20-r0")
83+
'1.11.20-r0'
84+
>>> _normalize_alpine_to_gentoo("57-1-r2")
85+
'57.1-r2'
86+
>>> _normalize_alpine_to_gentoo("0.12.5.-r0")
87+
'0.12.5-r0'
88+
>>> _normalize_alpine_to_gentoo("0.8.21.r2")
89+
'0.8.21-r2'
90+
>>> _normalize_alpine_to_gentoo("1.2.3-r1")
91+
'1.2.3-r1'
92+
>>> _normalize_alpine_to_gentoo("1.2.3_alpha1-r1")
93+
'1.2.3_alpha1-r1'
94+
"""
95+
# Handle ".rN" (no dash before revision): "0.8.21.r2" -> "0.8.21-r2"
96+
string = re.sub(r"\.r(\d+)$", r"-r\1", string)
97+
98+
# Handle trailing dot before revision: "0.12.5.-r0" -> "0.12.5-r0"
99+
string = re.sub(r"\.-r(\d+)$", r"-r\1", string)
100+
101+
# Map Alpine-only suffix words to Gentoo equivalents.
102+
# Must be done before the letter+digit substitution below to avoid mangling them.
103+
# _git, _cvs, _svn are snapshot/SCM builds -> treat as pre-release (alpha)
104+
string = re.sub(r"_(git|cvs|svn)(\d*)", r"_alpha\2", string)
105+
# _rev, _jdk -> treat as patch release (p)
106+
string = re.sub(r"_(rev|jdk)(\d*)", r"_p\2", string)
107+
108+
# Handle single letter+digit suffix: "1.9.5p2-r0" -> "1.9.5_p2-r0"
109+
# Matches a letter immediately preceded by a digit, followed by one or more
110+
# digits, just before the revision marker "-rN" or end of string.
111+
string = re.sub(r"(?<=\d)([a-zA-Z])(\d+)(?=-r\d|$)", r"_\1\2", string)
112+
113+
# Handle dash as a numeric version-component separator: "1.11-20-r0" -> "1.11.20-r0"
114+
# Replaces "-N" only when N is a pure digit run not followed by the revision
115+
# marker "rN" (i.e., skip "-r0", "-r1", …).
116+
string = re.sub(r"-(?!r\d)(\d+)", r".\1", string)
117+
118+
return string
119+
120+
61121
@attr.s(frozen=True, order=True, eq=True, hash=True)
62122
class Version:
63123
"""
@@ -431,6 +491,11 @@ def __gt__(self, other):
431491

432492

433493
class AlpineLinuxVersion(GentooVersion):
494+
@classmethod
495+
def normalize(cls, string):
496+
string = super().normalize(string)
497+
return _normalize_alpine_to_gentoo(string)
498+
434499
@classmethod
435500
def is_valid(cls, string):
436501
return is_valid_alpine_version(string) and gentoo.is_valid(string)

tests/test_alpine.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,56 @@ def test_alpine_vers_cmp2(test_case):
3939
avc.assert_result()
4040

4141

42+
@pytest.mark.parametrize(
43+
("version", "expected_value"),
44+
[
45+
# dot immediately before revision marker (issue #59)
46+
("0.12.5.-r0", "0.12.5-r0"),
47+
# dot instead of dash before revision number (issue #59)
48+
("0.8.21.r2", "0.8.21-r2"),
49+
# dash as numeric version-component separator (issue #59)
50+
("1.11-20-r0", "1.11.20-r0"),
51+
("57-1-r2", "57.1-r2"),
52+
# single letter + digit suffix, e.g. OpenSSH portable releases (issue #59)
53+
("1.9.5p2-r0", "1.9.5_p2-r0"),
54+
("3.3.3p1-r3", "3.3.3_p1-r3"),
55+
("6.6.2p1-r0", "6.6.2_p1-r0"),
56+
("6.6.4p1-r1", "6.6.4_p1-r1"),
57+
("6.7.1p1-r1", "6.7.1_p1-r1"),
58+
# _git snapshot suffix mapped to _alpha for comparison (issue #59)
59+
("5.15.3_git20200401-r0", "5.15.3_alpha20200401-r0"),
60+
("5.15.3_git20210510-r0", "5.15.3_alpha20210510-r0"),
61+
],
62+
)
63+
def test_alpine_extended_version_formats(version, expected_value):
64+
"""Versions with Alpine-specific patterns must parse and normalise correctly."""
65+
v = AlpineLinuxVersion(version)
66+
assert v.value == expected_value
67+
68+
69+
@pytest.mark.parametrize(
70+
("smaller", "larger"),
71+
[
72+
# portable-release ordering: p1 < p2
73+
("1.9.5p1-r0", "1.9.5p2-r0"),
74+
# git snapshot is a pre-release, comes before the stable release
75+
("5.15.3_git20200401-r0", "5.15.3-r0"),
76+
# earlier git snapshot < later git snapshot
77+
("5.15.3_git20200401-r0", "5.15.3_git20210510-r0"),
78+
# dash-separated version component ordering
79+
("1.11-20-r0", "1.11-21-r0"),
80+
("57-1-r2", "57-2-r0"),
81+
# dot-r vs normal version
82+
("0.8.21.r2", "0.8.22-r0"),
83+
],
84+
)
85+
def test_alpine_extended_version_comparison(smaller, larger):
86+
"""Extended Alpine version formats must compare in the correct order."""
87+
v1 = AlpineLinuxVersion(smaller)
88+
v2 = AlpineLinuxVersion(larger)
89+
assert v1 < v2
90+
91+
4292
@pytest.mark.parametrize(
4393
"test_case",
4494
[

0 commit comments

Comments
 (0)