Skip to content

Commit

Permalink
Packagist version enumeration support (#675)
Browse files Browse the repository at this point in the history
Fixes #230 

Potential improvements before merging:
- From the Best Practices here: https://packagist.org/apidoc, it is
recommended to send a User-agent header to allow them to contact us to
resolve any bandwidth issues.
- Also can utilize `If-Modified-Since` headers to avoid expiring caches
too early when calling the API
- The version enumeration is not quite identical to the C implementation
in PHP, though matches the slightly ambiguous specification given in the
documentation. Should be fine for the vast majority of cases.

We should also think about pushing the packagist comparison upstream to
the univers library.
  • Loading branch information
another-rex authored Sep 13, 2022
1 parent d3e1ea0 commit 050c386
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 34 deletions.
58 changes: 25 additions & 33 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions osv/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def set(self, key, value, ttl):

class _CacheEntry:
data: typing.Any
# TODO(rexpan):
# Add more complex expiry logic by checking Last-Modified headers
expiry: float

def __init__(self, data, ttl):
Expand Down
31 changes: 30 additions & 1 deletion osv/ecosystems.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from . import debian_version_cache
from . import maven
from . import nuget
from . import packagist_version
from . import semver_index
from .cache import Cache
from .cache import cached
Expand Down Expand Up @@ -376,7 +377,7 @@ def version_is_valid(v):

versions = [v for v in raw_versions if version_is_valid(v)]
# Sort to ensure it is in the correct order
versions.sort(key=self.sort_key)
self.sort_versions(versions)
# The only versions with +deb
versions = [
x for x in versions
Expand All @@ -397,12 +398,40 @@ def version_is_valid(v):
return self._get_affected_versions(versions, introduced, fixed, limits)


class Packagist(Ecosystem):
"""Packagist ecosystem"""

_API_PACKAGE_URL = 'https://repo.packagist.org/p2/{package}.json'

def sort_key(self, version):
return packagist_version.PackagistVersion(version)

def enumerate_versions(self, package, introduced, fixed, limits=None):
url = self._API_PACKAGE_URL.format(package=package.lower())
request_helper = RequestHelper(shared_cache)
try:
text_response = request_helper.get(url)
except RequestError as ex:
if ex.response.status_code == 404:
raise EnumerateError(f'Package {package} not found') from ex
raise RuntimeError('Failed to get Packagist versions for '
f'{package} with: {ex.response.text}') from ex

response = json.loads(text_response)
versions: list[str] = [x['version'] for x in response['packages'][package]]
self.sort_versions(versions)
# TODO(rexpan): Potentially filter out branch versions like dev-master

return self._get_affected_versions(versions, introduced, fixed, limits)


_ecosystems = {
'crates.io': Crates(),
'Go': Go(),
'Maven': Maven(),
'npm': NPM(),
'NuGet': NuGet(),
'Packagist': Packagist(),
'PyPI': PyPI(),
'RubyGems': RubyGems(),
}
Expand Down
41 changes: 41 additions & 0 deletions osv/ecosystems_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

class GetNextVersionTest(unittest.TestCase):
"""get_next_version tests."""
_TEST_DATA_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'testdata')

def test_pypi(self):
"""Test PyPI."""
Expand Down Expand Up @@ -134,6 +136,45 @@ def test_debian(self, requests_mock: mock.MagicMock):
ecosystem.enumerate_versions('cyrus-sasl2', '0', None)
self.assertEqual(requests_mock.call_count, 2)

def test_packagist(self):
ecosystem = ecosystems.get('Packagist')
self.assertLess(
ecosystem.sort_key('4.3-2RC1'), ecosystem.sort_key('4.3-2RC2'))
self.assertGreater(
ecosystem.sort_key('4.3-2RC2'), ecosystem.sort_key('4.3-2beta5'))
self.assertGreater(
ecosystem.sort_key('4.3-2'), ecosystem.sort_key('4.3-2beta1'))
self.assertGreater(ecosystem.sort_key('1.0.0'), ecosystem.sort_key('1.0'))
self.assertEqual(
ecosystem.sort_key('1.0.0rc2'), ecosystem.sort_key('1.0.0.rc2'))

enumerated_versions = ecosystem.enumerate_versions('neos/neos', '3.3.0',
'4.4.0')
self.assertIn('4.3.19', enumerated_versions)
self.assertIn('4.2.18', enumerated_versions)
self.assertIn('3.3.1', enumerated_versions)
self.assertIn('3.3.0', enumerated_versions)

with open(os.path.join(self._TEST_DATA_DIR,
'packagist_test_cases.txt')) as file:
for line in file.readlines():
if line.startswith('//') or line.isspace():
continue
pieces = line.strip('\n').split(' ')
sort_value = ecosystem.sort_key(pieces[0]).__cmp__(
ecosystem.sort_key(pieces[2]))

if pieces[1] == '<':
expected_value = -1
elif pieces[1] == '=':
expected_value = 0
elif pieces[1] == '>':
expected_value = 1
else:
raise RuntimeError('Input not expected: ' + pieces[1])

self.assertEqual(expected_value, sort_value, pieces)

def test_semver(self):
"""Test SemVer."""
ecosystem = ecosystems.get('Go')
Expand Down
Loading

0 comments on commit 050c386

Please sign in to comment.