code.gitea.io/gitea@v1.19.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 } 56 57 // Metadata represents the metadata of a Nuget package 58 type Metadata struct { 59 Description string `json:"description,omitempty"` 60 ReleaseNotes string `json:"release_notes,omitempty"` 61 Authors string `json:"authors,omitempty"` 62 ProjectURL string `json:"project_url,omitempty"` 63 RepositoryURL string `json:"repository_url,omitempty"` 64 RequireLicenseAcceptance bool `json:"require_license_acceptance"` 65 Dependencies map[string][]Dependency `json:"dependencies,omitempty"` 66 } 67 68 // Dependency represents a dependency of a Nuget package 69 type Dependency struct { 70 ID string `json:"id"` 71 Version string `json:"version"` 72 } 73 74 type nuspecPackage struct { 75 Metadata struct { 76 ID string `xml:"id"` 77 Version string `xml:"version"` 78 Authors string `xml:"authors"` 79 RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` 80 ProjectURL string `xml:"projectUrl"` 81 Description string `xml:"description"` 82 ReleaseNotes string `xml:"releaseNotes"` 83 PackageTypes struct { 84 PackageType []struct { 85 Name string `xml:"name,attr"` 86 } `xml:"packageType"` 87 } `xml:"packageTypes"` 88 Repository struct { 89 URL string `xml:"url,attr"` 90 } `xml:"repository"` 91 Dependencies struct { 92 Group []struct { 93 TargetFramework string `xml:"targetFramework,attr"` 94 Dependency []struct { 95 ID string `xml:"id,attr"` 96 Version string `xml:"version,attr"` 97 Exclude string `xml:"exclude,attr"` 98 } `xml:"dependency"` 99 } `xml:"group"` 100 } `xml:"dependencies"` 101 } `xml:"metadata"` 102 } 103 104 // ParsePackageMetaData parses the metadata of a Nuget package file 105 func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { 106 archive, err := zip.NewReader(r, size) 107 if err != nil { 108 return nil, err 109 } 110 111 for _, file := range archive.File { 112 if filepath.Dir(file.Name) != "." { 113 continue 114 } 115 if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") { 116 if file.UncompressedSize64 > maxNuspecFileSize { 117 return nil, ErrNuspecFileTooLarge 118 } 119 f, err := archive.Open(file.Name) 120 if err != nil { 121 return nil, err 122 } 123 defer f.Close() 124 125 return ParseNuspecMetaData(f) 126 } 127 } 128 return nil, ErrMissingNuspecFile 129 } 130 131 // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package 132 func ParseNuspecMetaData(r io.Reader) (*Package, error) { 133 var p nuspecPackage 134 if err := xml.NewDecoder(r).Decode(&p); err != nil { 135 return nil, err 136 } 137 138 if !idmatch.MatchString(p.Metadata.ID) { 139 return nil, ErrNuspecInvalidID 140 } 141 142 v, err := version.NewSemver(p.Metadata.Version) 143 if err != nil { 144 return nil, ErrNuspecInvalidVersion 145 } 146 147 if !validation.IsValidURL(p.Metadata.ProjectURL) { 148 p.Metadata.ProjectURL = "" 149 } 150 151 packageType := DependencyPackage 152 for _, pt := range p.Metadata.PackageTypes.PackageType { 153 if pt.Name == "SymbolsPackage" { 154 packageType = SymbolsPackage 155 break 156 } 157 } 158 159 m := &Metadata{ 160 Description: p.Metadata.Description, 161 ReleaseNotes: p.Metadata.ReleaseNotes, 162 Authors: p.Metadata.Authors, 163 ProjectURL: p.Metadata.ProjectURL, 164 RepositoryURL: p.Metadata.Repository.URL, 165 RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, 166 Dependencies: make(map[string][]Dependency), 167 } 168 169 for _, group := range p.Metadata.Dependencies.Group { 170 deps := make([]Dependency, 0, len(group.Dependency)) 171 for _, dep := range group.Dependency { 172 if dep.ID == "" || dep.Version == "" { 173 continue 174 } 175 deps = append(deps, Dependency{ 176 ID: dep.ID, 177 Version: dep.Version, 178 }) 179 } 180 if len(deps) > 0 { 181 m.Dependencies[group.TargetFramework] = deps 182 } 183 } 184 return &Package{ 185 PackageType: packageType, 186 ID: p.Metadata.ID, 187 Version: toNormalizedVersion(v), 188 Metadata: m, 189 }, nil 190 } 191 192 // https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers 193 // https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121 194 func toNormalizedVersion(v *version.Version) string { 195 var buf bytes.Buffer 196 segments := v.Segments64() 197 fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) 198 if len(segments) > 3 && segments[3] > 0 { 199 fmt.Fprintf(&buf, ".%d", segments[3]) 200 } 201 pre := v.Prerelease() 202 if pre != "" { 203 fmt.Fprint(&buf, "-", pre) 204 } 205 return buf.String() 206 }