From c709fa17a77eae391cafbe72d6b2594f74d86a60 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Mon, 13 Mar 2023 21:28:39 +0100
Subject: [PATCH] Add Swift package registry (#22404)

This PR adds a [Swift](https://www.swift.org/) package registry.


![grafik](https://user-images.githubusercontent.com/1666336/211842523-07521cbd-8fb6-400f-820c-ee8048b05ae8.png)
---
 custom/conf/app.example.ini                   |   2 +
 .../doc/advanced/config-cheat-sheet.en-us.md  |   1 +
 docs/content/doc/packages/overview.en-us.md   |   1 +
 docs/content/doc/packages/swift.en-us.md      |  93 ++++
 models/packages/descriptor.go                 |   3 +
 models/packages/package.go                    |   6 +
 modules/packages/swift/metadata.go            | 214 ++++++++
 modules/packages/swift/metadata_test.go       | 144 ++++++
 modules/setting/packages.go                   |   2 +
 options/locale/locale_en-US.ini               |   4 +
 public/img/svg/gitea-swift.svg                |   1 +
 routers/api/packages/api.go                   |  36 ++
 routers/api/packages/swift/swift.go           | 464 ++++++++++++++++++
 routers/api/v1/packages/package.go            |   2 +-
 services/forms/package_form.go                |   2 +-
 services/packages/packages.go                 |   2 +
 templates/package/content/swift.tmpl          |  40 ++
 templates/package/metadata/swift.tmpl         |   4 +
 templates/package/view.tmpl                   |   2 +
 templates/swagger/v1_json.tmpl                |   1 +
 tests/integration/api_packages_swift_test.go  | 326 ++++++++++++
 web_src/svg/gitea-swift.svg                   |   5 +
 22 files changed, 1353 insertions(+), 2 deletions(-)
 create mode 100644 docs/content/doc/packages/swift.en-us.md
 create mode 100644 modules/packages/swift/metadata.go
 create mode 100644 modules/packages/swift/metadata_test.go
 create mode 100644 public/img/svg/gitea-swift.svg
 create mode 100644 routers/api/packages/swift/swift.go
 create mode 100644 templates/package/content/swift.tmpl
 create mode 100644 templates/package/metadata/swift.tmpl
 create mode 100644 tests/integration/api_packages_swift_test.go
 create mode 100644 web_src/svg/gitea-swift.svg

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index b2b5af0af..e53ed7ad9 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -2516,6 +2516,8 @@ ROUTER = console
 ;LIMIT_SIZE_PYPI = -1
 ;; Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 ;LIMIT_SIZE_RUBYGEMS = -1
+;; Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_SWIFT = -1
 ;; Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 ;LIMIT_SIZE_VAGRANT = -1
 
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index b67d6cdf5..4b9c519cd 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -1254,6 +1254,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
 - `LIMIT_SIZE_PUB`: **-1**: Maximum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_PYPI`: **-1**: Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_RUBYGEMS`: **-1**: Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_SWIFT`: **-1**: Maximum size of a Swift upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 - `LIMIT_SIZE_VAGRANT`: **-1**: Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
 
 ## Mirror (`mirror`)
diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md
index f93fec639..08da8ced4 100644
--- a/docs/content/doc/packages/overview.en-us.md
+++ b/docs/content/doc/packages/overview.en-us.md
@@ -40,6 +40,7 @@ The following package managers are currently supported:
 | [Pub]({{< relref "doc/packages/pub.en-us.md" >}}) | Dart | `dart`, `flutter` |
 | [PyPI]({{< relref "doc/packages/pypi.en-us.md" >}}) | Python | `pip`, `twine` |
 | [RubyGems]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Ruby | `gem`, `Bundler` |
+| [Swift]({{< relref "doc/packages/rubygems.en-us.md" >}}) | Swift | `swift` |
 | [Vagrant]({{< relref "doc/packages/vagrant.en-us.md" >}}) | - | `vagrant` |
 
 **The following paragraphs only apply if Packages are not globally disabled!**
diff --git a/docs/content/doc/packages/swift.en-us.md b/docs/content/doc/packages/swift.en-us.md
new file mode 100644
index 000000000..61a4c9a55
--- /dev/null
+++ b/docs/content/doc/packages/swift.en-us.md
@@ -0,0 +1,93 @@
+---
+date: "2023-01-10T00:00:00+00:00"
+title: "Swift Packages Repository"
+slug: "packages/swift"
+draft: false
+toc: false
+menu:
+  sidebar:
+    parent: "packages"
+    name: "Swift"
+    weight: 95
+    identifier: "swift"
+---
+
+# Swift Packages Repository
+
+Publish [Swift](hhttps://www.swift.org/) packages for your user or organization.
+
+**Table of Contents**
+
+{{< toc >}}
+
+## Requirements
+
+To work with the Swift package registry, you need to use [swift](https://www.swift.org/getting-started/) to consume and a HTTP client (like `curl`) to publish packages.
+
+## Configuring the package registry
+
+To register the package registry and provide credentials, execute:
+
+```shell
+swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password}
+```
+
+| Placeholder  | Description |
+| ------------ | ----------- |
+| `owner`      | The owner of the package. |
+| `username`   | Your Gitea username. |
+| `password`   | Your Gitea password. If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password. |
+
+The login is optional and only needed if the package registry is private.
+
+## Publish a package
+
+First you have to pack the contents of your package:
+
+```shell
+swift package archive-source
+```
+
+To publish the package perform a HTTP PUT request with the package content in the request body.
+
+```shell --user your_username:your_password_or_token \
+curl -X PUT --user {username}:{password} \
+	 -H "Accept: application/vnd.swift.registry.v1+json" \
+	 -F source-archive=@/path/to/package.zip \
+	 -F metadata={metadata} \
+	 https://gitea.example.com/api/packages/{owner}/swift/{scope}/{name}/{version}
+```
+
+| Placeholder | Description |
+| ----------- | ----------- |
+| `username`  | Your Gitea username. |
+| `password`  | Your Gitea password. If you are using 2FA or OAuth use a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}) instead of the password. |
+| `owner`     | The owner of the package. |
+| `scope`     | The package scope. |
+| `name`      | The package name. |
+| `version`   | The package version. |
+| `metadata`  | (Optional) The metadata of the package. JSON encoded subset of https://schema.org/SoftwareSourceCode |
+
+You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
+
+## Install a package
+
+To install a Swift package from the package registry, add it in the `Package.swift` file dependencies list:
+
+```
+dependencies: [
+	.package(id: "{scope}.{name}", from:"{version}")
+]
+```
+
+| Parameter   | Description |
+| ----------- | ----------- |
+| `scope`     | The package scope. |
+| `name`      | The package name. |
+| `version`   | The package version. |
+
+Afterwards execute the following command to install it:
+
+```shell
+swift package resolve
+```
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index f4be21e74..06699b5d5 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/packages/pub"
 	"code.gitea.io/gitea/modules/packages/pypi"
 	"code.gitea.io/gitea/modules/packages/rubygems"
+	"code.gitea.io/gitea/modules/packages/swift"
 	"code.gitea.io/gitea/modules/packages/vagrant"
 
 	"github.com/hashicorp/go-version"
@@ -159,6 +160,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
 		metadata = &pypi.Metadata{}
 	case TypeRubyGems:
 		metadata = &rubygems.Metadata{}
+	case TypeSwift:
+		metadata = &swift.Metadata{}
 	case TypeVagrant:
 		metadata = &vagrant.Metadata{}
 	default:
diff --git a/models/packages/package.go b/models/packages/package.go
index 32f30fab9..ccc9257c3 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -44,6 +44,7 @@ const (
 	TypePub       Type = "pub"
 	TypePyPI      Type = "pypi"
 	TypeRubyGems  Type = "rubygems"
+	TypeSwift     Type = "swift"
 	TypeVagrant   Type = "vagrant"
 )
 
@@ -62,6 +63,7 @@ var TypeList = []Type{
 	TypePub,
 	TypePyPI,
 	TypeRubyGems,
+	TypeSwift,
 	TypeVagrant,
 }
 
@@ -96,6 +98,8 @@ func (pt Type) Name() string {
 		return "PyPI"
 	case TypeRubyGems:
 		return "RubyGems"
+	case TypeSwift:
+		return "Swift"
 	case TypeVagrant:
 		return "Vagrant"
 	}
@@ -133,6 +137,8 @@ func (pt Type) SVGName() string {
 		return "gitea-python"
 	case TypeRubyGems:
 		return "gitea-rubygems"
+	case TypeSwift:
+		return "gitea-swift"
 	case TypeVagrant:
 		return "gitea-vagrant"
 	}
diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go
new file mode 100644
index 000000000..24c4262ab
--- /dev/null
+++ b/modules/packages/swift/metadata.go
@@ -0,0 +1,214 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swift
+
+import (
+	"archive/zip"
+	"fmt"
+	"io"
+	"path"
+	"regexp"
+	"strings"
+
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/validation"
+
+	"github.com/hashicorp/go-version"
+)
+
+var (
+	ErrMissingManifestFile    = util.NewInvalidArgumentErrorf("Package.swift file is missing")
+	ErrManifestFileTooLarge   = util.NewInvalidArgumentErrorf("Package.swift file is too large")
+	ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
+
+	manifestPattern     = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
+	toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
+)
+
+const (
+	maxManifestFileSize = 128 * 1024
+
+	PropertyScope         = "swift.scope"
+	PropertyName          = "swift.name"
+	PropertyRepositoryURL = "swift.repository_url"
+)
+
+// Package represents a Swift package
+type Package struct {
+	RepositoryURLs []string
+	Metadata       *Metadata
+}
+
+// Metadata represents the metadata of a Swift package
+type Metadata struct {
+	Description   string               `json:"description,omitempty"`
+	Keywords      []string             `json:"keywords,omitempty"`
+	RepositoryURL string               `json:"repository_url,omitempty"`
+	License       string               `json:"license,omitempty"`
+	Author        Person               `json:"author,omitempty"`
+	Manifests     map[string]*Manifest `json:"manifests,omitempty"`
+}
+
+// Manifest represents a Package.swift file
+type Manifest struct {
+	Content      string `json:"content"`
+	ToolsVersion string `json:"tools_version,omitempty"`
+}
+
+// https://schema.org/SoftwareSourceCode
+type SoftwareSourceCode struct {
+	Context             []string            `json:"@context"`
+	Type                string              `json:"@type"`
+	Name                string              `json:"name"`
+	Version             string              `json:"version"`
+	Description         string              `json:"description,omitempty"`
+	Keywords            []string            `json:"keywords,omitempty"`
+	CodeRepository      string              `json:"codeRepository,omitempty"`
+	License             string              `json:"license,omitempty"`
+	Author              Person              `json:"author"`
+	ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
+	RepositoryURLs      []string            `json:"repositoryURLs,omitempty"`
+}
+
+// https://schema.org/ProgrammingLanguage
+type ProgrammingLanguage struct {
+	Type string `json:"@type"`
+	Name string `json:"name"`
+	URL  string `json:"url"`
+}
+
+// https://schema.org/Person
+type Person struct {
+	Type       string `json:"@type,omitempty"`
+	GivenName  string `json:"givenName,omitempty"`
+	MiddleName string `json:"middleName,omitempty"`
+	FamilyName string `json:"familyName,omitempty"`
+}
+
+func (p Person) String() string {
+	var sb strings.Builder
+	if p.GivenName != "" {
+		sb.WriteString(p.GivenName)
+	}
+	if p.MiddleName != "" {
+		if sb.Len() > 0 {
+			sb.WriteRune(' ')
+		}
+		sb.WriteString(p.MiddleName)
+	}
+	if p.FamilyName != "" {
+		if sb.Len() > 0 {
+			sb.WriteRune(' ')
+		}
+		sb.WriteString(p.FamilyName)
+	}
+	return sb.String()
+}
+
+// ParsePackage parses the Swift package upload
+func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
+	zr, err := zip.NewReader(sr, size)
+	if err != nil {
+		return nil, err
+	}
+
+	p := &Package{
+		Metadata: &Metadata{
+			Manifests: make(map[string]*Manifest),
+		},
+	}
+
+	for _, file := range zr.File {
+		manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
+		if len(manifestMatch) == 0 {
+			continue
+		}
+
+		if file.UncompressedSize64 > maxManifestFileSize {
+			return nil, ErrManifestFileTooLarge
+		}
+
+		f, err := zr.Open(file.Name)
+		if err != nil {
+			return nil, err
+		}
+
+		content, err := io.ReadAll(f)
+
+		if err := f.Close(); err != nil {
+			return nil, err
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		swiftVersion := ""
+		if len(manifestMatch) == 2 && manifestMatch[1] != "" {
+			v, err := version.NewSemver(manifestMatch[1])
+			if err != nil {
+				return nil, ErrInvalidManifestVersion
+			}
+			swiftVersion = TrimmedVersionString(v)
+		}
+
+		manifest := &Manifest{
+			Content: string(content),
+		}
+
+		toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content)
+		if len(toolsMatch) == 2 {
+			v, err := version.NewSemver(toolsMatch[1])
+			if err != nil {
+				return nil, ErrInvalidManifestVersion
+			}
+
+			manifest.ToolsVersion = TrimmedVersionString(v)
+		}
+
+		p.Metadata.Manifests[swiftVersion] = manifest
+	}
+
+	if _, found := p.Metadata.Manifests[""]; !found {
+		return nil, ErrMissingManifestFile
+	}
+
+	if mr != nil {
+		var ssc *SoftwareSourceCode
+		if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
+			return nil, err
+		}
+
+		p.Metadata.Description = ssc.Description
+		p.Metadata.Keywords = ssc.Keywords
+		p.Metadata.License = ssc.License
+		p.Metadata.Author = Person{
+			GivenName:  ssc.Author.GivenName,
+			MiddleName: ssc.Author.MiddleName,
+			FamilyName: ssc.Author.FamilyName,
+		}
+
+		p.Metadata.RepositoryURL = ssc.CodeRepository
+		if !validation.IsValidURL(p.Metadata.RepositoryURL) {
+			p.Metadata.RepositoryURL = ""
+		}
+
+		p.RepositoryURLs = ssc.RepositoryURLs
+	}
+
+	return p, nil
+}
+
+// TrimmedVersionString returns the version string without the patch segment if it is zero
+func TrimmedVersionString(v *version.Version) string {
+	segments := v.Segments64()
+
+	var b strings.Builder
+	fmt.Fprintf(&b, "%d.%d", segments[0], segments[1])
+	if segments[2] != 0 {
+		fmt.Fprintf(&b, ".%d", segments[2])
+	}
+	return b.String()
+}
diff --git a/modules/packages/swift/metadata_test.go b/modules/packages/swift/metadata_test.go
new file mode 100644
index 000000000..3913c2355
--- /dev/null
+++ b/modules/packages/swift/metadata_test.go
@@ -0,0 +1,144 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swift
+
+import (
+	"archive/zip"
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/go-version"
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	packageName          = "gitea"
+	packageVersion       = "1.0.1"
+	packageDescription   = "Package Description"
+	packageRepositoryURL = "https://gitea.io/gitea/gitea"
+	packageAuthor        = "KN4CK3R"
+	packageLicense       = "MIT"
+)
+
+func TestParsePackage(t *testing.T) {
+	createArchive := func(files map[string][]byte) *bytes.Reader {
+		var buf bytes.Buffer
+		zw := zip.NewWriter(&buf)
+		for filename, content := range files {
+			w, _ := zw.Create(filename)
+			w.Write(content)
+		}
+		zw.Close()
+		return bytes.NewReader(buf.Bytes())
+	}
+
+	t.Run("MissingManifestFile", func(t *testing.T) {
+		data := createArchive(map[string][]byte{"dummy.txt": {}})
+
+		p, err := ParsePackage(data, data.Size(), nil)
+		assert.Nil(t, p)
+		assert.ErrorIs(t, err, ErrMissingManifestFile)
+	})
+
+	t.Run("ManifestFileTooLarge", func(t *testing.T) {
+		data := createArchive(map[string][]byte{
+			"Package.swift": make([]byte, maxManifestFileSize+1),
+		})
+
+		p, err := ParsePackage(data, data.Size(), nil)
+		assert.Nil(t, p)
+		assert.ErrorIs(t, err, ErrManifestFileTooLarge)
+	})
+
+	t.Run("WithoutMetadata", func(t *testing.T) {
+		content1 := "// swift-tools-version:5.7\n//\n//  Package.swift"
+		content2 := "// swift-tools-version:5.6\n//\n//  Package@swift-5.6.swift"
+
+		data := createArchive(map[string][]byte{
+			"Package.swift":           []byte(content1),
+			"Package@swift-5.5.swift": []byte(content2),
+		})
+
+		p, err := ParsePackage(data, data.Size(), nil)
+		assert.NotNil(t, p)
+		assert.NoError(t, err)
+
+		assert.NotNil(t, p.Metadata)
+		assert.Empty(t, p.RepositoryURLs)
+		assert.Len(t, p.Metadata.Manifests, 2)
+		m := p.Metadata.Manifests[""]
+		assert.Equal(t, "5.7", m.ToolsVersion)
+		assert.Equal(t, content1, m.Content)
+		m = p.Metadata.Manifests["5.5"]
+		assert.Equal(t, "5.6", m.ToolsVersion)
+		assert.Equal(t, content2, m.Content)
+	})
+
+	t.Run("WithMetadata", func(t *testing.T) {
+		data := createArchive(map[string][]byte{
+			"Package.swift": []byte("// swift-tools-version:5.7\n//\n//  Package.swift"),
+		})
+
+		p, err := ParsePackage(
+			data,
+			data.Size(),
+			strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
+		)
+		assert.NotNil(t, p)
+		assert.NoError(t, err)
+
+		assert.NotNil(t, p.Metadata)
+		assert.Len(t, p.Metadata.Manifests, 1)
+		m := p.Metadata.Manifests[""]
+		assert.Equal(t, "5.7", m.ToolsVersion)
+
+		assert.Equal(t, packageDescription, p.Metadata.Description)
+		assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
+		assert.Equal(t, packageLicense, p.Metadata.License)
+		assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
+		assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
+		assert.ElementsMatch(t, []string{packageRepositoryURL}, p.RepositoryURLs)
+	})
+}
+
+func TestTrimmedVersionString(t *testing.T) {
+	cases := []struct {
+		Version  *version.Version
+		Expected string
+	}{
+		{
+			Version:  version.Must(version.NewVersion("1")),
+			Expected: "1.0",
+		},
+		{
+			Version:  version.Must(version.NewVersion("1.0")),
+			Expected: "1.0",
+		},
+		{
+			Version:  version.Must(version.NewVersion("1.0.0")),
+			Expected: "1.0",
+		},
+		{
+			Version:  version.Must(version.NewVersion("1.0.1")),
+			Expected: "1.0.1",
+		},
+		{
+			Version:  version.Must(version.NewVersion("1.0+meta")),
+			Expected: "1.0",
+		},
+		{
+			Version:  version.Must(version.NewVersion("1.0.0+meta")),
+			Expected: "1.0",
+		},
+		{
+			Version:  version.Must(version.NewVersion("1.0.1+meta")),
+			Expected: "1.0.1",
+		},
+	}
+
+	for _, c := range cases {
+		assert.Equal(t, c.Expected, TrimmedVersionString(c.Version))
+	}
+}
diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index 13599e5a6..ac0ad62bc 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -39,6 +39,7 @@ var (
 		LimitSizePub         int64
 		LimitSizePyPI        int64
 		LimitSizeRubyGems    int64
+		LimitSizeSwift       int64
 		LimitSizeVagrant     int64
 	}{
 		Enabled:              true,
@@ -81,6 +82,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
 	Packages.LimitSizePub = mustBytes(sec, "LIMIT_SIZE_PUB")
 	Packages.LimitSizePyPI = mustBytes(sec, "LIMIT_SIZE_PYPI")
 	Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
+	Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
 	Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 677af1397..e793c3ef0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3239,6 +3239,10 @@ rubygems.dependencies.development = Development Dependencies
 rubygems.required.ruby = Requires Ruby version
 rubygems.required.rubygems = Requires RubyGem version
 rubygems.documentation = For more information on the RubyGems registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/rubygems/">the documentation</a>.
+swift.registry = Setup this registry from the command line:
+swift.install = Add the package in your <code>Package.swift</code> file:
+swift.install2 = and run the following command:
+swift.documentation = For more information on the Swift registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/swift/">the documentation</a>.
 vagrant.install = To add a Vagrant box, run the following command:
 vagrant.documentation = For more information on the Vagrant registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/vagrant/">the documentation</a>.
 settings.link = Link this package to a repository
diff --git a/public/img/svg/gitea-swift.svg b/public/img/svg/gitea-swift.svg
new file mode 100644
index 000000000..ebfea951d
--- /dev/null
+++ b/public/img/svg/gitea-swift.svg
@@ -0,0 +1 @@
+<svg xml:space="preserve" viewBox="0 0 59.5 59.5" class="svg gitea-swift" width="16" height="16" aria-hidden="true"><path fill="#F05138" d="M59.387 16.45a82.463 82.463 0 0 0-.027-1.792c-.034-1.301-.111-2.614-.343-3.9-.234-1.308-.618-2.523-1.222-3.71a12.464 12.464 0 0 0-5.452-5.452C51.156.992 49.94.609 48.635.374c-1.287-.232-2.6-.308-3.902-.343a85.714 85.714 0 0 0-1.792-.027C42.231 0 41.522 0 40.813 0H18.578c-.71 0-1.418 0-2.127.004-.598.004-1.196.01-1.793.027-.325.008-.651.02-.978.036-.978.047-1.959.133-2.924.307-.98.176-1.908.436-2.811.81A12.503 12.503 0 0 0 3.89 3.89a12.46 12.46 0 0 0-2.294 3.158C.992 8.235.61 9.45.374 10.758c-.231 1.286-.308 2.599-.343 3.9a85.767 85.767 0 0 0-.027 1.792C-.001 17.16 0 17.869 0 18.578v22.234c0 .71 0 1.419.004 2.129.004.597.01 1.194.027 1.79.035 1.302.112 2.615.343 3.902.235 1.306.618 2.522 1.222 3.71a12.457 12.457 0 0 0 5.453 5.453c1.186.603 2.401.986 3.707 1.22 1.287.232 2.6.309 3.902.344.597.016 1.195.023 1.793.026.709.005 1.418.004 2.127.004h22.235c.71 0 1.419.001 2.128-.004.598-.003 1.195-.01 1.792-.026 1.302-.035 2.615-.112 3.902-.344 1.306-.234 2.521-.617 3.708-1.221a12.461 12.461 0 0 0 5.452-5.453c.604-1.187.988-2.403 1.222-3.71.232-1.286.309-2.599.343-3.9.017-.597.023-1.194.027-1.791.005-.71.004-1.42.004-2.129V18.578c0-.71 0-1.419-.004-2.128z"/><path fill="#fff" d="m47.061 36.661-.004-.005c.066-.223.133-.446.19-.675 2.466-9.82-3.55-21.432-13.731-27.545 4.461 6.048 6.434 13.373 4.681 19.78-.156.572-.344 1.12-.552 1.653-.225-.148-.51-.316-.89-.526 0 0-10.128-6.253-21.104-17.313-.288-.29 5.853 8.777 12.822 16.14-3.283-1.842-12.434-8.5-18.227-13.802.712 1.187 1.559 2.33 2.49 3.43 4.837 6.136 11.145 13.705 18.703 19.518-5.31 3.25-12.814 3.502-20.285.003a30.646 30.646 0 0 1-5.193-3.098c3.162 5.058 8.033 9.423 13.96 11.97 7.07 3.039 14.1 2.833 19.337.05l-.004.007.079-.047c.215-.116.428-.233.637-.358 2.516-1.306 7.485-2.63 10.152 2.559.653 1.27 2.041-5.46-3.062-11.739z"/></svg>
\ No newline at end of file
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 0e3d8b7a0..c0c7b117f 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -28,6 +28,7 @@ import (
 	"code.gitea.io/gitea/routers/api/packages/pub"
 	"code.gitea.io/gitea/routers/api/packages/pypi"
 	"code.gitea.io/gitea/routers/api/packages/rubygems"
+	"code.gitea.io/gitea/routers/api/packages/swift"
 	"code.gitea.io/gitea/routers/api/packages/vagrant"
 	"code.gitea.io/gitea/services/auth"
 	context_service "code.gitea.io/gitea/services/context"
@@ -375,6 +376,41 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
 				r.Delete("/yank", rubygems.DeletePackage)
 			}, reqPackageAccess(perm.AccessModeWrite))
 		}, reqPackageAccess(perm.AccessModeRead))
+		r.Group("/swift", func() {
+			r.Group("/{scope}/{name}", func() {
+				r.Group("", func() {
+					r.Get("", swift.EnumeratePackageVersions)
+					r.Get(".json", swift.EnumeratePackageVersions)
+				}, swift.CheckAcceptMediaType(swift.AcceptJSON))
+				r.Group("/{version}", func() {
+					r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
+					r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
+					r.Get("", func(ctx *context.Context) {
+						// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
+
+						version := ctx.Params("version")
+						if strings.HasSuffix(version, ".zip") {
+							swift.CheckAcceptMediaType(swift.AcceptZip)(ctx)
+							if ctx.Written() {
+								return
+							}
+							ctx.SetParams("version", version[:len(version)-4])
+							swift.DownloadPackageFile(ctx)
+						} else {
+							swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
+							if ctx.Written() {
+								return
+							}
+							if strings.HasSuffix(version, ".json") {
+								ctx.SetParams("version", version[:len(version)-5])
+							}
+							swift.PackageVersionMetadata(ctx)
+						}
+					})
+				})
+			})
+			r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/vagrant", func() {
 			r.Group("/authenticate", func() {
 				r.Get("", vagrant.CheckAuthenticate)
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
new file mode 100644
index 000000000..f78f70377
--- /dev/null
+++ b/routers/api/packages/swift/swift.go
@@ -0,0 +1,464 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swift
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"regexp"
+	"sort"
+	"strings"
+
+	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	packages_module "code.gitea.io/gitea/modules/packages"
+	swift_module "code.gitea.io/gitea/modules/packages/swift"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/routers/api/packages/helper"
+	packages_service "code.gitea.io/gitea/services/packages"
+
+	"github.com/hashicorp/go-version"
+)
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/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/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope
+	scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
+	// https://github.com/apple/swift-package-manager/blob/main/Documentation/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/apple/swift-package-manager/blob/main/Documentation/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/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling
+func apiError(ctx *context.Context, status int, obj interface{}) {
+	// https://www.rfc-editor.org/rfc/rfc7807
+	type Problem struct {
+		Status int    `json:"status"`
+		Detail string `json:"detail"`
+	}
+
+	helper.LogAndProcessError(ctx, status, obj, func(message string) {
+		setResponseHeaders(ctx.Resp, &headers{
+			Status:      status,
+			ContentType: "application/problem+json",
+		})
+		if err := json.NewEncoder(ctx.Resp).Encode(Problem{
+			Status: status,
+			Detail: message,
+		}); err != nil {
+			log.Error("JSON encode: %v", err)
+		}
+	})
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/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))
+		}
+	}
+}
+
+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/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases
+func EnumeratePackageVersions(ctx *context.Context) {
+	packageScope := ctx.Params("scope")
+	packageName := ctx.Params("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("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, 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:"id"`
+	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/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2
+func PackageVersionMetadata(ctx *context.Context) {
+	id := buildPackageID(ctx.Params("scope"), ctx.Params("name"))
+
+	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.Params("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)
+
+	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,
+			ProgrammingLanguage: swift_module.ProgrammingLanguage{
+				Type: "ComputerLanguage",
+				Name: "Swift",
+				URL:  "https://swift.org",
+			},
+			Author: swift_module.Person{
+				Type:       "Person",
+				GivenName:  metadata.Author.GivenName,
+				MiddleName: metadata.Author.MiddleName,
+				FamilyName: metadata.Author.FamilyName,
+			},
+		},
+	})
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release
+func DownloadManifest(ctx *context.Context) {
+	packageScope := ctx.Params("scope")
+	packageName := ctx.Params("name")
+	packageVersion := ctx.Params("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("%sapi/packages/%s/swift/%s/%s/%s/Package.swift", setting.AppURL, ctx.Package.Owner.LowerName, 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(),
+	})
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6
+func UploadPackageFile(ctx *context.Context) {
+	packageScope := ctx.Params("scope")
+	packageName := ctx.Params("name")
+
+	v, err := version.NewVersion(ctx.Params("version"))
+
+	if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
+		apiError(ctx, http.StatusBadRequest, err)
+		return
+	}
+
+	packageVersion := v.Core().String()
+
+	file, _, err := ctx.Req.FormFile("source-archive")
+	if err != nil {
+		apiError(ctx, http.StatusBadRequest, err)
+		return
+	}
+	defer file.Close()
+
+	buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer buf.Close()
+
+	var mr io.Reader
+	metadata := ctx.Req.FormValue("metadata")
+	if metadata != "" {
+		mr = strings.NewReader(metadata)
+	}
+
+	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(
+		&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/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4
+func DownloadPackageFile(ctx *context.Context) {
+	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.Params("scope"), ctx.Params("name")), ctx.Params("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, _, err := packages_service.GetPackageFileStream(ctx, pf)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer s.Close()
+
+	setResponseHeaders(ctx.Resp, &headers{
+		Digest: pd.Files[0].Blob.HashSHA256,
+	})
+
+	ctx.ServeContent(s, &context.ServeHeaderOptions{
+		Filename:     pf.Name,
+		ContentType:  "application/zip",
+		LastModified: pf.CreatedUnix.AsLocalTime(),
+	})
+}
+
+type LookupPackageIdentifiersResponse struct {
+	Identifiers []string `json:"identifiers"`
+}
+
+// https://github.com/apple/swift-package-manager/blob/main/Documentation/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: util.OptionalBoolFalse,
+	})
+	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,
+	})
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index ab077090d..200dc5aaf 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
 	//   in: query
 	//   description: package type filter
 	//   type: string
