NuGet prerelease lookups match other versions and miss equivalent ones

Summary

NuGet reads a prerelease label as text, so 1.0-beta and 1.0-beta.0 are two versions, and it ignores build metadata, so 2.0.0-alpha+build is the same version as 2.0.0-alpha. GitLab's NuGet lookups get both wrong for prerelease versions:

  • The normalized version stored for a prerelease version reads the dots and leading zeros of its label as version parts: 1.0-beta is stored as 1.0-beta.0, and 1.0.0-beta.007abc as 1.0.0-beta.7abc. A lookup of 1.0-beta.0 therefore finds a 1.0-beta package.
  • A lookup of a prerelease version keeps its build metadata, so after 2.0.0-alpha, a push of 2.0.0-alpha+build finds no package. The other order finds it, through the stored normalized version.
  • When it does, the file keeps the pushed version in its name: after 2.0.0-alpha+b, a push of 2.0.0-alpha adds foo.2.0.0-alpha.nupkg to that package, while a download of 2.0.0-alpha looks up foo.2.0.0-alpha+b.nupkg, the name built from the package's version, and serves the older file.
  • The lease of CreateOrUpdatePackageService is keyed on the prerelease version as pushed, so pushes of 2.0.0-alpha and 2.0.0-alpha+build at the same time take two leases, although the duplicate check of 2.0.0-alpha finds a stored 2.0.0-alpha+build. Both can create a package.

!265928 (merged) normalizes only release versions in these lookups and leaves these cases as they are on master: fixing the first one needs a backfill of packages_nuget_metadata.normalized_version, and the others are outside that merge request's scope.

Steps to reproduce

  1. In a group, turn off Allow duplicates for NuGet packages and leave Exceptions empty.
  2. In a project of that group, push a package as 2.0.0-alpha, then as 2.0.0-alpha+build.
  3. Push another package as 1.0-beta, then send DELETE /api/v4/projects/:id/packages/nuget/<name>/1.0-beta.0.
  4. In a group that allows duplicates, push a third package as 2.0.0-alpha+b, then as 2.0.0-alpha with another description, and download 2.0.0-alpha.

Example Project

Not needed: the steps work in any group with the NuGet registry.

What is the current bug behavior?

The push of 2.0.0-alpha+build answers 201 Created and creates a second package for the same NuGet version. The DELETE of 1.0-beta.0, a version that was never pushed, marks the 1.0-beta package for destruction. The download of 2.0.0-alpha returns the file of the first push.

What is the expected correct behavior?

The second push is refused with 409 Conflict, the DELETE answers 404 Not Found, and the download returns the file of the second push.

Relevant logs and/or screenshots

Not applicable.

Output of checks

Found in the review of !265928 (merged). Specs on master and on that merge request's branch show the lookups, and the #nuget_file_name rows of package_spec.rb on that branch show that a prerelease push keeps its own version in the file name, as master does. Not tried on GitLab.com.

Possible fixes

  1. Remove the build metadata of a requested prerelease version in lookups and in the lease key too. Unlike the rest of the normalization, removing it cannot make two different prerelease labels equal.
  2. When a prerelease push adds its file to a package whose version differs only by build metadata, name the file after that package's version, as a release push does.
  3. Normalize the release part of a prerelease version and keep its label as it is, so that 1.0-beta is stored as 1.0.0-beta, then backfill normalized_version for stored prerelease versions. A requested prerelease version can then be normalized the same way.
Edited by José M. Requena Plens