diff --git a/go/osv/ecosystem/echo.go b/go/osv/ecosystem/echo.go index bbd93baaee3..319399fc647 100644 --- a/go/osv/ecosystem/echo.go +++ b/go/osv/ecosystem/echo.go @@ -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 { @@ -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{}} } @@ -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 diff --git a/go/osv/ecosystem/echo_test.go b/go/osv/ecosystem/echo_test.go index 2398d955657..25239b2452a 100644 --- a/go/osv/ecosystem/echo_test.go +++ b/go/osv/ecosystem/echo_test.go @@ -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{ diff --git a/osv/ecosystems/_ecosystems_test.py b/osv/ecosystems/_ecosystems_test.py index 8c012eef4b5..faa257c57f9 100644 --- a/osv/ecosystems/_ecosystems_test.py +++ b/osv/ecosystems/_ecosystems_test.py @@ -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') diff --git a/osv/ecosystems/echo.py b/osv/ecosystems/echo.py index da8604f0f6a..ee9691b8baf 100644 --- a/osv/ecosystems/echo.py +++ b/osv/ecosystems/echo.py @@ -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 @@ -39,9 +40,11 @@ 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() @@ -49,16 +52,18 @@ def _delegate(self) -> OrderedEcosystem: 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