-	//   enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
+	//   enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, swift, vagrant]
 	// - name: q
 	//   in: query
 	//   description: name filter
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index b22ed47c7..699d0fe44 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -15,7 +15,7 @@ import (
 type PackageCleanupRuleForm struct {
 	ID            int64
 	Enabled       bool
-	Type          string `binding:"Required;In(cargo,chef,composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
+	Type          string `binding:"Required;In(cargo,chef,composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,swift,vagrant)"`
 	KeepCount     int    `binding:"In(0,1,5,10,25,50,100)"`
 	KeepPattern   string `binding:"RegexPattern"`
 	RemoveDays    int    `binding:"In(0,7,14,30,60,90,180)"`
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 3abca7337..dd5c63470 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -361,6 +361,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
 		typeSpecificSize = setting.Packages.LimitSizePyPI
 	case packages_model.TypeRubyGems:
 		typeSpecificSize = setting.Packages.LimitSizeRubyGems
+	case packages_model.TypeSwift:
+		typeSpecificSize = setting.Packages.LimitSizeSwift
 	case packages_model.TypeVagrant:
 		typeSpecificSize = setting.Packages.LimitSizeVagrant
 	}
diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl
new file mode 100644
index 000000000..3ff06483b
--- /dev/null
+++ b/templates/package/content/swift.tmpl
@@ -0,0 +1,40 @@
+{{if eq .PackageDescriptor.Package.Type "swift"}}
+	<h4 class="ui top attached header">{{.locale.Tr "packages.installation"}}</h4>
+	<div class="ui attached segment">
+		<div class="ui form">
+			<div class="field">
+				<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.swift.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>swift package-registry set <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></gitea-origin-url></code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{svg "octicon-code"}} {{.locale.Tr "packages.swift.install" | Safe}}</label>
+				<div class="markup"><pre class="code-block"><code>dependencies: [
+	.package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}")
+]</code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.swift.install2"}}</label>
+				<div class="markup"><pre class="code-block"><code>swift package resolve</code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{.locale.Tr "packages.swift.documentation" | Safe}}</label>
+			</div>
+		</div>
+	</div>
+
+	{{if .PackageDescriptor.Metadata.Description}}
+		<h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4>
+		<div class="ui attached segment">
+			{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
+		</div>
+	{{end}}
+
+	{{if .PackageDescriptor.Metadata.Keywords}}
+		<h4 class="ui top attached header">{{.locale.Tr "packages.keywords"}}</h4>
+		<div class="ui attached segment">
+			{{range .PackageDescriptor.Metadata.Keywords}}
+				{{.}}
+			{{end}}
+		</div>
+	{{end}}
+{{end}}
diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl
new file mode 100644
index 000000000..8a9ab071f
--- /dev/null
+++ b/templates/package/metadata/swift.tmpl
@@ -0,0 +1,4 @@
+{{if eq .PackageDescriptor.Package.Type "swift"}}
+	{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{.locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+{{end}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 839d9cf21..b2a2fb1e5 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -33,6 +33,7 @@
 					{{template "package/content/pub" .}}
 					{{template "package/content/pypi" .}}
 					{{template "package/content/rubygems" .}}
+					{{template "package/content/swift" .}}
 					{{template "package/content/vagrant" .}}
 				</div>
 				<div class="four wide column">
@@ -59,6 +60,7 @@
 							{{template "package/metadata/pub" .}}
 							{{template "package/metadata/pypi" .}}
 							{{template "package/metadata/rubygems" .}}
+							{{template "package/metadata/swift" .}}
 							{{template "package/metadata/vagrant" .}}
 							<div class="item">{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
 						</div>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index b64f6dcd8..9c46b25ea 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -2120,6 +2120,7 @@
               "pub",
               "pypi",
               "rubygems",
+              "swift",
               "vagrant"
             ],
             "type": "string",
diff --git a/tests/integration/api_packages_swift_test.go b/tests/integration/api_packages_swift_test.go
new file mode 100644
index 000000000..a3035ea60
--- /dev/null
+++ b/tests/integration/api_packages_swift_test.go
@@ -0,0 +1,326 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"archive/zip"
+	"bytes"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	swift_module "code.gitea.io/gitea/modules/packages/swift"
+	"code.gitea.io/gitea/modules/setting"
+	swift_router "code.gitea.io/gitea/routers/api/packages/swift"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPackageSwift(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+	packageScope := "test-scope"
+	packageName := "test_package"
+	packageID := packageScope + "." + packageName
+	packageVersion := "1.0.3"
+	packageAuthor := "KN4CK3R"
+	packageDescription := "Gitea Test Package"
+	packageRepositoryURL := "https://gitea.io/gitea/gitea"
+	contentManifest1 := "// swift-tools-version:5.7\n//\n//  Package.swift"
+	contentManifest2 := "// swift-tools-version:5.6\n//\n//  Package@swift-5.6.swift"
+
+	url := fmt.Sprintf("/api/packages/%s/swift", user.Name)
+
+	t.Run("CheckAcceptMediaType", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		for _, sub := range []string{
+			"/scope/package",
+			"/scope/package.json",
+			"/scope/package/1.0.0",
+			"/scope/package/1.0.0.json",
+			"/scope/package/1.0.0.zip",
+			"/scope/package/1.0.0/Package.swift",
+			"/identifiers",
+		} {
+			req := NewRequest(t, "GET", url+sub)
+			req.Header.Add("Accept", "application/unknown")
+			resp := MakeRequest(t, req, http.StatusBadRequest)
+
+			assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+			assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type"))
+		}
+
+		req := NewRequestWithBody(t, "PUT", url+"/scope/package/1.0.0", strings.NewReader(""))
+		req = AddBasicAuthHeader(req, user.Name)
+		req.Header.Add("Accept", "application/unknown")
+		resp := MakeRequest(t, req, http.StatusBadRequest)
+
+		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+		assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type"))
+	})
+
+	t.Run("Upload", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) {
+			var body bytes.Buffer
+			mpw := multipart.NewWriter(&body)
+
+			part, _ := mpw.CreateFormFile("source-archive", "source-archive.zip")
+			io.Copy(part, sr)
+
+			if metadata != "" {
+				mpw.WriteField("metadata", metadata)
+			}
+
+			mpw.Close()
+
+			req := NewRequestWithBody(t, "PUT", url, &body)
+			req.Header.Add("Content-Type", mpw.FormDataContentType())
+			req.Header.Add("Accept", swift_router.AcceptJSON)
+			req = AddBasicAuthHeader(req, user.Name)
+			MakeRequest(t, req, expectedStatus)
+		}
+
+		createArchive := func(files map[string]string) *bytes.Buffer {
+			var buf bytes.Buffer
+			zw := zip.NewWriter(&buf)
+			for filename, content := range files {
+				w, _ := zw.Create(filename)
+				w.Write([]byte(content))
+			}
+			zw.Close()
+			return &buf
+		}
+
+		for _, triple := range []string{"/sc_ope/package/1.0.0", "/scope/pack~age/1.0.0", "/scope/package/1_0.0"} {
+			req := NewRequestWithBody(t, "PUT", url+triple, bytes.NewReader([]byte{}))
+			req = AddBasicAuthHeader(req, user.Name)
+			resp := MakeRequest(t, req, http.StatusBadRequest)
+
+			assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+			assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type"))
+		}
+
+		uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion)
+
+		req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
+		MakeRequest(t, req, http.StatusUnauthorized)
+
+		uploadPackage(
+			t,
+			uploadURL,
+			http.StatusCreated,
+			createArchive(map[string]string{
+				"Package.swift":           contentManifest1,
+				"Package@swift-5.6.swift": contentManifest2,
+			}),
+			`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
+		)
+
+		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
+		assert.NoError(t, err)
+		assert.Len(t, pvs, 1)
+
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+		assert.NoError(t, err)
+		assert.NotNil(t, pd.SemVer)
+		assert.Equal(t, packageID, pd.Package.Name)
+		assert.Equal(t, packageVersion, pd.Version.Version)
+		assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
+		metadata := pd.Metadata.(*swift_module.Metadata)
+		assert.Equal(t, packageDescription, metadata.Description)
+		assert.Len(t, metadata.Manifests, 2)
+		assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
+		assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
+		assert.Len(t, pd.VersionProperties, 1)
+		assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
+
+		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+		assert.NoError(t, err)
+		assert.Len(t, pfs, 1)
+		assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion), pfs[0].Name)
+		assert.True(t, pfs[0].IsLead)
+
+		uploadPackage(
+			t,
+			uploadURL,
+			http.StatusConflict,
+			createArchive(map[string]string{
+				"Package.swift": contentManifest1,
+			}),
+			"",
+		)
+	})
+
+	t.Run("Download", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.zip", url, packageScope, packageName, packageVersion))
+		req = AddBasicAuthHeader(req, user.Name)
+		req.Header.Add("Accept", swift_router.AcceptZip)
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+		assert.Equal(t, "application/zip", resp.Header().Get("Content-Type"))
+
+		pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeSwift, packageID, packageVersion)
+		assert.NotNil(t, pv)
+		assert.NoError(t, err)
+
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv)
+		assert.NoError(t, err)
+		assert.Equal(t, "sha256="+pd.Files[0].Blob.HashSHA256, resp.Header().Get("Digest"))
+	})
+
+	t.Run("EnumeratePackageVersions", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", url, packageScope, packageName))
+		req = AddBasicAuthHeader(req, user.Name)
+		req.Header.Add("Accept", swift_router.AcceptJSON)
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion)
+
+		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+		assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link"))
+
+		body := resp.Body.String()
+
+		var result *swift_router.EnumeratePackageVersionsResponse
+		DecodeJSON(t, resp, &result)
+
+		assert.Len(t, result.Releases, 1)
+		assert.Contains(t, result.Releases, packageVersion)
+		assert.Equal(t, versionURL, result.Releases[packageVersion].URL)
+
+		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName))
+		req = AddBasicAuthHeader(req, user.Name)
+		resp = MakeRequest(t, req, http.StatusOK)
+
+		assert.Equal(t, body, resp.Body.String())
+	})
+
+	t.Run("PackageVersionMetadata", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion))
+		req = AddBasicAuthHeader(req, user.Name)
+		req.Header.Add("Accept", swift_router.AcceptJSON)
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+
+		body := resp.Body.String()
+
+		var result *swift_router.PackageVersionMetadataResponse
+		DecodeJSON(t, resp, &result)
+
+		pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeSwift, packageID, packageVersion)
+		assert.NotNil(t, pv)
+		assert.NoError(t, err)
+
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv)
+		assert.NoError(t, err)
+
+		assert.Equal(t, packageID, result.ID)
+		assert.Equal(t, packageVersion, result.Version)
+		assert.Len(t, result.Resources, 1)
+		assert.Equal(t, "source-archive", result.Resources[0].Name)
+		assert.Equal(t, "application/zip", result.Resources[0].Type)
+		assert.Equal(t, pd.Files[0].Blob.HashSHA256, result.Resources[0].Checksum)
+		assert.Equal(t, "SoftwareSourceCode", result.Metadata.Type)
+		assert.Equal(t, packageName, result.Metadata.Name)
+		assert.Equal(t, packageVersion, result.Metadata.Version)
+		assert.Equal(t, packageDescription, result.Metadata.Description)
+		assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
+		assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
+
+		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion))
+		req = AddBasicAuthHeader(req, user.Name)
+		resp = MakeRequest(t, req, http.StatusOK)
+
+		assert.Equal(t, body, resp.Body.String())
+	})
+
+	t.Run("DownloadManifest", func(t *testing.T) {
+		manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion)
+
+		t.Run("Default", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			req := NewRequest(t, "GET", manifestURL)
+			req = AddBasicAuthHeader(req, user.Name)
+			req.Header.Add("Accept", swift_router.AcceptSwift)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+			assert.Equal(t, "text/x-swift", resp.Header().Get("Content-Type"))
+			assert.Equal(t, contentManifest1, resp.Body.String())
+		})
+
+		t.Run("DifferentVersion", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			req := NewRequest(t, "GET", manifestURL+"?swift-version=5.6")
+			req = AddBasicAuthHeader(req, user.Name)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+			assert.Equal(t, "text/x-swift", resp.Header().Get("Content-Type"))
+			assert.Equal(t, contentManifest2, resp.Body.String())
+
+			req = NewRequest(t, "GET", manifestURL+"?swift-version=5.6.0")
+			req = AddBasicAuthHeader(req, user.Name)
+			MakeRequest(t, req, http.StatusOK)
+		})
+
+		t.Run("Redirect", func(t *testing.T) {
+			defer tests.PrintCurrentTest(t)()
+
+			req := NewRequest(t, "GET", manifestURL+"?swift-version=1.0")
+			req = AddBasicAuthHeader(req, user.Name)
+			resp := MakeRequest(t, req, http.StatusSeeOther)
+
+			assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+			assert.Equal(t, setting.AppURL+url[1:]+fmt.Sprintf("/%s/%s/%s/Package.swift", packageScope, packageName, packageVersion), resp.Header().Get("Location"))
+		})
+	})
+
+	t.Run("LookupPackageIdentifiers", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", url+"/identifiers")
+		req.Header.Add("Accept", swift_router.AcceptJSON)
+		resp := MakeRequest(t, req, http.StatusBadRequest)
+
+		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
+		assert.Equal(t, "application/problem+json", resp.Header().Get("Content-Type"))
+
+		req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/")
+		MakeRequest(t, req, http.StatusNotFound)
+
+		req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL)
+		req.Header.Add("Accept", swift_router.AcceptJSON)
+		resp = MakeRequest(t, req, http.StatusOK)
+
+		var result *swift_router.LookupPackageIdentifiersResponse
+		DecodeJSON(t, resp, &result)
+
+		assert.Len(t, result.Identifiers, 1)
+		assert.Equal(t, packageID, result.Identifiers[0])
+	})
+}
diff --git a/web_src/svg/gitea-swift.svg b/web_src/svg/gitea-swift.svg
new file mode 100644
index 000000000..8af43d32e
--- /dev/null
+++ b/web_src/svg/gitea-swift.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 59.5 59.5" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
+	<path d="m59.387 16.45c-0.0035-0.5973-0.0101-1.1943-0.0266-1.7923-0.0348-1.3008-0.1117-2.6134-0.3429-3.9003-0.2346-1.3069-0.6181-2.5221-1.2225-3.7093-0.5933-1.1659-1.3689-2.2328-2.2941-3.158-0.925-0.9252-1.9919-1.7007-3.1583-2.2943-1.1862-0.6037-2.4016-0.9871-3.7073-1.2217-1.2876-0.2319-2.6002-0.3083-3.902-0.3435-0.5977-0.0162-1.1948-0.023-1.7923-0.0267-0.7092-4e-3 -1.4189-4e-3 -2.1279-4e-3h-22.235c-0.7097 0-1.4186 0-2.1276 0.0041-0.5977 0.0037-1.1955 0.0105-1.7923 0.0267-0.3254 0.0088-0.6515 0.0202-0.9778 0.0359-0.9788 0.0472-1.9591 0.1337-2.9243 0.3076-0.9793 0.176-1.9079 0.4356-2.8113 0.8091-0.3011 0.1244-0.5995 0.2616-0.8961 0.4125-0.8748 0.4452-1.6938 0.9927-2.4387 1.6289-0.2483 0.2121-0.4884 0.434-0.7196 0.6653-0.9254 0.9252-1.701 1.9921-2.2943 3.158-0.6044 1.1872-0.9874 2.4024-1.2222 3.7093-0.231 1.2869-0.3078 2.5995-0.3428 3.9003-0.0164 0.598-0.0233 1.195-0.0272 1.7923-0.0045 0.7094-0.0039 1.4189-0.0039 2.1281v22.234c0 0.7099-7e-4 1.4187 0.0039 2.1286 0.0039 0.5973 0.0108 1.1943 0.0272 1.7913 0.035 1.3015 0.1117 2.6144 0.3428 3.9007 0.2348 1.3065 0.6178 2.5228 1.2222 3.7097 0.5933 1.1662 1.3689 2.2328 2.2943 3.1576 0.9247 0.9256 1.9919 1.701 3.1584 2.295 1.1863 0.6038 2.4016 0.9867 3.7076 1.2213 1.2868 0.2316 2.6004 0.3086 3.9019 0.3434 0.5968 0.0159 1.1946 0.023 1.7923 0.0264 0.709 0.0051 1.4179 0.0044 2.1276 0.0044h22.235c0.709 0 1.4187 7e-4 2.1278-0.0044 0.5975-0.0034 1.1946-0.0105 1.7923-0.0264 1.3018-0.0348 2.6144-0.1119 3.902-0.3434 1.3057-0.2346 2.5211-0.6176 3.7073-1.2213 1.1664-0.5939 2.2333-1.3694 3.1583-2.295 0.9252-0.9249 1.7009-1.9914 2.2941-3.1576 0.6044-1.1869 0.9879-2.4031 1.2225-3.7097 0.2312-1.2863 0.3081-2.5992 0.3429-3.9007 0.0164-0.597 0.023-1.1939 0.0266-1.7913 0.0046-0.7099 0.0042-1.4187 0.0042-2.1286v-22.234c1e-4 -0.7092 4e-4 -1.4187-0.0041-2.128z" fill="#F05138"/>
+	<path d="m47.061 36.661c-0.0014-0.0018-0.0027-0.0031-0.0042-0.0048 0.0657-0.2236 0.1335-0.4458 0.191-0.675 2.465-9.8209-3.5511-21.432-13.732-27.545 4.4613 6.0479 6.4339 13.373 4.6813 19.78-0.1563 0.5714-0.3442 1.1198-0.5519 1.6528-0.2254-0.1481-0.5094-0.3162-0.8908-0.5265 0 0-10.127-6.2527-21.103-17.312-0.288-0.2903 5.8528 8.777 12.822 16.14-3.2834-1.8427-12.434-8.5004-18.227-13.802 0.7117 1.1869 1.5582 2.3298 2.4887 3.4301 4.8375 6.1349 11.146 13.704 18.704 19.517-5.3104 3.2498-12.814 3.5025-20.285 0.0034-1.8479-0.866-3.5851-1.9109-5.1932-3.0981 3.1625 5.0585 8.0332 9.4229 13.961 11.971 7.0695 3.0381 14.1 2.8321 19.336 0.0498l-0.0041 6e-3c0.0239-0.0151 0.0543-0.0316 0.0791-0.0469 0.215-0.1156 0.4284-0.2333 0.6371-0.3576 2.5157-1.3058 7.4847-2.6306 10.152 2.5588 0.6532 1.27 2.0412-5.4604-3.0617-11.739z" fill="#fff"/>
+</svg>