diff options
Diffstat (limited to 'modules/packages/swift/metadata.go')
-rw-r--r-- | modules/packages/swift/metadata.go | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go new file mode 100644 index 0000000000..24c4262ab7 --- /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() +} |