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 }