Guard cargo publishes against name and version collisions
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

- serialise publishes per crate with an advisory lock in one transaction
- add a unique index on normalised name and version without build metadata
- validate versions as strict SemVer 2.0 and reject an empty .crate
- renumber the migration to 0005
This commit is contained in:
2026-10-17 12:43:44 +11:00
parent 365d93d637
commit ce8026d46e
7 changed files with 195 additions and 37 deletions
+58 -3
View File
@@ -2,17 +2,72 @@ package database
import (
"context"
"errors"
"fmt"
"strings"
"github.com/jackc/pgx/v5/pgconn"
"git.unkin.net/unkin/artifactapi/internal/provider"
)
var ErrCargoNameTaken = errors.New("crate name already taken")
func cargoNormName(name string) string {
return strings.ReplaceAll(strings.ToLower(name), "-", "_")
}
// InsertCargoIndexEntry rejects a name that differs from an existing crate only
// by case or -/_ (ErrCargoNameTaken) and a version equal up to build metadata
// (ErrAlreadyExists). A per-crate advisory lock serialises concurrent publishes.
func (db *DB) InsertCargoIndexEntry(ctx context.Context, e *provider.CargoIndexEntry) error {
_, err := db.Pool.Exec(ctx, `
tx, err := db.Pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
norm := cargoNormName(e.Name)
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('cargo:' || $1 || '/' || $2))`, e.RepoName, norm); err != nil {
return err
}
rows, err := tx.Query(ctx, `
SELECT name, version FROM cargo_index
WHERE repo_name = $1 AND replace(lower(name), '-', '_') = $2
`, e.RepoName, norm)
if err != nil {
return err
}
defer rows.Close()
base, _, _ := strings.Cut(e.Version, "+")
for rows.Next() {
var name, version string
if err := rows.Scan(&name, &version); err != nil {
return err
}
if name != e.Name {
return fmt.Errorf("%w: %q exists as %q", ErrCargoNameTaken, e.Name, name)
}
if v, _, _ := strings.Cut(version, "+"); v == base {
return fmt.Errorf("%w: %s@%s", ErrAlreadyExists, name, version)
}
}
if err := rows.Err(); err != nil {
return err
}
rows.Close()
if _, err := tx.Exec(ctx, `
INSERT INTO cargo_index (repo_name, file_path, name, version, line, yanked)
VALUES ($1, $2, $3, $4, $5, $6)
`, e.RepoName, e.FilePath, e.Name, e.Version, e.Line, e.Yanked)
return err
`, e.RepoName, e.FilePath, e.Name, e.Version, e.Line, e.Yanked); err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return fmt.Errorf("%w: %s@%s", ErrAlreadyExists, e.Name, e.Version)
}
return err
}
return tx.Commit(ctx)
}
func (db *DB) ListCargoIndexEntries(ctx context.Context, repoName, name string) ([]provider.CargoIndexEntry, error) {
+80
View File
@@ -2,7 +2,10 @@ package database
import (
"context"
"errors"
"fmt"
"os"
"sync"
"testing"
"time"
@@ -460,3 +463,80 @@ func TestCargoIndex(t *testing.T) {
t.Fatalf("after delete: %+v", entries)
}
}
func TestCargoIndexPublishConflicts(t *testing.T) {
requireDB(t)
entry := func(name, version string) *provider.CargoIndexEntry {
return &provider.CargoIndexEntry{
RepoName: "cargo-conflict", FilePath: "crates/" + name + "/" + name + "-" + version + ".crate",
Name: name, Version: version, Line: "{}",
}
}
if err := testDB.InsertCargoIndexEntry(ctx(), entry("foo-bar", "1.0.0+a")); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, version string
want error
}{
{"foo-bar", "1.0.0+b", ErrAlreadyExists},
{"foo-bar", "1.0.0", ErrAlreadyExists},
{"Foo-Bar", "2.0.0", ErrCargoNameTaken},
{"foo_bar", "2.0.0", ErrCargoNameTaken},
} {
if err := testDB.InsertCargoIndexEntry(ctx(), entry(tc.name, tc.version)); !errors.Is(err, tc.want) {
t.Errorf("%s@%s: err = %v, want %v", tc.name, tc.version, err, tc.want)
}
}
if err := testDB.InsertCargoIndexEntry(ctx(), entry("foo-bar", "1.0.1")); err != nil {
t.Fatal(err)
}
}
func TestCargoIndexConcurrentPublish(t *testing.T) {
requireDB(t)
names := []string{"Race", "race", "RACE", "race", "rAce", "RaCe", "racE", "race"}
errs := make(chan error, len(names))
var wg sync.WaitGroup
for i, name := range names {
wg.Add(1)
go func() {
defer wg.Done()
errs <- testDB.InsertCargoIndexEntry(ctx(), &provider.CargoIndexEntry{
RepoName: "cargo-race", FilePath: fmt.Sprintf("crates/%s/%d.crate", name, i),
Name: name, Version: "1.0.0", Line: "{}",
})
}()
}
wg.Wait()
close(errs)
ok := 0
for err := range errs {
switch {
case err == nil:
ok++
case !errors.Is(err, ErrAlreadyExists) && !errors.Is(err, ErrCargoNameTaken):
t.Errorf("unexpected error: %v", err)
}
}
if ok != 1 {
t.Fatalf("%d concurrent publishes succeeded, want 1", ok)
}
if entries, _ := testDB.ListCargoIndexEntries(ctx(), "cargo-race", "race"); len(entries) > 1 {
t.Fatalf("entries = %+v", entries)
}
}
func TestCargoIndexUniqueIndex(t *testing.T) {
requireDB(t)
insert := func(name, version, path string) error {
_, err := testDB.Pool.Exec(ctx(), `INSERT INTO cargo_index (repo_name, file_path, name, version, line) VALUES ('cargo-uniq', $1, $2, $3, '{}')`, path, name, version)
return err
}
if err := insert("a-b", "1.0.0+x", "p1"); err != nil {
t.Fatal(err)
}
if err := insert("A_B", "1.0.0+y", "p2"); err == nil {
t.Fatal("unique index allowed a normalised duplicate")
}
}