Skip to content

Commit 428fc10

Browse files
authored
fix: ensure '0' version is always sorted first (#4049)
Per the osv-schema: > `introduced` allows a version of the value `"0"` to represent a version that sorts before any other version. Previously, we were just coercing "0" to be the general 0 value for the ecosystem (e.g. in semver `0.0.0`). This caused problems when trying to match e.g. `0.0.0-pre`, which sorts before. Wrapped all the sort_keys in a `VersionKey` type and added a new sentinel `0` value type that always sorts before other versions. This is technically incorrect for ecosystems where "0" is a valid version and that supports pre-releases (e.g. in Maven `alpha` < `0-alpha` < `0`) but these edge cases are not worth handling. On an semi unrelated note, also bumped the max version that some invalid versions get mapped to from `999999` to `9999999999` so that they're larger than CalVer versions e.g. `20250607` I'm going to have to do a re-computation/reput of the AffectedVersions entities to make sure they're all still sorted.
1 parent 3b755a4 commit 428fc10

28 files changed

+200
-64
lines changed

osv/ecosystems/alpine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
class APK(OrderedEcosystem):
3232
"""Alpine Package Keeper ecosystem helper."""
3333

34-
def sort_key(self, version):
34+
def _sort_key(self, version):
3535
if not AlpineLinuxVersion.is_valid(version):
3636
# If version is not valid, it is most likely an invalid input
3737
# version then sort it to the last/largest element
38-
return AlpineLinuxVersion('999999')
38+
return AlpineLinuxVersion('9999999999')
3939
return AlpineLinuxVersion(version)
4040

4141

osv/ecosystems/alpine_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def test_apk(self):
4545
self.assertGreater(
4646
ecosystem.sort_key('1.13.2-r0'), ecosystem.sort_key('1.13.2_alpha'))
4747

48+
# Check the 0 sentinel value.
49+
self.assertLess(
50+
ecosystem.sort_key('0'), ecosystem.sort_key('0.0.0_alpha-r0'))
51+
4852
# Check invalid version handle
4953
self.assertGreater(
5054
ecosystem.sort_key('1-0-0'), ecosystem.sort_key('1.13.2-r0'))

osv/ecosystems/bioconductor_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,19 @@ def test_next_version(self):
3232
self.assertEqual('1.20.0', ecosystem.next_version('a4', '1.18.0'))
3333
with self.assertRaises(ecosystems.EnumerateError):
3434
ecosystem.next_version('doesnotexist123456', '1')
35+
36+
def test_sort_key(self):
37+
"""Test sort_key."""
38+
ecosystem = ecosystems.get('Bioconductor')
39+
self.assertGreater(
40+
ecosystem.sort_key('1.20.0'), ecosystem.sort_key('1.18.0'))
41+
self.assertLess(ecosystem.sort_key('1.18.0'), ecosystem.sort_key('1.18.1'))
42+
43+
# Check the 0 sentinel value.
44+
self.assertLess(ecosystem.sort_key('0'), ecosystem.sort_key('0.0.0'))
45+
46+
# Check >= / <= methods
47+
self.assertGreaterEqual(
48+
ecosystem.sort_key('1.20.0'), ecosystem.sort_key('1.18.0'))
49+
self.assertLessEqual(
50+
ecosystem.sort_key('1.18.0'), ecosystem.sort_key('1.20.0'))

osv/ecosystems/cran.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class CRAN(EnumerableEcosystem):
2929
_API_PACKAGE_URL_POSIT_CRAN = 'https://packagemanager.posit.co/__api__/' + \
3030
'repos/2/packages/{package}'
3131

32-
def sort_key(self, version):
32+
def _sort_key(self, version):
3333
"""Sort key."""
3434
# Some documentation on CRAN versioning and the R numeric_version method:
3535
# https://cran.r-project.org/doc/manuals/R-exts.html#The-DESCRIPTION-file

osv/ecosystems/cran_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,18 @@ def test_next_version(self):
4343

4444
# Test atypical versioned package
4545
self.assertEqual('0.99-8.47', ecosystem.next_version('aqp', '0.99-8.1'))
46+
47+
def test_sort_key(self):
48+
"""Test sort_key."""
49+
ecosystem = ecosystems.get('CRAN')
50+
self.assertGreater(ecosystem.sort_key('1.0-0'), ecosystem.sort_key('0.1-0'))
51+
self.assertLess(ecosystem.sort_key('0.1-0'), ecosystem.sort_key('0.1-1'))
52+
53+
# Check the 0 sentinel value.
54+
self.assertLess(ecosystem.sort_key('0'), ecosystem.sort_key('0.0-0'))
55+
56+
# Check >= / <= methods
57+
self.assertGreaterEqual(
58+
ecosystem.sort_key('1.10-0'), ecosystem.sort_key('1.2-0'))
59+
self.assertLessEqual(
60+
ecosystem.sort_key('1.2-0'), ecosystem.sort_key('1.10-0'))

osv/ecosystems/debian.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
class DPKG(OrderedEcosystem):
3030
"""Debian package (dpkg) ecosystem"""
3131

32-
def sort_key(self, version):
32+
def _sort_key(self, version):
3333
if not DebianVersion.is_valid(version):
3434
# If debian version is not valid, it is most likely an invalid fixed
3535
# version then sort it to the last/largest element
36-
return DebianVersion(999999, '999999')
36+
return DebianVersion(9999999999, '9999999999')
3737
return DebianVersion.from_string(version)
3838

3939

osv/ecosystems/debian_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ def test_dpkg(self):
3535
self.assertGreater(
3636
ecosystem.sort_key('1.13.6-2'), ecosystem.sort_key('1.13.6-1'))
3737

38+
# Check the 0 sentinel value.
39+
self.assertLess(ecosystem.sort_key('0'), ecosystem.sort_key('0:0~0-0'))
40+
3841
# Test that <end-of-life> specifically is greater than normal versions
3942
self.assertGreater(
4043
ecosystem.sort_key('<end-of-life>'), ecosystem.sort_key('1.13.6-1'))

osv/ecosystems/ecosystems_base.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,58 @@
1616
from typing import Any
1717
from warnings import deprecated
1818
import bisect
19+
import functools
1920
import requests
2021
from urllib.parse import quote
2122

2223
from . import config
2324

2425

26+
@functools.total_ordering
27+
class VersionKey:
28+
"""A wrapper class for version keys."""
29+
30+
_key: Any
31+
_is_zero: bool
32+
33+
def __init__(self, key: Any = None, is_zero: bool = False):
34+
self._key = key
35+
self._is_zero = is_zero
36+
37+
def __lt__(self, other):
38+
if not isinstance(other, VersionKey):
39+
return NotImplemented
40+
41+
if self._is_zero:
42+
return not other._is_zero
43+
44+
if other._is_zero:
45+
return False
46+
47+
return self._key < other._key
48+
49+
def __eq__(self, other):
50+
if not isinstance(other, VersionKey):
51+
return NotImplemented
52+
53+
if self._is_zero:
54+
return other._is_zero
55+
56+
if other._is_zero:
57+
return False
58+
59+
return self._key == other._key
60+
61+
def __repr__(self):
62+
if self._is_zero:
63+
return 'VersionKey(is_zero=True)'
64+
65+
return f'VersionKey(key={self._key!r})'
66+
67+
68+
_VERSION_ZERO = VersionKey(is_zero=True)
69+
70+
2571
class OrderedEcosystem(ABC):
2672
"""Ecosystem helper that supports comparison between versions."""
2773

@@ -34,12 +80,19 @@ def __init__(self, suffix: str | None = None):
3480
self.suffix = suffix
3581

3682
@abstractmethod
37-
def sort_key(self, version: str) -> Any:
83+
def _sort_key(self, version: str) -> Any:
3884
"""Comparable key for a version.
3985
4086
If the version string is invalid, return a very large version.
4187
"""
4288

89+
def sort_key(self, version: str) -> VersionKey:
90+
"""Sort key."""
91+
if version == '0':
92+
return _VERSION_ZERO
93+
94+
return VersionKey(self._sort_key(version))
95+
4396
def sort_versions(self, versions: list[str]):
4497
"""Sort versions."""
4598
versions.sort(key=self.sort_key)

osv/ecosystems/haskell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Hackage(EnumerableEcosystem):
3232

3333
_API_PACKAGE_URL = 'https://hackage.haskell.org/package/{package}.json'
3434

35-
def sort_key(self, version):
35+
def _sort_key(self, version):
3636
"""Sort key.
3737
3838
The Haskell package version data type is defined at

osv/ecosystems/haskell_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ def test_sort_key(self):
4242
self.assertGreater(
4343
ecosystem.sort_key('1-20-0'), ecosystem.sort_key('1.20.0'))
4444

45+
# Check the 0 sentinel value.
46+
self.assertLess(ecosystem.sort_key('0'), ecosystem.sort_key('0.0.0.1'))
47+
4548
# Check >= / <= methods
4649
self.assertGreaterEqual(
4750
ecosystem.sort_key('1-20-0'), ecosystem.sort_key('1.20.0'))

0 commit comments

Comments
 (0)