Skip to content

Commit 861a254

Browse files
committed
Canonicalize Swift package identifiers
1 parent f35f9a1 commit 861a254

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

internal/handler/swift.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func (h *SwiftHandler) handlePackageReleases(w http.ResponseWriter, r *http.Requ
6363
writeSwiftProblem(w, http.StatusBadRequest, "invalid package identifier")
6464
return
6565
}
66+
scope, name = canonicalSwiftPackage(scope, name)
6667

6768
upstreamURL := h.buildUpstreamURL(scope, name, "", "", r.URL.RawQuery)
6869
body, contentType, responseHeaders, err := h.fetchMetadataWithHeaders(
@@ -98,6 +99,7 @@ func (h *SwiftHandler) handleRelease(w http.ResponseWriter, r *http.Request) {
9899
writeSwiftProblem(w, http.StatusBadRequest, "invalid package release")
99100
return
100101
}
102+
scope, name = canonicalSwiftPackage(scope, name)
101103

102104
upstreamURL := h.buildUpstreamURL(scope, name, version, "", r.URL.RawQuery)
103105
body, contentType, err := h.proxy.FetchOrCacheMetadata(
@@ -118,6 +120,7 @@ func (h *SwiftHandler) handleManifest(w http.ResponseWriter, r *http.Request) {
118120
writeSwiftProblem(w, http.StatusBadRequest, "invalid package release")
119121
return
120122
}
123+
scope, name = canonicalSwiftPackage(scope, name)
121124

122125
upstreamURL := h.buildUpstreamURL(scope, name, version, "Package.swift", r.URL.RawQuery)
123126
h.proxySwiftResource(w, r, upstreamURL, swiftAcceptManifest)
@@ -151,6 +154,7 @@ func (h *SwiftHandler) handleSourceArchive(w http.ResponseWriter, r *http.Reques
151154
writeSwiftProblem(w, http.StatusBadRequest, "invalid package release")
152155
return
153156
}
157+
scope, name = canonicalSwiftPackage(scope, name)
154158

155159
packageName := scope + "/" + name
156160
filename := fmt.Sprintf("%s-%s.zip", name, version)
@@ -597,6 +601,10 @@ func validSwiftPackageReference(scope, name, version string) bool {
597601
return validSwiftScope(scope) && validSwiftPackageName(name) && version != "" && version != "." && version != ".." && !strings.ContainsAny(version, "/\\")
598602
}
599603

604+
func canonicalSwiftPackage(scope, name string) (string, string) {
605+
return strings.ToLower(scope), strings.ToLower(name)
606+
}
607+
600608
func validSwiftScope(scope string) bool {
601609
return validSwiftIdentifier(scope, swiftMaxScopeLength, "-")
602610
}

internal/handler/swift_test.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestSwiftPackageReleasesRewritesRegistryURLs(t *testing.T) {
3333

3434
proxy, _, _, _ := setupTestProxy(t)
3535
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
36-
req := httptest.NewRequest(http.MethodGet, "/apple/swift-argument-parser", nil)
36+
req := httptest.NewRequest(http.MethodGet, "/APPLE/SWIFT-ARGUMENT-PARSER", nil)
3737
req.Header.Set("Accept", swiftAcceptJSON)
3838
w := httptest.NewRecorder()
3939
handler.ServeHTTP(w, req)
@@ -83,7 +83,7 @@ func TestSwiftReleaseMetadataSupportsJSONExtensionAndHead(t *testing.T) {
8383
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
8484

8585
for _, method := range []string{http.MethodGet, http.MethodHead} {
86-
req := httptest.NewRequest(method, "/apple/example/1.2.3.json", nil)
86+
req := httptest.NewRequest(method, "/APPLE/EXAMPLE/1.2.3.json", nil)
8787
w := httptest.NewRecorder()
8888
handler.ServeHTTP(w, req)
8989
if w.Code != http.StatusOK {
@@ -121,7 +121,7 @@ func TestSwiftManifestProxiesQueryAndRewritesLinks(t *testing.T) {
121121

122122
proxy, _, _, _ := setupTestProxy(t)
123123
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes()
124-
req := httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3/Package.swift?swift-version=5.9", nil)
124+
req := httptest.NewRequest(http.MethodGet, "/APPLE/EXAMPLE/1.2.3/Package.swift?swift-version=5.9", nil)
125125
req.Header.Set("Accept", swiftAcceptManifest)
126126
w := httptest.NewRecorder()
127127
handler.ServeHTTP(w, req)
@@ -271,6 +271,65 @@ func TestSwiftSourceArchiveRejectsChecksumMismatch(t *testing.T) {
271271
}
272272
}
273273

274+
func TestSwiftSourceArchiveCanonicalizesPackageIdentity(t *testing.T) {
275+
archive := []byte("swift source archive")
276+
checksumBytes := sha256.Sum256(archive)
277+
checksum := hex.EncodeToString(checksumBytes[:])
278+
var metadataPaths []string
279+
280+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
281+
metadataPaths = append(metadataPaths, r.URL.Path)
282+
w.Header().Set("Content-Type", "application/json")
283+
_, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum)
284+
}))
285+
defer upstream.Close()
286+
287+
proxy, db, store, fetcher := setupTestProxy(t)
288+
handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes()
289+
requestArchive := func(path string) {
290+
fetcher.artifact = &fetch.Artifact{
291+
Body: io.NopCloser(strings.NewReader(string(archive))),
292+
Size: int64(len(archive)),
293+
ContentType: "application/zip",
294+
}
295+
w := httptest.NewRecorder()
296+
handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
297+
if w.Code != http.StatusOK {
298+
t.Fatalf("GET %s status = %d, want 200; body: %s", path, w.Code, w.Body.String())
299+
}
300+
}
301+
302+
requestArchive("/apple/example/1.2.3.zip")
303+
requestArchive("/APPLE/EXAMPLE/1.2.3.zip")
304+
305+
if len(store.files) != 1 {
306+
t.Errorf("cached files = %d, want 1", len(store.files))
307+
}
308+
for _, path := range metadataPaths {
309+
if path != "/apple/example/1.2.3" {
310+
t.Errorf("metadata path = %q, want canonical lowercase path", path)
311+
}
312+
}
313+
314+
canonicalPURL := packageurl.MakeString("swift", "apple/example", "")
315+
canonical, err := db.GetPackageByPURL(canonicalPURL)
316+
if err != nil {
317+
t.Fatalf("getting canonical package: %v", err)
318+
}
319+
if canonical == nil {
320+
t.Fatalf("canonical package %q not found", canonicalPURL)
321+
}
322+
323+
nonCanonicalPURL := packageurl.MakeString("swift", "APPLE/EXAMPLE", "")
324+
nonCanonical, err := db.GetPackageByPURL(nonCanonicalPURL)
325+
if err != nil {
326+
t.Fatalf("getting non-canonical package: %v", err)
327+
}
328+
if nonCanonical != nil {
329+
t.Errorf("non-canonical package %q was cached", nonCanonicalPURL)
330+
}
331+
}
332+
274333
func TestSwiftSourceArchiveHeadRejectsCachedChecksumMismatch(t *testing.T) {
275334
archive := []byte("cached archive")
276335
expectedChecksum := sha256.Sum256([]byte("expected archive"))

0 commit comments

Comments
 (0)