Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

507 lines
15 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package swift
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
packages_model "github.com/hanzoai/git/models/packages"
"github.com/hanzoai/git/modules/json"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/optional"
packages_module "github.com/hanzoai/git/modules/packages"
swift_module "github.com/hanzoai/git/modules/packages/swift"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/routers/api/packages/helper"
"github.com/hanzoai/git/services/context"
packages_service "github.com/hanzoai/git/services/packages"
"github.com/hashicorp/go-version"
)
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#35-api-versioning
const (
AcceptJSON = "application/vnd.swift.registry.v1+json"
AcceptSwift = "application/vnd.swift.registry.v1+swift"
AcceptZip = "application/vnd.swift.registry.v1+zip"
)
var (
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#361-package-scope
scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#362-package-name
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
)
type headers struct {
Status int
ContentType string
Digest string
Location string
Link string
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#35-api-versioning
func setResponseHeaders(resp http.ResponseWriter, h *headers) {
if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType)
}
if h.Digest != "" {
resp.Header().Set("Digest", "sha256="+h.Digest)
}
if h.Location != "" {
resp.Header().Set("Location", h.Location)
}
if h.Link != "" {
resp.Header().Set("Link", h.Link)
}
resp.Header().Set("Content-Version", "1")
if h.Status != 0 {
resp.WriteHeader(h.Status)
}
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#33-error-handling
func apiError(ctx *context.Context, status int, obj any) {
// https://www.rfc-editor.org/rfc/rfc7807
type Problem struct {
Status int `json:"status"`
Detail string `json:"detail"`
}
message := helper.ProcessErrorForUser(ctx, status, obj)
setResponseHeaders(ctx.Resp, &headers{
Status: status,
ContentType: "application/problem+json",
})
_ = json.NewEncoder(ctx.Resp).Encode(Problem{
Status: status,
Detail: message,
})
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#35-api-versioning
func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) {
return func(ctx *context.Context) {
accept := ctx.Req.Header.Get("Accept")
if accept != "" && accept != requiredAcceptHeader {
apiError(ctx, http.StatusBadRequest, fmt.Sprintf("Unexpected accept header. Should be '%s'.", requiredAcceptHeader))
}
}
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/PackageRegistryUsage.md#registry-authentication
func CheckAuthenticate(ctx *context.Context) {
if ctx.Doer == nil {
apiError(ctx, http.StatusUnauthorized, nil)
return
}
ctx.Status(http.StatusOK)
}
func buildPackageID(scope, name string) string {
return scope + "." + name
}
type Release struct {
URL string `json:"url"`
}
type EnumeratePackageVersionsResponse struct {
Releases map[string]Release `json:"releases"`
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#41-list-package-releases
func EnumeratePackageVersions(ctx *context.Context) {
packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name")
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName))
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
sort.Slice(pds, func(i, j int) bool {
return pds[i].SemVer.LessThan(pds[j].SemVer)
})
baseURL := fmt.Sprintf("%s/%s/%s/", setting.PackageRegistryURL(ctx.Package.Owner.LowerName, "swift"), packageScope, packageName)
releases := make(map[string]Release)
for _, pd := range pds {
version := pd.SemVer.String()
releases[version] = Release{
URL: baseURL + version,
}
}
setResponseHeaders(ctx.Resp, &headers{
Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version),
})
ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{
Releases: releases,
})
}
type Resource struct {
Name string `json:"name"`
Type string `json:"type"`
Checksum string `json:"checksum"`
}
type PackageVersionMetadataResponse struct {
ID string `json:"id"`
Version string `json:"version"`
Resources []Resource `json:"resources"`
Metadata *swift_module.SoftwareSourceCode `json:"metadata"`
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-2
func PackageVersionMetadata(ctx *context.Context) {
id := buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name"))
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.PathParam("version"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
metadata := pd.Metadata.(*swift_module.Metadata)
repositoryURLs := make([]string, 0, len(pd.VersionProperties))
for _, property := range pd.VersionProperties {
if property.Name == swift_module.PropertyRepositoryURL {
repositoryURLs = append(repositoryURLs, property.Value)
}
}
var author *swift_module.Person
if metadata.Author.Name != "" || metadata.Author.GivenName != "" || metadata.Author.MiddleName != "" || metadata.Author.FamilyName != "" {
author = &swift_module.Person{
Type: "Person",
Name: metadata.Author.Name,
GivenName: metadata.Author.GivenName,
MiddleName: metadata.Author.MiddleName,
FamilyName: metadata.Author.FamilyName,
}
}
setResponseHeaders(ctx.Resp, &headers{})
ctx.JSON(http.StatusOK, PackageVersionMetadataResponse{
ID: id,
Version: pd.Version.Version,
Resources: []Resource{
{
Name: "source-archive",
Type: "application/zip",
Checksum: pd.Files[0].Blob.HashSHA256,
},
},
Metadata: &swift_module.SoftwareSourceCode{
Context: []string{"http://schema.org/"},
Type: "SoftwareSourceCode",
Name: pd.PackageProperties.GetByName(swift_module.PropertyName),
Version: pd.Version.Version,
Description: metadata.Description,
Keywords: metadata.Keywords,
CodeRepository: metadata.RepositoryURL,
License: metadata.License,
LicenseURL: metadata.LicenseURL,
Author: author,
ProgrammingLanguage: swift_module.ProgrammingLanguage{
Type: "ComputerLanguage",
Name: "Swift",
URL: "https://swift.org",
},
RepositoryURLs: repositoryURLs,
},
})
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#43-fetch-manifest-for-a-package-release
func DownloadManifest(ctx *context.Context) {
packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name")
packageVersion := ctx.PathParam("version")
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName), packageVersion)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
swiftVersion := ctx.FormTrim("swift-version")
if swiftVersion != "" {
v, err := version.NewVersion(swiftVersion)
if err == nil {
swiftVersion = swift_module.TrimmedVersionString(v)
}
}
m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion]
if !ok {
setResponseHeaders(ctx.Resp, &headers{
Status: http.StatusSeeOther,
Location: fmt.Sprintf("%s/%s/%s/%s/Package.swift", setting.PackageRegistryURL(ctx.Package.Owner.LowerName, "swift"), packageScope, packageName, packageVersion),
})
return
}
setResponseHeaders(ctx.Resp, &headers{})
filename := "Package.swift"
if swiftVersion != "" {
filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion)
}
ctx.ServeContent(strings.NewReader(m.Content), context.ServeHeaderOptions{
ContentType: "text/x-swift",
Filename: filename,
LastModified: pv.CreatedUnix.AsLocalTime(),
})
}
// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
multipartFile, _, err := ctx.Req.FormFile(formKey)
if err != nil && !errors.Is(err, http.ErrMissingFile) {
return nil, err
}
if multipartFile != nil {
return multipartFile, nil
}
content := ctx.Req.FormValue(formKey)
if content == "" {
return nil, nil //nolint:nilnil // return nil to indicate that the content does not exist
}
return io.NopCloser(strings.NewReader(content)), nil
}
// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name")
v, err := version.NewVersion(ctx.PathParam("version"))
if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
packageVersion := v.Core().String()
file, err := formFileOptionalReadCloser(ctx, "source-archive")
if file == nil || err != nil {
apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
return
}
defer file.Close()
buf, err := packages_module.CreateHashedBufferFromReader(file)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
mr, err := formFileOptionalReadCloser(ctx, "metadata")
if err != nil {
apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
return
}
if mr != nil {
defer mr.Close()
}
pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pv, _, err := packages_service.CreatePackageAndAddFile(
ctx,
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeSwift,
Name: buildPackageID(packageScope, packageName),
Version: packageVersion,
},
SemverCompatible: true,
Creator: ctx.Doer,
Metadata: pck.Metadata,
PackageProperties: map[string]string{
swift_module.PropertyScope: packageScope,
swift_module.PropertyName: packageName,
},
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s.zip", packageName, packageVersion),
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
},
)
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusConflict, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
for _, url := range pck.RepositoryURLs {
_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url)
if err != nil {
log.Error("InsertProperty failed: %v", err)
}
}
setResponseHeaders(ctx.Resp, &headers{})
ctx.Status(http.StatusCreated)
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-4
func DownloadPackageFile(ctx *context.Context) {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name")), ctx.PathParam("version"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pf := pd.Files[0].File
s, u, _, err := packages_service.OpenFileForDownload(ctx, pf, ctx.Req.Method)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
setResponseHeaders(ctx.Resp, &headers{
Digest: pd.Files[0].Blob.HashSHA256,
})
helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{
Filename: pf.Name,
ContentType: "application/zip",
LastModified: pf.CreatedUnix.AsLocalTime(),
})
}
type LookupPackageIdentifiersResponse struct {
Identifiers []string `json:"identifiers"`
}
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-5
func LookupPackageIdentifiers(ctx *context.Context) {
url := ctx.FormTrim("url")
if url == "" {
apiError(ctx, http.StatusBadRequest, nil)
return
}
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeSwift,
Properties: map[string]string{
swift_module.PropertyRepositoryURL: url,
},
IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
identifiers := make([]string, 0, len(pds))
for _, pd := range pds {
identifiers = append(identifiers, pd.Package.Name)
}
setResponseHeaders(ctx.Resp, &headers{})
ctx.JSON(http.StatusOK, LookupPackageIdentifiersResponse{
Identifiers: identifiers,
})
}