mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Add Swift package registry (#22404)
This PR adds a [Swift](https://www.swift.org/) package registry. 
This commit is contained in:
		
							
								
								
									
										214
									
								
								modules/packages/swift/metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								modules/packages/swift/metadata.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| } | ||||
							
								
								
									
										144
									
								
								modules/packages/swift/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								modules/packages/swift/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user