Skip to content
Merged
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
15 changes: 11 additions & 4 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ func IsDefaultRegistry(purlType, registryURL string) bool {
return true
}

cfg := TypeInfo(purlType)
if cfg == nil || cfg.DefaultRegistry == nil {
data, err := loadTypes()
if err != nil {
return false
}
cfg, ok := data.Types[purlType]
if !ok || cfg.DefaultRegistry == nil {
return false
}

Expand All @@ -21,8 +25,11 @@ func IsDefaultRegistry(purlType, registryURL string) bool {
return false
}

// Compare hosts
defaultHost := extractHost(defaultURL)
defaultHost, ok := data.defaultHosts[defaultURL]
if !ok {
// TypeInfo exposes the default URL through a shared pointer.
defaultHost = extractHost(defaultURL)
}
givenHost := extractHost(registryURL)

if defaultHost == "" || givenHost == "" {
Expand Down
96 changes: 96 additions & 0 deletions defaults_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package purl

import (
"net/url"
"strings"
"testing"
)

func TestBuildPURLRegistryHostSemantics(t *testing.T) {
for _, tc := range []struct {
registry string
isDefault bool
}{
{"https://registry.npmjs.org/package/-/package.tgz", true},
{"http://registry.npmjs.org:8080/package", true},
{"//registry.npmjs.org/package", true},
{"https://cdn.registry.npmjs.org/package", true},
{"https://user:pass@registry.npmjs.org/package", true},
{"https://evil.example@registry.npmjs.org/package", true},
{"https://registry.npmjs.org@evil.example/package", false},
{"https://registry.npmjs.org.evil.example/package", false},
{"https://notregistry.npmjs.org/package", false},
{"https://REGISTRY.NPMJS.ORG/package", false},
{"https://registry.npmjs.org./package", false},
{"https://registry.npmjs.org:bad/package", false},
{"https://registry.npmjs.org/%zz", false},
{"https://[::1]/package", false},
{"registry.npmjs.org/package", false},
} {
t.Run(tc.registry, func(t *testing.T) {
got := BuildPURLString("npm", "package", "1.2.3", tc.registry)
if tc.isDefault {
if got != "pkg:npm/package@1.2.3" {
t.Fatalf("default registry retained: %q", got)
}
} else if !strings.HasPrefix(got, "pkg:npm/package@1.2.3?repository_url=") {
t.Fatalf("registry lost: %q", got)
}
})
}
}

func TestDefaultRegistryPointerChanges(t *testing.T) {
cfg := TypeInfo("npm")
original := *cfg.DefaultRegistry
t.Cleanup(func() { *cfg.DefaultRegistry = original })
for _, registry := range []string{"https://custom.example/registry", "https://crates.io", "", "https://[invalid", original} {
*cfg.DefaultRegistry = registry
for _, input := range []string{original, "https://custom.example/package.tgz", "https://crates.io", ""} {
want := uncachedDefaultRegistry("npm", input)
if got := IsDefaultRegistry("npm", input); got != want {
t.Fatalf("default %q, input %q: got %t, want %t", registry, input, got, want)
}
got := BuildPURLString("npm", "package", "1.2.3", input)
if strings.Contains(got, "?repository_url=") == want {
t.Fatalf("default %q, input %q: unexpected PURL %q", registry, input, got)
}
}
}
}

func uncachedDefaultRegistry(purlType, registry string) bool {
if registry == "" {
return true
}
cfg := TypeInfo(purlType)
if cfg == nil || cfg.DefaultRegistry == nil {
return false
}
standard, err := url.Parse(*cfg.DefaultRegistry)
if err != nil || standard.Hostname() == "" {
return false
}
given, err := url.Parse(registry)
if err != nil || given.Hostname() == "" {
return false
}
return given.Hostname() == standard.Hostname() || strings.HasSuffix(given.Hostname(), "."+standard.Hostname())
}

func FuzzDefaultRegistryCompatibility(f *testing.F) {
for _, kind := range append(KnownTypes(), "unknown") {
f.Add(kind, DefaultRegistry(kind))
f.Add(kind, "https://user@registry.npmjs.org:443/package.tgz")
f.Add(kind, "https://registry.npmjs.org@evil.example/%zz")
}
f.Fuzz(func(t *testing.T, kind, registry string) {
want := uncachedDefaultRegistry(kind, registry)
if got := IsDefaultRegistry(kind, registry); got != want {
t.Fatalf("IsDefaultRegistry(%q, %q) = %t, want %t", kind, registry, got, want)
}
if got := IsNonDefaultRegistry(kind, registry); got == want {
t.Fatalf("IsNonDefaultRegistry(%q, %q) = %t, want %t", kind, registry, got, !want)
}
})
}
89 changes: 89 additions & 0 deletions makepurl_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package purl

import (
"fmt"
"testing"
)

var benchmarkRegistryDefault bool

var resolvedURLCases = []struct {
name string
registry string
want string
}{
{"none", "", "pkg:npm/%40scope/package@1.2.3"},
{"default", "https://registry.npmjs.org/@scope/package/-/package-1.2.3.tgz", "pkg:npm/%40scope/package@1.2.3"},
{"subdomain", "https://cdn.registry.npmjs.org/@scope/package/-/package-1.2.3.tgz", "pkg:npm/%40scope/package@1.2.3"},
{"private", "https://npm.example.invalid/@scope/package/-/package-1.2.3.tgz", "pkg:npm/%40scope/package@1.2.3?repository_url=https:%2F%2Fnpm.example.invalid%2F%40scope%2Fpackage%2F-%2Fpackage-1.2.3.tgz"},
}

func TestBuildPURLResolvedURLs(t *testing.T) {
for _, tc := range resolvedURLCases {
t.Run(tc.name, func(t *testing.T) {
if got := BuildPURLString("npm", "@scope/package", "1.2.3", tc.registry); got != tc.want {
t.Fatalf("got %q, want %q", got, tc.want)
}
})
}
}

func BenchmarkBuildPURLResolvedURL(b *testing.B) {
for _, tc := range resolvedURLCases {
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchmarkVersionPURLString = BuildPURLString("npm", "@scope/package", "1.2.3", tc.registry)
}
if benchmarkVersionPURLString != tc.want {
b.Fatalf("got %q, want %q", benchmarkVersionPURLString, tc.want)
}
})
}
}

