code.gitea.io/gitea@v1.22.3/modules/packages/swift/metadata.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package swift
     5  
     6  import (
     7  	"archive/zip"
     8  	"fmt"
     9  	"io"
    10  	"path"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/modules/json"
    15  	"code.gitea.io/gitea/modules/util"
    16  	"code.gitea.io/gitea/modules/validation"
    17  
    18  	"github.com/hashicorp/go-version"
    19  )
    20  
    21  var (
    22  	ErrMissingManifestFile    = util.NewInvalidArgumentErrorf("Package.swift file is missing")
    23  	ErrManifestFileTooLarge   = util.NewInvalidArgumentErrorf("Package.swift file is too large")
    24  	ErrInvalidManifestVersion = util.NewInvalidArgumentErrorf("manifest version is invalid")
    25  
    26  	manifestPattern     = regexp.MustCompile(`\APackage(?:@swift-(\d+(?:\.\d+)?(?:\.\d+)?))?\.swift\z`)
    27  	toolsVersionPattern = regexp.MustCompile(`\A// swift-tools-version:(\d+(?:\.\d+)?(?:\.\d+)?)`)
    28  )
    29  
    30  const (
    31  	maxManifestFileSize = 128 * 1024
    32  
    33  	PropertyScope         = "swift.scope"
    34  	PropertyName          = "swift.name"
    35  	PropertyRepositoryURL = "swift.repository_url"
    36  )
    37  
    38  // Package represents a Swift package
    39  type Package struct {
    40  	RepositoryURLs []string
    41  	Metadata       *Metadata
    42  }
    43  
    44  // Metadata represents the metadata of a Swift package
    45  type Metadata struct {
    46  	Description   string               `json:"description,omitempty"`
    47  	Keywords      []string             `json:"keywords,omitempty"`
    48  	RepositoryURL string               `json:"repository_url,omitempty"`
    49  	License       string               `json:"license,omitempty"`
    50  	Author        Person               `json:"author,omitempty"`
    51  	Manifests     map[string]*Manifest `json:"manifests,omitempty"`
    52  }
    53  
    54  // Manifest represents a Package.swift file
    55  type Manifest struct {
    56  	Content      string `json:"content"`
    57  	ToolsVersion string `json:"tools_version,omitempty"`
    58  }
    59  
    60  // https://schema.org/SoftwareSourceCode
    61  type SoftwareSourceCode struct {
    62  	Context             []string            `json:"@context"`
    63  	Type                string              `json:"@type"`
    64  	Name                string              `json:"name"`
    65  	Version             string              `json:"version"`
    66  	Description         string              `json:"description,omitempty"`
    67  	Keywords            []string            `json:"keywords,omitempty"`
    68  	CodeRepository      string              `json:"codeRepository,omitempty"`
    69  	License             string              `json:"license,omitempty"`
    70  	Author              Person              `json:"author"`
    71  	ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
    72  	RepositoryURLs      []string            `json:"repositoryURLs,omitempty"`
    73  }
    74  
    75  // https://schema.org/ProgrammingLanguage
    76  type ProgrammingLanguage struct {
    77  	Type string `json:"@type"`
    78  	Name string `json:"name"`
    79  	URL  string `json:"url"`
    80  }
    81  
    82  // https://schema.org/Person
    83  type Person struct {
    84  	Type       string `json:"@type,omitempty"`
    85  	GivenName  string `json:"givenName,omitempty"`
    86  	MiddleName string `json:"middleName,omitempty"`
    87  	FamilyName string `json:"familyName,omitempty"`
    88  }
    89  
    90  func (p Person) String() string {
    91  	var sb strings.Builder
    92  	if p.GivenName != "" {
    93  		sb.WriteString(p.GivenName)
    94  	}
    95  	if p.MiddleName != "" {
    96  		if sb.Len() > 0 {
    97  			sb.WriteRune(' ')
    98  		}
    99  		sb.WriteString(p.MiddleName)
   100  	}
   101  	if p.FamilyName != "" {
   102  		if sb.Len() > 0 {
   103  			sb.WriteRune(' ')
   104  		}
   105  		sb.WriteString(p.FamilyName)
   106  	}
   107  	return sb.String()
   108  }
   109  
   110  // ParsePackage parses the Swift package upload
   111  func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
   112  	zr, err := zip.NewReader(sr, size)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	p := &Package{
   118  		Metadata: &Metadata{
   119  			Manifests: make(map[string]*Manifest),
   120  		},
   121  	}
   122  
   123  	for _, file := range zr.File {
   124  		manifestMatch := manifestPattern.FindStringSubmatch(path.Base(file.Name))
   125  		if len(manifestMatch) == 0 {
   126  			continue
   127  		}
   128  
   129  		if file.UncompressedSize64 > maxManifestFileSize {
   130  			return nil, ErrManifestFileTooLarge
   131  		}
   132  
   133  		f, err := zr.Open(file.Name)
   134  		if err != nil {
   135  			return nil, err
   136  		}
   137  
   138  		content, err := io.ReadAll(f)
   139  
   140  		if err := f.Close(); err != nil {
   141  			return nil, err
   142  		}
   143  
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  
   148  		swiftVersion := ""
   149  		if len(manifestMatch) == 2 && manifestMatch[1] != "" {
   150  			v, err := version.NewSemver(manifestMatch[1])
   151  			if err != nil {
   152  				return nil, ErrInvalidManifestVersion
   153  			}
   154  			swiftVersion = TrimmedVersionString(v)
   155  		}
   156  
   157  		manifest := &Manifest{
   158  			Content: string(content),
   159  		}
   160  
   161  		toolsMatch := toolsVersionPattern.FindStringSubmatch(manifest.Content)
   162  		if len(toolsMatch) == 2 {
   163  			v, err := version.NewSemver(toolsMatch[1])
   164  			if err != nil {
   165  				return nil, ErrInvalidManifestVersion
   166  			}
   167  
   168  			manifest.ToolsVersion = TrimmedVersionString(v)
   169  		}
   170  
   171  		p.Metadata.Manifests[swiftVersion] = manifest
   172  	}
   173  
   174  	if _, found := p.Metadata.Manifests[""]; !found {
   175  		return nil, ErrMissingManifestFile
   176  	}
   177  
   178  	if mr != nil {
   179  		var ssc *SoftwareSourceCode
   180  		if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
   181  			return nil, err
   182  		}
   183  
   184  		p.Metadata.Description = ssc.Description
   185  		p.Metadata.Keywords = ssc.Keywords
   186  		p.Metadata.License = ssc.License
   187  		p.Metadata.Author = Person{
   188  			GivenName:  ssc.Author.GivenName,
   189  			MiddleName: ssc.Author.MiddleName,
   190  			FamilyName: ssc.Author.FamilyName,
   191  		}
   192  
   193  		p.Metadata.RepositoryURL = ssc.CodeRepository
   194  		if !validation.IsValidURL(p.Metadata.RepositoryURL) {
   195  			p.Metadata.RepositoryURL = ""
   196  		}
   197  
   198  		p.RepositoryURLs = ssc.RepositoryURLs
   199  	}
   200  
   201  	return p, nil
   202  }
   203  
   204  // TrimmedVersionString returns the version string without the patch segment if it is zero
   205  func TrimmedVersionString(v *version.Version) string {
   206  	segments := v.Segments64()
   207  
   208  	var b strings.Builder
   209  	fmt.Fprintf(&b, "%d.%d", segments[0], segments[1])
   210  	if segments[2] != 0 {
   211  		fmt.Fprintf(&b, ".%d", segments[2])
   212  	}
   213  	return b.String()
   214  }