code.gitea.io/gitea@v1.22.3/modules/packages/nuget/metadata.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package nuget 5 6 import ( 7 "archive/zip" 8 "bytes" 9 "encoding/xml" 10 "fmt" 11 "io" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 "code.gitea.io/gitea/modules/util" 17 "code.gitea.io/gitea/modules/validation" 18 19 "github.com/hashicorp/go-version" 20 ) 21 22 var ( 23 // ErrMissingNuspecFile indicates a missing Nuspec file 24 ErrMissingNuspecFile = util.NewInvalidArgumentErrorf("Nuspec file is missing") 25 // ErrNuspecFileTooLarge indicates a Nuspec file which is too large 26 ErrNuspecFileTooLarge = util.NewInvalidArgumentErrorf("Nuspec file is too large") 27 // ErrNuspecInvalidID indicates an invalid id in the Nuspec file 28 ErrNuspecInvalidID = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid id") 29 // ErrNuspecInvalidVersion indicates an invalid version in the Nuspec file 30 ErrNuspecInvalidVersion = util.NewInvalidArgumentErrorf("Nuspec file contains an invalid version") 31 ) 32 33 // PackageType specifies the package type the metadata describes 34 type PackageType int 35 36 const ( 37 // DependencyPackage represents a package (*.nupkg) 38 DependencyPackage PackageType = iota + 1 39 // SymbolsPackage represents a symbol package (*.snupkg) 40 SymbolsPackage 41 42 PropertySymbolID = "nuget.symbol.id" 43 ) 44 45 var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`) 46 47 const maxNuspecFileSize = 3 * 1024 * 1024 48 49 // Package represents a Nuget package 50 type Package struct { 51 PackageType PackageType 52 ID string 53 Version string 54 Metadata *Metadata 55 NuspecContent *bytes.Buffer 56 } 57 58 // Metadata represents the metadata of a Nuget package 59 type Metadata struct { 60 Description string `json:"description,omitempty"` 61 ReleaseNotes string `json:"release_notes,omitempty"` 62 Readme string `json:"readme,omitempty"` 63 Authors string `json:"authors,omitempty"` 64 ProjectURL string `json:"project_url,omitempty"` 65 RepositoryURL string `json:"repository_url,omitempty"` 66 RequireLicenseAcceptance bool `json:"require_license_acceptance"` 67 Dependencies map[string][]Dependency `json:"dependencies,omitempty"` 68 } 69 70 // Dependency represents a dependency of a Nuget package 71 type Dependency struct { 72 ID string `json:"id"` 73 Version string `json:"version"` 74 } 75 76 // https://learn.microsoft.com/en-us/nuget/reference/nuspec 77 type nuspecPackage struct { 78 Metadata struct { 79 ID string `xml:"id"` 80 Version string `xml:"version"` 81 Authors string `xml:"authors"` 82 RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` 83 ProjectURL string `xml:"projectUrl"` 84 Description string `xml:"description"` 85 ReleaseNotes string `xml:"releaseNotes"` 86 Readme string `xml:"readme"` 87 PackageTypes struct { 88 PackageType []struct { 89 Name string `xml:"name,attr"` 90 } `xml:"packageType"` 91 } `xml:"packageTypes"` 92 Repository struct { 93 URL string `xml:"url,attr"` 94 } `xml:"repository"` 95 Dependencies struct { 96 Dependency []struct { 97 ID string `xml:"id,attr"` 98 Version string `xml:"version,attr"` 99 Exclude string `xml:"exclude,attr"` 100 } `xml:"dependency"` 101 Group []struct { 102 TargetFramework string `xml:"targetFramework,attr"` 103 Dependency []struct { 104 ID string `xml:"id,attr"` 105 Version string `xml:"version,attr"` 106 Exclude string `xml:"exclude,attr"` 107 } `xml:"dependency"` 108 } `xml:"group"` 109 } `xml:"dependencies"` 110 } `xml:"metadata"` 111 } 112 113 // ParsePackageMetaData parses the metadata of a Nuget package file 114 func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { 115 archive, err := zip.NewReader(r, size) 116 if err != nil { 117 return nil, err 118 } 119 120 for _, file := range archive.File { 121 if filepath.Dir(file.Name) != "." { 122 continue 123 } 124 if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") { 125 if file.UncompressedSize64 > maxNuspecFileSize { 126 return nil, ErrNuspecFileTooLarge 127 } 128 f, err := archive.Open(file.Name) 129 if err != nil { 130 return nil, err 131 } 132 defer f.Close() 133 134 return ParseNuspecMetaData(archive, f) 135 } 136 } 137 return nil, ErrMissingNuspecFile 138 } 139 140 // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package 141 func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { 142 var nuspecBuf bytes.Buffer 143 var p nuspecPackage 144 if err := xml.NewDecoder(io.TeeReader(r, &nuspecBuf)).Decode(&p); err != nil { 145 return nil, err 146 } 147 148 if !idmatch.MatchString(p.Metadata.ID) { 149 return nil, ErrNuspecInvalidID 150 } 151 152 v, err := version.NewSemver(p.Metadata.Version) 153 if err != nil { 154 return nil, ErrNuspecInvalidVersion 155 } 156 157 if !validation.IsValidURL(p.Metadata.ProjectURL) { 158 p.Metadata.ProjectURL = "" 159 } 160 161 packageType := DependencyPackage 162 for _, pt := range p.Metadata.PackageTypes.PackageType { 163 if pt.Name == "SymbolsPackage" { 164 packageType = SymbolsPackage 165 break 166 } 167 } 168 169 m := &Metadata{ 170 Description: p.Metadata.Description, 171 ReleaseNotes: p.Metadata.ReleaseNotes, 172 Authors: p.Metadata.Authors, 173 ProjectURL: p.Metadata.ProjectURL, 174 RepositoryURL: p.Metadata.Repository.URL, 175 RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, 176 Dependencies: make(map[string][]Dependency), 177 } 178 179 if p.Metadata.Readme != "" { 180 f, err := archive.Open(p.Metadata.Readme) 181 if err == nil { 182 buf, _ := io.ReadAll(f) 183 m.Readme = string(buf) 184 _ = f.Close() 185 } 186 } 187 188 if len(p.Metadata.Dependencies.Dependency) > 0 { 189 deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency)) 190 for _, dep := range p.Metadata.Dependencies.Dependency { 191 if dep.ID == "" || dep.Version == "" { 192 continue 193 } 194 deps = append(deps, Dependency{ 195 ID: dep.ID, 196 Version: dep.Version, 197 }) 198 } 199 m.Dependencies[""] = deps 200 } 201 for _, group := range p.Metadata.Dependencies.Group { 202 deps := make([]Dependency, 0, len(group.Dependency)) 203 for _, dep := range group.Dependency { 204 if dep.ID == "" || dep.Version == "" { 205 continue 206 } 207 deps = append(deps, Dependency{ 208 ID: dep.ID, 209 Version: dep.Version, 210 }) 211 } 212 if len(deps) > 0 { 213 m.Dependencies[group.TargetFramework] = deps 214 } 215 } 216 return &Package{ 217 PackageType: packageType, 218 ID: p.Metadata.ID, 219 Version: toNormalizedVersion(v), 220 Metadata: m, 221 NuspecContent: &nuspecBuf, 222 }, nil 223 } 224 225 // https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers 226 // https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121 227 func toNormalizedVersion(v *version.Version) string { 228 var buf bytes.Buffer 229 segments := v.Segments64() 230 fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) 231 if len(segments) > 3 && segments[3] > 0 { 232 fmt.Fprintf(&buf, ".%d", segments[3]) 233 } 234 pre := v.Prerelease() 235 if pre != "" { 236 fmt.Fprint(&buf, "-", pre) 237 } 238 return buf.String() 239 }