From 30c4c1f217aeaf7c28e9417a7191f6b254b546b9 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Fri, 30 Oct 2026 17:14:14 +0000 Subject: [PATCH] Build PURLs for Swift registry identities --- go.mod | 2 +- go.sum | 4 +-- internal/swift/urls.go | 9 +++++-- purl_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++ swift_test.go | 30 ++++++++++++++++++++++- 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 purl_test.go diff --git a/go.mod b/go.mod index d9f4b11..01d4564 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a github.com/git-pkgs/artifacts v0.2.1 github.com/git-pkgs/pom v0.1.8 - github.com/git-pkgs/purl v0.1.23 + github.com/git-pkgs/purl v0.2.1 github.com/git-pkgs/spdx v0.3.3 github.com/git-pkgs/vers v0.7.2 github.com/opencontainers/go-digest v1.0.0 diff --git a/go.sum b/go.sum index fc0710f..467e283 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/git-pkgs/artifacts v0.2.1 h1:VwdxR4yTDaqBZ34h0slxQBVyr2Xfa+QGOKCKviMx github.com/git-pkgs/artifacts v0.2.1/go.mod h1:Otosgq52pXT5UNN7lh6s/lszpWKVDO8XrOjN1M70/IA= github.com/git-pkgs/pom v0.1.8 h1:mXCuWw6XkrKKZb6IurRZyIznYLPhV5AYg5ZO38Uj/l8= github.com/git-pkgs/pom v0.1.8/go.mod h1:sQBeYRzjCUn77fcABPn6I/a1aBHFxm55jTmkNxYtGtw= -github.com/git-pkgs/purl v0.1.23 h1:RG7F73S4ZnbAnB/posWf2WS4OhdG0xUbyX8UIkEUhPw= -github.com/git-pkgs/purl v0.1.23/go.mod h1:l8rCfvt5iXVrFIcoQ8+vCLOaHhrtEW2id0LENP+IzMw= +github.com/git-pkgs/purl v0.2.1 h1:b46hRlOMbrG6aXjjFJdYixGi5WTh0TLlAnH9iNeuOS0= +github.com/git-pkgs/purl v0.2.1/go.mod h1:l8rCfvt5iXVrFIcoQ8+vCLOaHhrtEW2id0LENP+IzMw= github.com/git-pkgs/spdx v0.3.3 h1:xXlUoZzjDSkwxC6mWHfaW+kc8a0+LH6d+gGHuZWftqI= github.com/git-pkgs/spdx v0.3.3/go.mod h1:n9rAicgw+Wqtfursju6oKaAL6cUCTiIS926mb1KWoKU= github.com/git-pkgs/vers v0.7.2 h1:Ctex1O+Ai/FONmyDaw37EolBXg0og6UdxbCjjYbeRHs= diff --git a/internal/swift/urls.go b/internal/swift/urls.go index f976c51..d2f58fd 100644 --- a/internal/swift/urls.go +++ b/internal/swift/urls.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + "github.com/git-pkgs/purl" "github.com/git-pkgs/vers" ) @@ -66,5 +67,9 @@ func (u *URLs) Download(name, version string) string { func (u *URLs) Documentation(_, _ string) string { return "" } -// Swift PURLs identify source repositories, not registry-scoped packages. -func (u *URLs) PURL(_, _ string) string { return "" } +func (u *URLs) PURL(name, version string) string { + if _, err := identityPath(name); err != nil { + return "" + } + return purl.BuildPURLString(ecosystem, name, version, "") +} diff --git a/purl_test.go b/purl_test.go new file mode 100644 index 0000000..13291c5 --- /dev/null +++ b/purl_test.go @@ -0,0 +1,55 @@ +package registries_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/git-pkgs/purl" + "github.com/git-pkgs/registries" +) + +func TestFetchVersionFromPURLIdentities(t *testing.T) { + for _, tc := range []struct{ ecosystem, name, version, path string }{ + {"golang", "github.com/Azure/GoSDK", "v1.2.3", "/github.com/!azure/!go!s!d!k/@v/v1.2.3.info"}, + {"golang", "github.com/azure/gosdk", "v1.2.3", "/github.com/azure/gosdk/@v/v1.2.3.info"}, + {"swift", "Mona.Linked-List", "1.0.0", "/mona/linked-list/1.0.0"}, + } { + t.Run(tc.ecosystem+"/"+tc.name, func(t *testing.T) { + var paths []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + paths = append(paths, r.URL.Path) + if tc.ecosystem == "swift" && r.URL.Path == strings.TrimSuffix(tc.path, "/"+tc.version) { + _ = json.NewEncoder(w).Encode(map[string]any{"releases": map[string]any{tc.version: map[string]any{}}}) + return + } + if tc.ecosystem == "golang" && r.URL.Path == strings.TrimSuffix(tc.path, tc.version+".info")+"list" { + _, _ = w.Write([]byte(tc.version + "\n")) + return + } + if r.URL.Path != tc.path { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + if tc.ecosystem == "swift" { + _ = json.NewEncoder(w).Encode(swiftRelease(tc.version)) + } else { + _ = json.NewEncoder(w).Encode(map[string]string{"Version": tc.version, "Time": "2026-01-01T00:00:00Z"}) + } + })) + defer server.Close() + input := purl.BuildPURLString(tc.ecosystem, tc.name, tc.version, server.URL) + version, err := registries.FetchVersionFromPURL(context.Background(), input, registries.NewClient(registries.WithMaxRetries(0))) + if err != nil || version == nil || version.Number != tc.version { + t.Fatalf("FetchVersionFromPURL(%q) = %+v, %v; requests=%v", input, version, err, paths) + } + if len(paths) == 0 || paths[len(paths)-1] != tc.path { + t.Errorf("requests=%v, want %s", paths, tc.path) + } + }) + } +} diff --git a/swift_test.go b/swift_test.go index b8477c7..71e65fb 100644 --- a/swift_test.go +++ b/swift_test.go @@ -120,7 +120,6 @@ func TestSwiftRegistryURLsAndIdentity(t *testing.T) { t.Errorf("Registry = %q", got) } if urls.Download("mona.linked-list", "") != "" || - urls.PURL("mona.linked-list", "1.0.0") != "" || urls.Documentation("mona.linked-list", "1.0.0") != "" { t.Error("unexpected inferred URL") } @@ -132,6 +131,9 @@ func TestSwiftRegistryURLsAndIdentity(t *testing.T) { if _, err := reg.FetchPackage(context.Background(), name); err == nil { t.Errorf("accepted invalid identity %q", name) } + if got := urls.PURL(name, "1.0.0"); got != "" { + t.Errorf("PURL accepted invalid registry identity %q: %q", name, got) + } } for _, version := range []string{"../1", "1.0.0/../../x", "1.0.0?x", "main"} { if got := urls.Download("mona.linked-list", version); got != "" { @@ -147,6 +149,32 @@ func TestSwiftRegistryURLsAndIdentity(t *testing.T) { } } +func TestSwiftRegistryPURLs(t *testing.T) { + reg, err := registries.New("swift", "https://registry.example.com", nil) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct{ name, version, want string }{ + {"apple.swift-argument-parser", "1.7.0", "pkg:swift/apple/swift-argument-parser@1.7.0"}, + {"Mona.Linked-List", "", "pkg:swift/Mona/Linked-List"}, + {"My-Scope2.My_Package", "1.0.0+build", "pkg:swift/My-Scope2/My_Package@1.0.0%2Bbuild"}, + } { + t.Run(tc.name, func(t *testing.T) { + urls := registries.BuildURLs(reg.URLs(), tc.name, tc.version) + if got := urls["purl"]; got != tc.want { + t.Fatalf("PURL = %q, want %q", got, tc.want) + } + parsed, err := registries.ParsePURL(urls["purl"]) + if err != nil { + t.Fatal(err) + } + if parsed.FullName() != tc.name || parsed.Version != tc.version { + t.Errorf("round trip: name = %q, version = %q", parsed.FullName(), parsed.Version) + } + }) + } +} + func TestSwiftUnsupported(t *testing.T) { reg := newSwiftRegistry(t, func(_ http.ResponseWriter, r *http.Request) { t.Errorf("unexpected request %s", r.URL)