func BenchmarkBuildPURLResolvedURLMixed(b *testing.B) {
const count = 1000
type input struct{ name, url, want string }
inputs := make([]input, count)
for i := range inputs {
name := fmt.Sprintf("@scope/package-%d", i)
inputs[i] = input{
name: name,
url: "https://registry.npmjs.org/" + name + "/-/package-1.2.3.tgz",
want: fmt.Sprintf("pkg:npm/%%40scope/package-%d@1.2.3", i),
}
if got := BuildPURLString("npm", name, "1.2.3", inputs[i].url); got != inputs[i].want {
b.Fatalf("got %q, want %q", got, inputs[i].want)
}
}
i := 0
b.ReportAllocs()
for b.Loop() {
in := inputs[i%count]
benchmarkVersionPURLString = BuildPURLString("npm", in.name, "1.2.3", in.url)
i++
}
}

func BenchmarkDefaultRegistryResolvedURL(b *testing.B) {
for _, tc := range resolvedURLCases {
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchmarkRegistryDefault = IsDefaultRegistry("npm", tc.registry)
}
})
}
}

func BenchmarkCleanVersionNPM(b *testing.B) {
for _, version := range []string{"1.2.3", "1.2.3-beta.1", "^1.2.3"} {
b.Run(version, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchmarkVersionPURLString = CleanVersion(version, "npm")
}
})
}
}
18 changes: 13 additions & 5 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ func (t *TypeConfig) NamespaceProhibited() bool {
}

type typesData struct {
Version string `json:"version"`
Description string `json:"description"`
Source string `json:"source"`
LastUpdated string `json:"last_updated"`
Types map[string]TypeConfig `json:"types"`
defaultHosts map[string]string
Version string `json:"version"`
Description string `json:"description"`
Source string `json:"source"`
LastUpdated string `json:"last_updated"`
Types map[string]TypeConfig `json:"types"`
}

var (
Expand All @@ -72,6 +73,13 @@ func loadTypes() (*typesData, error) {
loadOnce.Do(func() {
loadedData = &typesData{}
loadErr = json.Unmarshal(typesJSON, loadedData)
loadedData.defaultHosts = make(map[string]string)
for _, cfg := range loadedData.Types {
if cfg.DefaultRegistry != nil {
registry := *cfg.DefaultRegistry
loadedData.defaultHosts[registry] = extractHost(registry)
}
}
})
return loadedData, loadErr
}
Expand Down