mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
229 lines
6.1 KiB
Go
229 lines
6.1 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package swift
|
|
|
|
import (
|
|
"archive/zip"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/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"`
|
|
LicenseURL string `json:"license_url,omitempty"`
|
|
Author Person `json:"author"`
|
|
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"`
|
|
LicenseURL string `json:"licenseURL,omitempty"`
|
|
Author *Person `json:"author,omitempty"`
|
|
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"`
|
|
Name string `json:"name,omitempty"` // inherited from https://schema.org/Thing
|
|
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.LicenseURL = ssc.LicenseURL
|
|
if ssc.Author != nil {
|
|
author := Person{
|
|
Name: ssc.Author.Name,
|
|
GivenName: ssc.Author.GivenName,
|
|
MiddleName: ssc.Author.MiddleName,
|
|
FamilyName: ssc.Author.FamilyName,
|
|
}
|
|
// If Name is not provided, generate it from individual name components
|
|
if author.Name == "" {
|
|
author.Name = author.String()
|
|
}
|
|
p.Metadata.Author = author
|
|
}
|
|
|
|
p.Metadata.RepositoryURL = ssc.CodeRepository
|
|
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
|
|
p.Metadata.RepositoryURL = ""
|
|
}
|
|
if !validation.IsValidURL(p.Metadata.LicenseURL) {
|
|
p.Metadata.LicenseURL = ""
|
|
}
|
|
|
|
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()
|
|
}
|