Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions go/osv/ecosystem/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
// - Echo:PyPI - Python packages (PyPI/PEP 440 versioning)
// - Echo:Maven - Maven packages (Maven versioning)
// - Echo:npm - npm packages (SemVer versioning, +echo.N aware)
// - Echo:NuGet - NuGet packages (NuGet versioning, +echo.N aware)
//
// Versioning is delegated to the underlying ecosystem helper.
type echoEcosystem struct {
Expand All @@ -40,7 +41,9 @@ func echoFactory(p *Provider, suffix string) Ecosystem {
case strings.EqualFold(suffix, "maven"):
return echoEcosystem{Ecosystem: mavenEcosystem{p: p}}
case strings.EqualFold(suffix, "npm"):
return echoEcosystem{Ecosystem: echoSemverEcosystem{}}
return echoEcosystem{Ecosystem: echoBuildEcosystem{Ecosystem: semverLikeEcosystem{}}}
case strings.EqualFold(suffix, "nuget"):
return echoEcosystem{Ecosystem: echoBuildEcosystem{Ecosystem: nugetEcosystem{p: p}}}
default:
return echoEcosystem{Ecosystem: dpkgEcosystem{}}
}
Expand All @@ -63,43 +66,44 @@ func echoBuildNumber(version string) int {
return n
}

// echoSemverEcosystem orders Echo:npm packages. npm uses SemVer, which
// excludes build metadata from precedence, so Echo's `+echo.N` builds would
// otherwise compare equal to the base version and to each other. PyPI and
// Maven order `+echo.N` natively (local versions / qualifiers); npm does not,
// so we tie-break on the echo build number to keep
// `1.2.3 < 1.2.3+echo.1 < 1.2.3+echo.2 < 1.2.4`.
// echoBuildEcosystem orders Echo:npm and Echo:NuGet packages. npm and NuGet
// follow SemVer, which excludes build metadata from precedence, so Echo's
// `+echo.N` builds would otherwise compare equal to the base version and to
// each other. PyPI and Maven order `+echo.N` natively (local versions /
// qualifiers); npm and NuGet do not, so we tie-break on the echo build number
// to keep `1.2.3 < 1.2.3+echo.1 < 1.2.3+echo.2 < 1.2.4`.
//
// It embeds semverLikeEcosystem (the ECOSYSTEM version type), matching how
// Echo advisories express their ranges.
type echoSemverEcosystem struct {
semverLikeEcosystem
// It wraps the ecosystem's own helper (semverLikeEcosystem for npm, the
// ECOSYSTEM version type matching how Echo advisories express their ranges;
// nugetEcosystem for NuGet, which also handles four-part versions).
type echoBuildEcosystem struct {
Ecosystem
}

func (e echoSemverEcosystem) Parse(version string) (Version, error) {
inner, err := e.semverLikeEcosystem.Parse(version)
func (e echoBuildEcosystem) Parse(version string) (Version, error) {
inner, err := e.Ecosystem.Parse(version)
if err != nil {
return nil, err
}

return echoSemverVersion{inner: inner, build: echoBuildNumber(version)}, nil
return echoBuildVersion{inner: inner, build: echoBuildNumber(version)}, nil
}

// echoSemverVersion is a SemVer version paired with its `+echo.N` build number.
type echoSemverVersion struct {
// echoBuildVersion is a version paired with its `+echo.N` build number.
type echoBuildVersion struct {
inner Version
build int
}

var _ Version = echoSemverVersion{}
var _ Version = echoBuildVersion{}

func (v echoSemverVersion) Compare(other Version) (int, error) {
otherV, ok := other.(echoSemverVersion)
func (v echoBuildVersion) Compare(other Version) (int, error) {
otherV, ok := other.(echoBuildVersion)
if !ok {
return 0, ErrVersionEcosystemMismatch
}

// SemVer precedence first (build metadata is ignored there); if equal,
// Inner precedence first (build metadata is ignored there); if equal,
// tie-break on the echo build number.
if c, err := v.inner.Compare(otherV.inner); err != nil || c != 0 {
return c, err
Expand Down
28 changes: 28 additions & 0 deletions go/osv/ecosystem/echo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,34 @@ func TestEchoEcosystem_NPM(t *testing.T) {
runEchoTest(t, e, tests)
}

// NuGet also follows SemVer precedence (build metadata ignored), so Echo:NuGet
// tie-breaks on +echo.N too, on top of NuGet's own four-part versions and
// case-insensitive prereleases.
func TestEchoEcosystem_NuGet(t *testing.T) {
e := echoFactory(nil, "nuget")
tests := []echoTestCase{
// Base NuGet ordering (unchanged).
{"1.0.1", "1.0.0", 1},
{"1.0.0", "1.0.0-rc.0", 1},
{"4.3.0.1", "4.3.0", 1},
{"4.3.0.0", "4.3.0", 0},
{"1.0.0-BETA", "1.0.0-beta", 0},
// +echo.N ordering: base < echo.1 < echo.2 < echo.10 < next patch.
{"12.0.3+echo.1", "12.0.3", 1},
{"12.0.3+echo.2", "12.0.3+echo.1", 1},
{"12.0.3+echo.10", "12.0.3+echo.2", 1},
{"12.0.4", "12.0.3+echo.1", 1},
{"12.0.3+echo.1", "12.0.3+echo.1", 0},
// Four-part versions.
{"4.3.0.1+echo.2", "4.3.0.1+echo.1", 1},
{"4.3.0.2", "4.3.0.1+echo.1", 1},
// A +echo.N build of a prerelease still sorts before the final release.
{"8.0.0-rc.1+echo.1", "8.0.0-rc.1", 1},
{"8.0.0", "8.0.0-rc.1+echo.1", 1},
}
runEchoTest(t, e, tests)
}

func TestEchoEcosystem_Maven(t *testing.T) {
e := echoFactory(nil, "maven")
tests := []echoTestCase{
Expand Down
39 changes: 39 additions & 0 deletions osv/ecosystems/_ecosystems_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,45 @@ def test_echo_npm_ecosystem(self):
self.assertLess(
echo_npm.sort_key('19.0.0-next.3+echo.1'), echo_npm.sort_key('19.0.0'))

def test_echo_nuget_ecosystem(self):
"""Test that Echo:NuGet uses NuGet ordering and is +echo.N aware"""
self.assertTrue(ecosystems.is_known('Echo:NuGet'))

echo_nuget = ecosystems.get('Echo:NuGet')
self.assertIsNotNone(echo_nuget)

# Base NuGet ordering (including prereleases and four-part versions).
self.assertLess(echo_nuget.sort_key('1.0.0'), echo_nuget.sort_key('1.0.1'))
self.assertLess(
echo_nuget.sort_key('1.0.0-rc.0'), echo_nuget.sort_key('1.0.0'))
self.assertLess(
echo_nuget.sort_key('4.3.0'), echo_nuget.sort_key('4.3.0.1'))

# NuGet follows SemVer precedence (build metadata ignored), but Echo's
# +echo.N builds must still order: 12.0.3 < 12.0.3+echo.1 < ... < 12.0.4.
self.assertLess(
echo_nuget.sort_key('12.0.3'), echo_nuget.sort_key('12.0.3+echo.1'))
self.assertLess(
echo_nuget.sort_key('12.0.3+echo.1'),
echo_nuget.sort_key('12.0.3+echo.2'))
self.assertLess(
echo_nuget.sort_key('12.0.3+echo.2'),
echo_nuget.sort_key('12.0.3+echo.10'))
self.assertLess(
echo_nuget.sort_key('12.0.3+echo.1'), echo_nuget.sort_key('12.0.4'))
self.assertLess(
echo_nuget.sort_key('4.3.0.1+echo.1'),
echo_nuget.sort_key('4.3.0.1+echo.2'))
self.assertLess(
echo_nuget.sort_key('4.3.0.1+echo.2'), echo_nuget.sort_key('4.3.0.2'))

# A +echo.N build of a prerelease still sorts before the final release.
self.assertLess(
echo_nuget.sort_key('8.0.0-rc.1'),
echo_nuget.sort_key('8.0.0-rc.1+echo.1'))
self.assertLess(
echo_nuget.sort_key('8.0.0-rc.1+echo.1'), echo_nuget.sort_key('8.0.0'))

def test_echo_base_ecosystem(self):
"""Test that plain Echo uses Debian version ordering"""
echo = ecosystems.get('Echo')
Expand Down
11 changes: 8 additions & 3 deletions osv/ecosystems/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .debian import DPKG
from .ecosystems_base import OrderedEcosystem
from .maven import Maven
from .nuget import NuGet
from .pypi import PyPI
from .semver_ecosystem_helper import SemverLike

Expand All @@ -39,26 +40,30 @@ class Echo(OrderedEcosystem):
- Echo:PyPI - Python packages (PyPI/PEP 440 versioning)
- Echo:Maven - Maven packages (Maven versioning)
- Echo:npm - npm packages (SemVer versioning, +echo.N aware)
- Echo:NuGet - NuGet packages (NuGet versioning, +echo.N aware)
"""

def _delegate(self) -> OrderedEcosystem:
"""The ecosystem helper for this ecosystem's suffix (dpkg if none)."""
suffix = self.suffix.lower() if self.suffix else ''
if suffix == 'pypi':
return PyPI()
if suffix == 'maven':
return Maven()
if suffix == 'npm':
return SemverLike()
if suffix == 'nuget':
return NuGet()
return DPKG()

def _sort_key(self, version: str):
delegate = self._delegate()
key = delegate._sort_key(version) # pylint: disable=protected-access
if isinstance(delegate, SemverLike):
if isinstance(delegate, (SemverLike, NuGet)):
# SemVer excludes build metadata from precedence, so `1.2.3`,
# `1.2.3+echo.1` and `1.2.3+echo.2` would all compare equal. PyPI and
# Maven order `+echo.N` natively (local versions / qualifiers); npm does
# not, so tie-break on the build number to keep
# Maven order `+echo.N` natively (local versions / qualifiers); npm and
# NuGet do not, so tie-break on the build number to keep
# `1.2.3 < 1.2.3+echo.1 < 1.2.3+echo.2 < 1.2.4`.
return (key, _echo_build_number(version))
return key
Expand Down
Loading