mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-14 09:54:18 +02:00
Gitea changed the `dist.tarball` it advertises for already published versions twice: | Gitea | `dist.tarball` path of `@scope/name@1.0.0` | |---|---| | 1.27 | `%40scope%2Fname/-/1.0.0/name-1.0.0.tgz` | | 28.0, https://github.com/go-gitea/gitea/pull/39061 | `@scope%2Fname/-/1.0.0/name-1.0.0.tgz` | | 28.1, https://github.com/go-gitea/gitea/pull/39434 | `@scope/name/-/name-1.0.0.tgz` | pnpm 11.5.2+ verifies the tarball URLs pinned in its lockfile against the registry metadata, so frozen installs of lockfiles written against 1.27 fail with `ERR_PNPM_TARBALL_URL_MISMATCH`. - Versions uploaded before this PR get their 1.27 URL back. - New versions store their canonical tarball path at upload, so later URL changes can't reach published versions. The host still follows the request. Lockfiles written against 28.0 for scoped packages, or by pnpm 12 with `@scope:registry` against 28.1, need those pinned `tarball:` entries removed, as 1.27 lockfiles of the same versions need the other URL. Signed-off-by: silverwind <me@silverwind.io>
475 lines
13 KiB
Go
475 lines
13 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package npm
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha512"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dev/modules/json"
|
|
"gitea.dev/modules/test"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestParsePackage(t *testing.T) {
|
|
packageScope := "@scope"
|
|
packageName := "test-package"
|
|
packageFullName := packageScope + "/" + packageName
|
|
packageVersion := "1.0.1-Pre"
|
|
packageTag := "latest"
|
|
packageAuthor := "KN4CK3R"
|
|
packageBin := "gitea"
|
|
packageDescription := "Test Description"
|
|
repository := Repository{
|
|
Type: "gitea",
|
|
URL: "http://localhost:3000/gitea/test.git",
|
|
Directory: "packages/test-package",
|
|
}
|
|
|
|
dataBytes := buildTarball(map[string]string{
|
|
"package/package.json": `{"name": "@scope/test-package","version": "1.0.1-pre","description": "Test Description","author": "KN4CK3R"}`,
|
|
})
|
|
data := base64.StdEncoding.EncodeToString(dataBytes)
|
|
integrity := "sha512-" + base64Sha512(dataBytes)
|
|
|
|
t.Run("InvalidUpload", func(t *testing.T) {
|
|
p, _, err := ParseUpload(bytes.NewReader([]byte{0}))
|
|
assert.Nil(t, p)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("InvalidUploadNoData", func(t *testing.T) {
|
|
p, err := parseUploadPackage(&packageUpload{})
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidPackage)
|
|
})
|
|
|
|
t.Run("InvalidPackageName", func(t *testing.T) {
|
|
test := func(t *testing.T, name string) {
|
|
p, err := parseUploadPackage(&packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: name,
|
|
Name: name,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: name,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidPackageName)
|
|
}
|
|
|
|
test(t, " test ")
|
|
test(t, " test")
|
|
test(t, "test ")
|
|
test(t, "te st")
|
|
test(t, "Test")
|
|
test(t, "_test")
|
|
test(t, ".test")
|
|
test(t, "^test")
|
|
test(t, "te^st")
|
|
test(t, "te|st")
|
|
test(t, "te)(st")
|
|
test(t, "te'st")
|
|
test(t, "te!st")
|
|
test(t, "te*st")
|
|
test(t, "te~st")
|
|
test(t, "invalid/scope")
|
|
test(t, "@invalid/_name")
|
|
test(t, "@invalid/.name")
|
|
})
|
|
|
|
t.Run("ValidPackageName", func(t *testing.T) {
|
|
test := func(t *testing.T, name string) {
|
|
p, err := parseUploadPackage(&packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: name,
|
|
Name: name,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: name,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
|
|
}
|
|
|
|
test(t, "test")
|
|
test(t, "@scope/name")
|
|
test(t, "@scope/q")
|
|
test(t, "q")
|
|
test(t, "@scope/package-name")
|
|
test(t, "@scope/package.name")
|
|
test(t, "@scope/package_name")
|
|
test(t, "123name")
|
|
test(t, "----")
|
|
test(t, packageFullName)
|
|
})
|
|
|
|
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
|
version := "first-version"
|
|
p, err := parseUploadPackage(&packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: packageFullName,
|
|
Name: packageFullName,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
version: {
|
|
Name: packageFullName,
|
|
Version: version,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
|
|
})
|
|
|
|
t.Run("InvalidAttachment", func(t *testing.T) {
|
|
b, _ := json.Marshal(packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: packageFullName,
|
|
Name: packageFullName,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: packageFullName,
|
|
Version: packageVersion,
|
|
},
|
|
},
|
|
},
|
|
Attachments: map[string]*PackageAttachment{
|
|
"dummy.tgz": {},
|
|
},
|
|
})
|
|
|
|
p, _, err := ParseUpload(bytes.NewReader(b))
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidAttachment)
|
|
})
|
|
|
|
t.Run("InvalidData", func(t *testing.T) {
|
|
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
|
|
b, _ := json.Marshal(packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: packageFullName,
|
|
Name: packageFullName,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: packageFullName,
|
|
Version: packageVersion,
|
|
},
|
|
},
|
|
},
|
|
Attachments: map[string]*PackageAttachment{
|
|
filename: {
|
|
Data: "/",
|
|
},
|
|
},
|
|
})
|
|
|
|
p, _, err := ParseUpload(bytes.NewReader(b))
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidAttachment)
|
|
})
|
|
|
|
t.Run("InvalidIntegrity", func(t *testing.T) {
|
|
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
|
|
b, _ := json.Marshal(packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: packageFullName,
|
|
Name: packageFullName,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: packageFullName,
|
|
Version: packageVersion,
|
|
Dist: PackageDistribution{
|
|
Integrity: "sha512-test==",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Attachments: map[string]*PackageAttachment{
|
|
filename: {
|
|
Data: data,
|
|
},
|
|
},
|
|
})
|
|
|
|
p, _, err := ParseUpload(bytes.NewReader(b))
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidIntegrity)
|
|
})
|
|
|
|
t.Run("InvalidIntegrity2", func(t *testing.T) {
|
|
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
|
|
b, _ := json.Marshal(packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: packageFullName,
|
|
Name: packageFullName,
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: packageFullName,
|
|
Version: packageVersion,
|
|
Dist: PackageDistribution{
|
|
Integrity: integrity,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Attachments: map[string]*PackageAttachment{
|
|
filename: {
|
|
Data: base64.StdEncoding.EncodeToString([]byte("data")),
|
|
},
|
|
},
|
|
})
|
|
|
|
p, _, err := ParseUpload(bytes.NewReader(b))
|
|
assert.Nil(t, p)
|
|
assert.ErrorIs(t, err, ErrInvalidIntegrity)
|
|
})
|
|
|
|
t.Run("Valid", func(t *testing.T) {
|
|
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
|
|
b, _ := json.Marshal(packageUpload{
|
|
PackageMetadata: PackageMetadata{
|
|
ID: packageFullName,
|
|
Name: packageFullName,
|
|
DistTags: map[string]string{
|
|
packageTag: packageVersion,
|
|
},
|
|
Versions: map[string]*PackageMetadataVersion{
|
|
packageVersion: {
|
|
Name: packageFullName,
|
|
Version: packageVersion,
|
|
Description: packageDescription,
|
|
Author: User{Name: packageAuthor},
|
|
License: "MIT",
|
|
Homepage: "https://gitea.io/",
|
|
Readme: packageDescription,
|
|
Dependencies: map[string]string{
|
|
"package": "1.2.0",
|
|
},
|
|
Bin: map[string]string{
|
|
"bin": packageBin,
|
|
},
|
|
Dist: PackageDistribution{
|
|
Integrity: integrity,
|
|
},
|
|
Repository: repository,
|
|
},
|
|
},
|
|
},
|
|
Attachments: map[string]*PackageAttachment{
|
|
filename: {
|
|
Data: data,
|
|
},
|
|
packageFullName + "-" + packageVersion + ".sigstore": {
|
|
Data: "{}",
|
|
},
|
|
},
|
|
})
|
|
|
|
p, _, err := ParseUpload(bytes.NewReader(b))
|
|
assert.NotNil(t, p)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, packageFullName, p.Name)
|
|
assert.Equal(t, packageVersion, p.Version)
|
|
assert.Equal(t, []string{packageTag}, p.DistTags)
|
|
assert.Equal(t, strings.ToLower(fmt.Sprintf("%s-%s.tgz", packageName, packageVersion)), p.Filename)
|
|
assert.Equal(t, fmt.Sprintf("%s/-/%s-%s.tgz", packageFullName, packageName, packageVersion), p.Metadata.GiteaTarballPath)
|
|
b, _ = base64.StdEncoding.DecodeString(data)
|
|
assert.Equal(t, b, p.Data)
|
|
assert.Equal(t, packageName, p.Metadata.Name)
|
|
assert.Equal(t, packageScope, p.Metadata.Scope)
|
|
assert.Equal(t, packageDescription, p.Metadata.Description)
|
|
assert.Equal(t, packageDescription, p.Metadata.Readme)
|
|
assert.Equal(t, packageAuthor, p.Metadata.Author)
|
|
assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
|
|
assert.Equal(t, "MIT", string(p.Metadata.License))
|
|
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
|
|
assert.Contains(t, p.Metadata.Dependencies, "package")
|
|
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
|
|
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
|
|
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
|
|
assert.Equal(t, repository.Directory, p.Metadata.Repository.Directory)
|
|
})
|
|
|
|
t.Run("ValidLicenseMap", func(t *testing.T) {
|
|
packageJSON := `{
|
|
"versions": {
|
|
"0.1.1": {
|
|
"name": "dev-null",
|
|
"version": "0.1.1",
|
|
"license": {
|
|
"type": "MIT"
|
|
},
|
|
"dist": {
|
|
"integrity": "sha256-"
|
|
}
|
|
}
|
|
},
|
|
"_attachments": {
|
|
"foo": {
|
|
"data": "AAAA"
|
|
}
|
|
}
|
|
}`
|
|
p, _, err := ParseUpload(strings.NewReader(packageJSON))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "MIT", string(p.Metadata.License))
|
|
})
|
|
|
|
t.Run("ValidRepositoryAndBinAsString", func(t *testing.T) {
|
|
// npm allows "repository" and "bin" to be plain strings, not only objects.
|
|
packageJSON := `{
|
|
"versions": {
|
|
"0.1.1": {
|
|
"name": "dev-null",
|
|
"version": "0.1.1",
|
|
"bin": "./cli.js",
|
|
"repository": "https://gitea.io/gitea/test.git",
|
|
"dist": {
|
|
"integrity": "sha256-"
|
|
}
|
|
}
|
|
},
|
|
"_attachments": {
|
|
"foo": {
|
|
"data": "AAAA"
|
|
}
|
|
}
|
|
}`
|
|
p, _, err := ParseUpload(strings.NewReader(packageJSON))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
|
|
// a string bin is named after the package
|
|
require.Equal(t, "./cli.js", p.Metadata.Bin["dev-null"])
|
|
})
|
|
}
|
|
|
|
// buildTarball assembles a gzipped tar with the given entries.
|
|
func buildTarball(files map[string]string) []byte {
|
|
return test.WriteTarCompression(gzip.NewWriter, files).Bytes()
|
|
}
|
|
|
|
func TestInspectTarball(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantShrinkwrap, wantInstaller bool
|
|
}{
|
|
{
|
|
name: "empty",
|
|
files: map[string]string{},
|
|
},
|
|
{
|
|
name: "shrinkwrap only",
|
|
files: map[string]string{"package/npm-shrinkwrap.json": "{}"},
|
|
wantShrinkwrap: true,
|
|
},
|
|
{
|
|
name: "postinstall only",
|
|
files: map[string]string{"package/package.json": `{"scripts":{"postinstall":"echo hi"}}`},
|
|
wantInstaller: true,
|
|
},
|
|
{
|
|
name: "preinstall",
|
|
files: map[string]string{"package/package.json": `{"scripts":{"preinstall":"noop"}}`},
|
|
wantInstaller: true,
|
|
},
|
|
{
|
|
name: "install",
|
|
files: map[string]string{"package/package.json": `{"scripts":{"install":"noop"}}`},
|
|
wantInstaller: true,
|
|
},
|
|
{
|
|
name: "whitespace-only script does not count",
|
|
files: map[string]string{"package/package.json": `{"scripts":{"postinstall":" "}}`},
|
|
},
|
|
{
|
|
name: "unrelated lifecycle script ignored",
|
|
files: map[string]string{"package/package.json": `{"scripts":{"test":"jest"}}`},
|
|
},
|
|
{
|
|
name: "both",
|
|
files: map[string]string{
|
|
"package/npm-shrinkwrap.json": "{}",
|
|
"package/package.json": `{"scripts":{"install":"go"}}`,
|
|
},
|
|
wantShrinkwrap: true,
|
|
wantInstaller: true,
|
|
},
|
|
{
|
|
name: "nested shrinkwrap ignored",
|
|
files: map[string]string{
|
|
"package/subdir/npm-shrinkwrap.json": "{}",
|
|
},
|
|
},
|
|
{
|
|
name: "leading ./ prefix stripped",
|
|
files: map[string]string{"./package/npm-shrinkwrap.json": "{}"},
|
|
// npm pack sometimes emits "./package/..." entries.
|
|
wantShrinkwrap: true,
|
|
},
|
|
{
|
|
name: "gyp file implies node-gyp install",
|
|
files: map[string]string{"package/binding.gyp": "{}"},
|
|
wantInstaller: true,
|
|
},
|
|
{
|
|
name: "gypfile false disables gyp install",
|
|
files: map[string]string{"package/binding.gyp": "{}", "package/package.json": `{"gypfile":false}`},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
data := buildTarball(c.files)
|
|
gotShrink, gotInstaller := inspectTarball(data)
|
|
assert.Equal(t, c.wantShrinkwrap, gotShrink, "shrinkwrap")
|
|
assert.Equal(t, c.wantInstaller, gotInstaller, "installScript")
|
|
})
|
|
}
|
|
|
|
t.Run("malformed gzip returns false", func(t *testing.T) {
|
|
hasShrinkwrap, hasInstaller := inspectTarball([]byte("not a gzip"))
|
|
assert.False(t, hasShrinkwrap)
|
|
assert.False(t, hasInstaller)
|
|
})
|
|
|
|
t.Run("malformed package.json falls through", func(t *testing.T) {
|
|
data := buildTarball(map[string]string{"package/package.json": "{ this is not json"})
|
|
_, hasInstaller := inspectTarball(data)
|
|
assert.False(t, hasInstaller)
|
|
})
|
|
}
|
|
|
|
func TestParseUpload(t *testing.T) {
|
|
pkg := "@scope/test-package"
|
|
|
|
t.Run("dispatches deprecate on missing _attachments", func(t *testing.T) {
|
|
body := fmt.Sprintf(`{"name":%q,"versions":{"1.0.0":{"deprecated":"gone"},"1.0.1":{"deprecated":""},"1.0.2":{},"1.0.3":null}}`, pkg)
|
|
p, dep, err := ParseUpload(strings.NewReader(body))
|
|
require.NoError(t, err)
|
|
assert.Nil(t, p)
|
|
require.NotNil(t, dep)
|
|
assert.Equal(t, map[string]string{"1.0.0": "gone", "1.0.1": ""}, dep.Versions)
|
|
})
|
|
}
|
|
|
|
func base64Sha512(data []byte) string {
|
|
h := sha512.Sum512(data)
|
|
return base64.StdEncoding.EncodeToString(h[:])
|
|
}
|